{"jsonrpc":"2.0","id":"","result":{"genesis":{"genesis_time":"2026-10-21T16:11:46Z","chain_id":"test-ibc-1","consensus_params":{"Block":{"MaxTxBytes":"1000000","MaxDataBytes":"2000000","MaxBlockBytes":"0","MaxGas":"3000000000","TimeIotaMS":"100"},"Validator":{"PubKeyTypeURLs":["/tm.PubKeySecp256k1"]}},"validators":[{"address":"g1ha9qz79y4ururxxsa9k52dwuuu7rzj2xavtyye","pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AotQOMq2h95CyOXxyAF8MyKhB2Vwmp2ddpKFI96r1PTU"},"power":"10","name":"val0"},{"address":"g1wgdmlmkaclrfgg0nz99xdzwtwj575fvvs06f9y","pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Ascpe6DfPmeG3E4v9D84njIzG8M27vPcg/CI5/JeIgNW"},"power":"10","name":"val1"},{"address":"g1uh8ja6juqhpvz5jwd2mqdqt472vgp50eldgtwl","pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A/6yNjV7K0WOd4ZJpeSw9RZXuGeUUxTwsDq8DJO8LmS+"},"power":"10","name":"val2"},{"address":"g1qx6vp3en22ny6vl0sm203xqcwsle2vzk4xxknc","pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6yUrRY2oS3MrG/P2fjregO1tNudkcyB+UyIs/VFbhBP"},"power":"10","name":"val3"},{"address":"g1sam5cj8pc596y3krrhdr70f8csyms679ydkgw0","pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Ar6Mp8+KvM8q+XWCJOlXWbdu0Sv6pp80RZD+rmMmIX1c"},"power":"10","name":"val4"}],"app_hash":null,"app_state":{"@type":"/gno.GenesisState","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":"100000000ugnot","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","code_submission_policy":"permissionless","code_submitters":null,"pkg_approvers":null,"run_submitters":null,"inert_submission_charge":"","inert_charge_collector":"g1fgj3pu3rd7myc5hdk9q4wql8s96us5zqsyftw5"},"realm_params":null},"balances":["g1q392jxrvawgfe3q5tu585ap5j99frzkhcqzsu0=100000000000ugnot","g1qapdp8av4xjf5ktavtqgluc25kh3sts2wfg8c8=100000000000000ugnot","g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx=10000000000000ugnot","g1vk6zdqj9kg2lhth8rqdf3des2ucpp76m4lzq9q=100000000000ugnot","g1dqnvmvlp5nq4uy7why0qw8lyq2xe0h0ulal8yy=10000000000ugnot","g1dynw7thjvgpj7y2efvy7x3e35f2p96ftzgx6aw=10000000000ugnot","g1s2fm43qel0stvuyeyjdzjq6muu9x4fgmuxs3xx=10000000000ugnot","g1jqqfy6fq3z85xduazzqecnfrg6kwy6yjaakan6=10000000000ugnot","g1n3j4l5fxlslex34knj0swpqgsw4jna4w66l88t=10000000000ugnot","g1kg6w7a4f0kup8wj9y33jdkqvvux7n559avyzcs=1000000000000ugnot","g1hc9ad9quga48r8tvadwj7nsaf7m866h0k3rzth=1000000000000ugnot","g16pcnylx43k0rp272jnhzyp490acmns24qrgua6=100000000000ugnot","g1llaf5vpg5tcjdj7l4z7eq9jplemu5jtqp249n3=10000000000ugnot"],"txs":[{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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, up to 64 bits; see below).              |\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`int256` and `uint256` are not supported by any verb (`%d`, `%v`, ...) or by\n`Sprint`/`Println`: they print as unhandled values. Format them with\n`strconv.FormatUint256(v, 10)` / `strconv.FormatInt256(v, 10)`. Keeping the\n256-bit types out of ufmt keeps ufmt, and the packages that import it,\nbuildable by `gno tool transpile --gobuild`, which skips any package using a\ntype Go does not have.\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"},{"name":"ufmt_test.gno","body":"package ufmt\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"testing\"\n)\n\ntype stringer struct{}\n\nfunc (stringer) String() string {\n\treturn \"I'm a stringer\"\n}\n\nfunc TestSprintf(t *testing.T) {\n\ttru := true\n\tcases := []struct {\n\t\tformat         string\n\t\tvalues         []any\n\t\texpectedOutput string\n\t}{\n\t\t{\"hello %s!\", []any{\"planet\"}, \"hello planet!\"},\n\t\t{\"hello %v!\", []any{\"planet\"}, \"hello planet!\"},\n\t\t{\"hi %%%s!\", []any{\"worl%d\"}, \"hi %worl%d!\"},\n\t\t{\"%s %c %d %t\", []any{\"foo\", 'α', 421, true}, \"foo α 421 true\"},\n\t\t{\"string [%s]\", []any{\"foo\"}, \"string [foo]\"},\n\t\t{\"int [%d]\", []any{int(42)}, \"int [42]\"},\n\t\t{\"int [%v]\", []any{int(42)}, \"int [42]\"},\n\t\t{\"int8 [%d]\", []any{int8(8)}, \"int8 [8]\"},\n\t\t{\"int8 [%v]\", []any{int8(8)}, \"int8 [8]\"},\n\t\t{\"int16 [%d]\", []any{int16(16)}, \"int16 [16]\"},\n\t\t{\"int16 [%v]\", []any{int16(16)}, \"int16 [16]\"},\n\t\t{\"int32 [%d]\", []any{int32(32)}, \"int32 [32]\"},\n\t\t{\"int32 [%v]\", []any{int32(32)}, \"int32 [32]\"},\n\t\t{\"int64 [%d]\", []any{int64(64)}, \"int64 [64]\"},\n\t\t{\"int64 [%v]\", []any{int64(64)}, \"int64 [64]\"},\n\t\t{\"uint [%d]\", []any{uint(42)}, \"uint [42]\"},\n\t\t{\"uint [%v]\", []any{uint(42)}, \"uint [42]\"},\n\t\t{\"uint8 [%d]\", []any{uint8(8)}, \"uint8 [8]\"},\n\t\t{\"uint8 [%v]\", []any{uint8(8)}, \"uint8 [8]\"},\n\t\t{\"uint16 [%d]\", []any{uint16(16)}, \"uint16 [16]\"},\n\t\t{\"uint16 [%v]\", []any{uint16(16)}, \"uint16 [16]\"},\n\t\t{\"uint32 [%d]\", []any{uint32(32)}, \"uint32 [32]\"},\n\t\t{\"uint32 [%v]\", []any{uint32(32)}, \"uint32 [32]\"},\n\t\t{\"uint64 [%d]\", []any{uint64(64)}, \"uint64 [64]\"},\n\t\t{\"uint64 [%v]\", []any{uint64(64)}, \"uint64 [64]\"},\n\t\t{\"float64 [%e]\", []any{float64(64.1)}, \"float64 [6.41e+01]\"},\n\t\t{\"float64 [%E]\", []any{float64(64.1)}, \"float64 [6.41E+01]\"},\n\t\t{\"float64 [%f]\", []any{float64(64.1)}, \"float64 [64.100000]\"},\n\t\t{\"float64 [%F]\", []any{float64(64.1)}, \"float64 [64.100000]\"},\n\t\t{\"float64 [%g]\", []any{float64(64.1)}, \"float64 [64.1]\"},\n\t\t{\"float64 [%G]\", []any{float64(64.1)}, \"float64 [64.1]\"},\n\t\t{\"bool [%t]\", []any{true}, \"bool [true]\"},\n\t\t{\"bool [%v]\", []any{true}, \"bool [true]\"},\n\t\t{\"bool [%t]\", []any{false}, \"bool [false]\"},\n\t\t{\"bool [%v]\", []any{false}, \"bool [false]\"},\n\t\t{\"no args\", nil, \"no args\"},\n\t\t{\"finish with %\", nil, \"finish with %\"},\n\t\t{\"stringer [%s]\", []any{stringer{}}, \"stringer [I'm a stringer]\"},\n\t\t{\"â\", nil, \"â\"},\n\t\t{\"Hello, World! 😊\", nil, \"Hello, World! 😊\"},\n\t\t{\"unicode formatting: %s\", []any{\"😊\"}, \"unicode formatting: 😊\"},\n\t\t{\"invalid hex [%x]\", []any{\"invalid\"}, \"invalid hex [(unhandled)]\"},\n\t\t{\"rune as character [%c]\", []any{rune('A')}, \"rune as character [A]\"},\n\t\t{\"int as character [%c]\", []any{int('B')}, \"int as character [B]\"},\n\t\t{\"quoted string [%q]\", []any{\"hello\"}, \"quoted string [\\\"hello\\\"]\"},\n\t\t{\"quoted string with escape [%q]\", []any{\"\\thello\\nworld\\\\\"}, \"quoted string with escape [\\\"\\\\thello\\\\nworld\\\\\\\\\\\"]\"},\n\t\t{\"invalid quoted string [%q]\", []any{123}, \"invalid quoted string [(unhandled)]\"},\n\t\t{\"type of bool [%T]\", []any{true}, \"type of bool [bool]\"},\n\t\t{\"type of int [%T]\", []any{123}, \"type of int [int]\"},\n\t\t{\"type of string [%T]\", []any{\"hello\"}, \"type of string [string]\"},\n\t\t{\"type of []byte [%T]\", []any{[]byte{1, 2, 3}}, \"type of []byte [[]byte]\"},\n\t\t{\"type of []rune [%T]\", []any{[]rune{'a', 'b', 'c'}}, \"type of []rune [[]rune]\"},\n\t\t{\"type of unknown [%T]\", []any{struct{}{}}, \"type of unknown [unknown]\"},\n\t\t// mismatch printing\n\t\t{\"%s\", []any{nil}, \"%!s(\u003cnil\u003e)\"},\n\t\t{\"%s\", []any{421}, \"%!s(int=421)\"},\n\t\t{\"%s\", []any{\"z\"}, \"z\"},\n\t\t{\"%s\", []any{tru}, \"%!s(bool=true)\"},\n\t\t{\"%s\", []any{'z'}, \"%!s(int32=122)\"},\n\n\t\t{\"%c\", []any{nil}, \"%!c(\u003cnil\u003e)\"},\n\t\t{\"%c\", []any{421}, \"ƥ\"},\n\t\t{\"%c\", []any{\"z\"}, \"%!c(string=z)\"},\n\t\t{\"%c\", []any{tru}, \"%!c(bool=true)\"},\n\t\t{\"%c\", []any{'z'}, \"z\"},\n\n\t\t{\"%d\", []any{nil}, \"%!d(\u003cnil\u003e)\"},\n\t\t{\"%d\", []any{421}, \"421\"},\n\t\t{\"%d\", []any{\"z\"}, \"%!d(string=z)\"},\n\t\t{\"%d\", []any{tru}, \"%!d(bool=true)\"},\n\t\t{\"%d\", []any{'z'}, \"122\"},\n\n\t\t{\"%t\", []any{nil}, \"%!t(\u003cnil\u003e)\"},\n\t\t{\"%t\", []any{421}, \"%!t(int=421)\"},\n\t\t{\"%t\", []any{\"z\"}, \"%!t(string=z)\"},\n\t\t{\"%t\", []any{tru}, \"true\"},\n\t\t{\"%t\", []any{'z'}, \"%!t(int32=122)\"},\n\n\t\t{\"%.2f\", []any{3.14159}, \"3.14\"},\n\t\t{\"%.4f\", []any{3.14159}, \"3.1416\"},\n\t\t{\"%.0f\", []any{3.14159}, \"3\"},\n\t\t{\"%.1f\", []any{3.0}, \"3.0\"},\n\t\t{\"%.3F\", []any{3.14159}, \"3.142\"},\n\t\t{\"%.2e\", []any{314.159}, \"3.14e+02\"},\n\t\t{\"%.3E\", []any{314.159}, \"3.142E+02\"},\n\t\t{\"%.3g\", []any{3.14159}, \"3.14\"},\n\t\t{\"%.5G\", []any{3.14159}, \"3.1416\"},\n\t\t{\"%.0f\", []any{3.6}, \"4\"},\n\t\t{\"%.0f\", []any{3.4}, \"3\"},\n\t\t{\"%.1f\", []any{0.0}, \"0.0\"},\n\t\t{\"%.2f\", []any{1e6}, \"1000000.00\"},\n\t\t{\"%.2f\", []any{1e-6}, \"0.00\"},\n\n\t\t{\"%5s\", []any{\"Hello World\"}, \"Hello World\"},\n\t\t{\"%3s\", []any{\"Hi\"}, \" Hi\"},\n\t\t{\"%2s\", []any{\"Hello\"}, \"Hello\"},\n\t\t{\"%1s\", []any{\"A\"}, \"A\"},\n\t\t{\"%0s\", []any{\"Test\"}, \"Test\"},\n\t\t{\"%5s!\", []any{\"Hello World\"}, \"Hello World!\"},\n\t\t{\"_%5s_\", []any{\"abc\"}, \"_  abc_\"},\n\t\t{\"%2s%4s\", []any{\"ab\", \"cde\"}, \"ab cde\"},\n\t\t{\"%5s\", []any{\"\"}, \"     \"},\n\t\t{\"%3s\", []any{nil}, \"%!s(\u003cnil\u003e)\"},\n\t\t{\"%2s\", []any{123}, \"%!s(int=123)\"},\n\t}\n\n\tfor _, tc := range cases {\n\t\tname := fmt.Sprintf(tc.format, tc.values...)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot := Sprintf(tc.format, tc.values...)\n\t\t\tif got != tc.expectedOutput {\n\t\t\t\tt.Errorf(\"got %q, want %q.\", got, tc.expectedOutput)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestErrorf(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tformat   string\n\t\targs     []any\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"simple string\",\n\t\t\tformat:   \"error: %s\",\n\t\t\targs:     []any{\"something went wrong\"},\n\t\t\texpected: \"error: something went wrong\",\n\t\t},\n\t\t{\n\t\t\tname:     \"integer value\",\n\t\t\tformat:   \"value: %d\",\n\t\t\targs:     []any{42},\n\t\t\texpected: \"value: 42\",\n\t\t},\n\t\t{\n\t\t\tname:     \"boolean value\",\n\t\t\tformat:   \"success: %t\",\n\t\t\targs:     []any{true},\n\t\t\texpected: \"success: true\",\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple values\",\n\t\t\tformat:   \"error %d: %s (success=%t)\",\n\t\t\targs:     []any{123, \"failure occurred\", false},\n\t\t\texpected: \"error 123: failure occurred (success=false)\",\n\t\t},\n\t\t{\n\t\t\tname:     \"literal percent\",\n\t\t\tformat:   \"literal %%\",\n\t\t\targs:     []any{},\n\t\t\texpected: \"literal %\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := Errorf(tt.format, tt.args...)\n\t\t\tif err.Error() != tt.expected {\n\t\t\t\tt.Errorf(\"Errorf(%q, %v) = %q, expected %q\", tt.format, tt.args, err.Error(), tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestPrintErrors(t *testing.T) {\n\tgot := Sprintf(\"error: %s\", errors.New(\"can I be printed?\"))\n\texpectedOutput := \"error: can I be printed?\"\n\tif got != expectedOutput {\n\t\tt.Errorf(\"got %q, want %q.\", got, expectedOutput)\n\t}\n}\n\nfunc TestSprint(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\targs     []any\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"Empty args\",\n\t\t\targs:     []any{},\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"String args\",\n\t\t\targs:     []any{\"Hello\", \"World\"},\n\t\t\texpected: \"Hello World\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Integer args\",\n\t\t\targs:     []any{1, 2, 3},\n\t\t\texpected: \"1 2 3\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Mixed args\",\n\t\t\targs:     []any{\"Hello\", 42, true, false, \"World\"},\n\t\t\texpected: \"Hello 42 true false World\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Unhandled type\",\n\t\t\targs:     []any{\"Hello\", 3.14, []int{1, 2, 3}},\n\t\t\texpected: \"Hello 3.140000 (unhandled)\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot := Sprint(tc.args...)\n\t\t\tif got != tc.expected {\n\t\t\t\tt.Errorf(\"got %q, want %q.\", got, tc.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFprintf(t *testing.T) {\n\tvar buf bytes.Buffer\n\tn, err := Fprintf(\u0026buf, \"Count: %d, Message: %s\", 42, \"hello\")\n\tif err != nil {\n\t\tt.Fatalf(\"Fprintf failed: %v\", err)\n\t}\n\n\tconst expected = \"Count: 42, Message: hello\"\n\tif buf.String() != expected {\n\t\tt.Errorf(\"Expected %q, got %q\", expected, buf.String())\n\t}\n\tif n != len(expected) {\n\t\tt.Errorf(\"Expected %d bytes written, got %d\", len(expected), n)\n\t}\n}\n\n// TODO: replace os.Stdout with a buffer to capture the output and test it.\nfunc TestPrintf(t *testing.T) {\n\tn, err := Printf(\"The answer is %d\", 42)\n\tif err != nil {\n\t\tt.Fatalf(\"Printf failed: %v\", err)\n\t}\n\n\tconst expected = \"The answer is 42\"\n\tif n != len(expected) {\n\t\tt.Errorf(\"Expected 14 bytes written, got %d\", n)\n\t}\n}\n\nfunc TestAppendf(t *testing.T) {\n\tb := []byte(\"Header: \")\n\tresult := Appendf(b, \"Value %d\", 7)\n\tconst expected = \"Header: Value 7\"\n\tif string(result) != expected {\n\t\tt.Errorf(\"Expected %q, got %q\", expected, string(result))\n\t}\n}\n\nfunc TestFprint(t *testing.T) {\n\tvar buf bytes.Buffer\n\tn, err := Fprint(\u0026buf, \"Hello\", 42, true)\n\tif err != nil {\n\t\tt.Fatalf(\"Fprint failed: %v\", err)\n\t}\n\n\tconst expected = \"Hello 42 true\"\n\tif buf.String() != expected {\n\t\tt.Errorf(\"Expected %q, got %q\", expected, buf.String())\n\t}\n\tif n != len(expected) {\n\t\tt.Errorf(\"Expected %d bytes written, got %d\", len(expected), n)\n\t}\n}\n\n// TODO: replace os.Stdout with a buffer to capture the output and test it.\nfunc TestPrint(t *testing.T) {\n\tn, err := Print(\"Mixed\", 3.14, false)\n\tif err != nil {\n\t\tt.Fatalf(\"Print failed: %v\", err)\n\t}\n\n\tconst expected = \"Mixed 3.140000 false\"\n\tif n != len(expected) {\n\t\tt.Errorf(\"Expected 12 bytes written, got %d\", n)\n\t}\n}\n\nfunc TestAppend(t *testing.T) {\n\tb := []byte{0x01, 0x02}\n\tresult := Append(b, \"Test\", 99)\n\n\tconst expected = \"\\x01\\x02Test 99\"\n\tif string(result) != expected {\n\t\tt.Errorf(\"Expected %q, got %q\", expected, string(result))\n\t}\n}\n\nfunc TestFprintln(t *testing.T) {\n\tvar buf bytes.Buffer\n\tn, err := Fprintln(\u0026buf, \"Line\", 1)\n\tif err != nil {\n\t\tt.Fatalf(\"Fprintln failed: %v\", err)\n\t}\n\n\tconst expected = \"Line 1\\n\"\n\tif buf.String() != expected {\n\t\tt.Errorf(\"Expected %q, got %q\", expected, buf.String())\n\t}\n\tif n != len(expected) {\n\t\tt.Errorf(\"Expected %d bytes written, got %d\", len(expected), n)\n\t}\n}\n\n// TODO: replace os.Stdout with a buffer to capture the output and test it.\nfunc TestPrintln(t *testing.T) {\n\tn, err := Println(\"Output\", \"test\")\n\tif err != nil {\n\t\tt.Fatalf(\"Println failed: %v\", err)\n\t}\n\n\tconst expected = \"Output test\\n\"\n\tif n != len(expected) {\n\t\tt.Errorf(\"Expected 12 bytes written, got %d\", n)\n\t}\n}\n\nfunc TestSprintln(t *testing.T) {\n\tresult := Sprintln(\"Item\", 42)\n\n\tconst expected = \"Item 42\\n\"\n\tif result != expected {\n\t\tt.Errorf(\"Expected %q, got %q\", expected, result)\n\t}\n}\n\nfunc TestAppendln(t *testing.T) {\n\tb := []byte(\"Start:\")\n\tresult := Appendln(b, \"End\")\n\n\tconst expected = \"Start:End\\n\"\n\tif string(result) != expected {\n\t\tt.Errorf(\"Expected %q, got %q\", expected, string(result))\n\t}\n}\n\nfunc assertNoError(t *testing.T, err error) {\n\tt.Helper()\n\tif err != nil {\n\t\tt.Fatalf(\"Unexpected error: %v\", err)\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"unelANucQDRQ9Elb4pm08FTvMje/M9uVq08VAOk6WwpXoyXsN8DBKZQc3j8vR3h9nHn2eqm7y8pHrovX1/RHGQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"diff","path":"gno.land/p/onbloc/diff/v0","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":"diff_test.gno","body":"package diff\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestMyersDiff(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\told      string\n\t\tnew      string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"No difference\",\n\t\t\told:      \"abc\",\n\t\t\tnew:      \"abc\",\n\t\t\texpected: \"abc\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Simple insertion\",\n\t\t\told:      \"ac\",\n\t\t\tnew:      \"abc\",\n\t\t\texpected: \"a[+b]c\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Simple deletion\",\n\t\t\told:      \"abc\",\n\t\t\tnew:      \"ac\",\n\t\t\texpected: \"a[-b]c\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Simple substitution\",\n\t\t\told:      \"abc\",\n\t\t\tnew:      \"abd\",\n\t\t\texpected: \"ab[-c][+d]\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Multiple changes\",\n\t\t\told:      \"The quick brown fox jumps over the lazy dog\",\n\t\t\tnew:      \"The quick brown cat jumps over the lazy dog\",\n\t\t\texpected: \"The quick brown [-fox][+cat] jumps over the lazy dog\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Prefix and suffix\",\n\t\t\told:      \"Hello, world!\",\n\t\t\tnew:      \"Hello, beautiful world!\",\n\t\t\texpected: \"Hello, [+beautiful ]world!\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Complete change\",\n\t\t\told:      \"abcdef\",\n\t\t\tnew:      \"ghijkl\",\n\t\t\texpected: \"[-abcdef][+ghijkl]\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Empty strings\",\n\t\t\told:      \"\",\n\t\t\tnew:      \"\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Old empty\",\n\t\t\told:      \"\",\n\t\t\tnew:      \"abc\",\n\t\t\texpected: \"[+abc]\",\n\t\t},\n\t\t{\n\t\t\tname:     \"New empty\",\n\t\t\told:      \"abc\",\n\t\t\tnew:      \"\",\n\t\t\texpected: \"[-abc]\",\n\t\t},\n\t\t{\n\t\t\tname:     \"non-ascii (Korean characters)\",\n\t\t\told:      \"ASCII 문자가 아닌 것도 되나?\",\n\t\t\tnew:      \"ASCII 문자가 아닌 것도 됨.\",\n\t\t\texpected: \"ASCII 문자가 아닌 것도 [-되나?][+됨.]\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Emoji diff\",\n\t\t\told:      \"Hello 👋 World 🌍\",\n\t\t\tnew:      \"Hello 👋 Beautiful 🌸 World 🌍\",\n\t\t\texpected: \"Hello 👋 [+Beautiful 🌸 ]World 🌍\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Mixed multibyte and ASCII\",\n\t\t\told:      \"こんにちは World\",\n\t\t\tnew:      \"こんばんは World\",\n\t\t\texpected: \"こん[-にち][+ばん]は World\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Chinese characters\",\n\t\t\told:      \"我喜欢编程\",\n\t\t\tnew:      \"我喜欢看书和编程\",\n\t\t\texpected: \"我喜欢[+看书和]编程\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Combining characters\",\n\t\t\told:      \"e\\u0301\", // é (e + ´)\n\t\t\tnew:      \"e\\u0300\", // è (e + `)\n\t\t\texpected: \"e[-\\u0301][+\\u0300]\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Right-to-Left languages\",\n\t\t\told:      \"שלום\",\n\t\t\tnew:      \"שלום עולם\",\n\t\t\texpected: \"שלום[+ עולם]\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Normalization NFC and NFD\",\n\t\t\told:      \"e\\u0301\", // NFD (decomposed)\n\t\t\tnew:      \"\\u00e9\",  // NFC (precomposed)\n\t\t\texpected: \"[-e\\u0301][+\\u00e9]\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Case sensitivity\",\n\t\t\told:      \"abc\",\n\t\t\tnew:      \"Abc\",\n\t\t\texpected: \"[-a][+A]bc\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Surrogate pairs\",\n\t\t\told:      \"Hello 🌍\",\n\t\t\tnew:      \"Hello 🌎\",\n\t\t\texpected: \"Hello [-🌍][+🌎]\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Control characters\",\n\t\t\told:      \"Line1\\nLine2\",\n\t\t\tnew:      \"Line1\\r\\nLine2\",\n\t\t\texpected: \"Line1[+\\r]\\nLine2\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Mixed scripts\",\n\t\t\told:      \"Hello नमस्ते こんにちは\",\n\t\t\tnew:      \"Hello สวัสดี こんにちは\",\n\t\t\texpected: \"Hello [-नमस्ते][+สวัสดี] こんにちは\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Unicode normalization\",\n\t\t\told:      \"é\",       // U+00E9 (precomposed)\n\t\t\tnew:      \"e\\u0301\", // U+0065 U+0301 (decomposed)\n\t\t\texpected: \"[-é][+e\\u0301]\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Directional marks\",\n\t\t\told:      \"Hello\\u200Eworld\", // LTR mark\n\t\t\tnew:      \"Hello\\u200Fworld\", // RTL mark\n\t\t\texpected: \"Hello[-\\u200E][+\\u200F]world\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Zero-width characters\",\n\t\t\told:      \"ab\\u200Bc\", // Zero-width space\n\t\t\tnew:      \"abc\",\n\t\t\texpected: \"ab[-\\u200B]c\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Worst-case scenario (completely different strings)\",\n\t\t\told:      strings.Repeat(\"a\", 1000),\n\t\t\tnew:      strings.Repeat(\"b\", 1000),\n\t\t\texpected: \"[-\" + strings.Repeat(\"a\", 1000) + \"][+\" + strings.Repeat(\"b\", 1000) + \"]\",\n\t\t},\n\t\t//{ // disabled for testing performance\n\t\t// XXX: consider adding a flag to run such tests, not like `-short`, or switching to a `-bench`, maybe.\n\t\t//\tname:     \"Very long strings\",\n\t\t//\told:      strings.Repeat(\"a\", 10000) + \"b\" + strings.Repeat(\"a\", 10000),\n\t\t//\tnew:      strings.Repeat(\"a\", 10000) + \"c\" + strings.Repeat(\"a\", 10000),\n\t\t//\texpected: strings.Repeat(\"a\", 10000) + \"[-b][+c]\" + strings.Repeat(\"a\", 10000),\n\t\t//},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tdiff := MyersDiff(tc.old, tc.new)\n\t\t\tresult := Format(diff)\n\t\t\tif result != tc.expected {\n\t\t\t\tt.Errorf(\"Expected: %s, got: %s\", tc.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/onbloc/diff/v0\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"Ku2X2TuYWuWiRk0Zg6Scoz8C3ljkJNgIExXFmC9PvoFko2zReeRPqf/y6hgIOMCeyMnm0AiD5oamOTJvENuRoA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"nestedpkg","path":"gno.land/p/nt/nestedpkg/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/nestedpkg/v0\"\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"+AJ0i9LWpbrEfO1Y/PrV8vgHO+X+Ko4wrozbkcpi/vBp2il5lGUbEp04DyAUtsu7aKg5RvkTZiR5oOD2lgOY/Q=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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":"crypto_test.gno","body":"package testutils\n\nimport (\n\t\"testing\"\n)\n\nfunc TestTestAddress(t *testing.T) {\n\ttestAddr := TestAddress(\"author1\")\n\tif string(testAddr) != \"g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6\" {\n\t\tpanic(\"not equal\")\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 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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"u986GqmEPnEs8976Nf7FeryTpaAJqhpZFC9jyiPLuSoYXUUBM8kc19sMokdgNhiBP4nfnwpnoXHx3HLZNVipcQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"R7EtjgLdqqDIWM6JDdu77zEyRYoqT7DuIZ+OKp7zK8RTfRTdkzEhvGzpRmvlQDMDjA4mOBPtGJ/UEL1N4Khj2A=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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":"nestedpkg_test.gno","body":"package vm\n\nimport (\n\t\"testing\"\n)\n\nfunc TestNestedPkg(cur realm, t *testing.T) {\n\t// direct child\n\tcurPath := \"gno.land/r/tests/vm/foo\"\n\ttesting.SetRealm(testing.NewCodeRealm(curPath))\n\tif !IsCallerSubPath(cross(cur)) {\n\t\tt.Errorf(curPath + \" should be a sub path\")\n\t}\n\tif IsCallerParentPath(cross(cur)) {\n\t\tt.Errorf(curPath + \" should not be a parent path\")\n\t}\n\tif !HasCallerSameNamespace(cross(cur)) {\n\t\tt.Errorf(curPath + \" should be from the same namespace\")\n\t}\n\n\t// grand-grand-child\n\tcurPath = \"gno.land/r/tests/vm/foo/bar/baz\"\n\ttesting.SetRealm(testing.NewCodeRealm(curPath))\n\tif !IsCallerSubPath(cross(cur)) {\n\t\tt.Errorf(curPath + \" should be a sub path\")\n\t}\n\tif IsCallerParentPath(cross(cur)) {\n\t\tt.Errorf(curPath + \" should not be a parent path\")\n\t}\n\tif !HasCallerSameNamespace(cross(cur)) {\n\t\tt.Errorf(curPath + \" should be from the same namespace\")\n\t}\n\n\t// NOTE: This is now back in the gno.land/r/tests/vm structure,\n\t// so the direct parent test case is valid again.\n\n\t// direct parent (was previously fake parent)\n\tcurPath = \"gno.land/r/test\" // without the 's' at the end\n\ttesting.SetRealm(testing.NewCodeRealm(curPath))\n\tif IsCallerSubPath(cross(cur)) {\n\t\tt.Errorf(curPath + \" should not be a sub path\")\n\t}\n\tif IsCallerParentPath(cross(cur)) {\n\t\tt.Errorf(curPath + \" should not be a parent path\")\n\t}\n\tif HasCallerSameNamespace(cross(cur)) {\n\t\tt.Errorf(curPath + \" should not be from the same namespace\")\n\t}\n\n\t// fake parent (prefix)\n\tcurPath = \"gno.land/r/dem\"\n\ttesting.SetRealm(testing.NewCodeRealm(curPath))\n\tif IsCallerSubPath(cross(cur)) {\n\t\tt.Errorf(curPath + \" should not be a sub path\")\n\t}\n\tif IsCallerParentPath(cross(cur)) {\n\t\tt.Errorf(curPath + \" should not be a parent path\")\n\t}\n\tif HasCallerSameNamespace(cross(cur)) {\n\t\tt.Errorf(curPath + \" should not be from the same namespace\")\n\t}\n\n\t// different namespace\n\tcurPath = \"gno.land/r/foo\"\n\ttesting.SetRealm(testing.NewCodeRealm(curPath))\n\tif IsCallerSubPath(cross(cur)) {\n\t\tt.Errorf(curPath + \" should not be a sub path\")\n\t}\n\tif IsCallerParentPath(cross(cur)) {\n\t\tt.Errorf(curPath + \" should not be a parent path\")\n\t}\n\tif HasCallerSameNamespace(cross(cur)) {\n\t\tt.Errorf(curPath + \" should not be from the same namespace\")\n\t}\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/nt/nestedpkg/v0\"\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"},{"name":"tests_test.gno","body":"package vm_test\n\nimport (\n\t\"chain\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\ttests \"gno.land/r/tests/vm\"\n)\n\nfunc TestAssertOriginCall(cur realm, t *testing.T) {\n\t// CallAssertOriginCall(): no panic\n\tcaller := testutils.TestAddress(\"caller\")\n\ttesting.SetRealm(testing.NewUserRealm(caller))\n\ttests.CallAssertOriginCall(cross(cur))\n\tif !tests.CallIsOriginCall(cross(cur)) {\n\t\tt.Errorf(\"expected IsOriginCall=true but got false\")\n\t}\n\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/tests/vm\"))\n\t// CallAssertOriginCall() from a block: abort\n\tr := revive(func() {\n\t\t// if called inside a function literal, this is no longer an origin call\n\t\t// because there's one additional frame (the function literal block).\n\t\tif tests.CallIsOriginCall(cross(cur)) {\n\t\t\tt.Errorf(\"expected IsOriginCall=false but got true\")\n\t\t}\n\t\ttests.CallAssertOriginCall(cross(cur)) // \u003c---\n\t})\n\tif fmt.Sprintf(\"%v\", r) != \"invalid non-origin call\" {\n\t\tt.Error(\"expected abort but did not\")\n\t}\n\t// CallSubtestsAssertOriginCall(): abort\n\tr = revive(func() {\n\t\t// if called inside a function literal, this is no longer an origin call\n\t\t// because there's one additional frame (the function literal block).\n\t\tif tests.CallSubtestsIsOriginCall(cross(cur)) {\n\t\t\tt.Errorf(\"expected IsOriginCall=false but got true\")\n\t\t}\n\t\ttests.CallSubtestsAssertOriginCall(cross(cur))\n\t})\n\tif fmt.Sprintf(\"%v\", r) != \"invalid non-origin call\" {\n\t\tt.Error(\"expected abort but did not\")\n\t}\n}\n\nfunc TestPreviousRealm(cur realm, t *testing.T) {\n\tvar (\n\t\tfirstRealm = chain.PackageAddress(\"gno.land/r/tests/vm_test\")\n\t\trTestsAddr = chain.PackageAddress(\"gno.land/r/tests/vm\")\n\t)\n\t// When only one realm in the frames, PreviousRealm returns the same realm\n\tif addr := tests.GetPreviousRealm(cross(cur)).Address(); addr != firstRealm {\n\t\tprintln(tests.GetPreviousRealm(cross(cur)))\n\t\tt.Errorf(\"want GetPreviousRealm().Address==%s, got %s\", firstRealm, addr)\n\t}\n\t// When 2 or more realms in the frames, PreviousRealm returns the second to last\n\tif addr := tests.GetRSubtestsPreviousRealm(cross(cur)).Address(); addr != rTestsAddr {\n\t\tt.Errorf(\"want GetRSubtestsPreviousRealm().Address==%s, got %s\", rTestsAddr, addr)\n\t}\n}\n"},{"name":"z0_filetest.gno","body":"// PKGPATH: gno.land/r/tests/vm/filetests/z0_filetest\n\npackage z0_filetest\n\nimport (\n\ttests \"gno.land/r/tests/vm\"\n)\n\nfunc main(cur realm) {\n\tprintln(\"tests.CallIsOriginCall:\", tests.CallIsOriginCall(cross(cur)))\n\ttests.CallAssertOriginCall(cross(cur))\n\tprintln(\"tests.CallAssertOriginCall doesn't panic when called directly\")\n\n\t{\n\t\t// if called inside a block, this is no longer an origin call because\n\t\t// there's one additional frame (the block).\n\t\tprintln(\"tests.CallIsOriginCall:\", tests.CallIsOriginCall(cross(cur)))\n\t\tdefer func() {\n\t\t\tr := recover()\n\t\t\tprintln(\"tests.AssertOriginCall panics if when called inside a function literal:\", r)\n\t\t}()\n\t\ttests.CallAssertOriginCall(cross(cur))\n\t}\n}\n\n// Output:\n// tests.CallIsOriginCall: false\n// tests.CallAssertOriginCall doesn't panic when called directly\n// tests.CallIsOriginCall: false\n// tests.AssertOriginCall panics if when called inside a function literal: undefined\n"},{"name":"z1_filetest.gno","body":"// PKGPATH: gno.land/r/tests/vm/filetests/z1_filetest\n\npackage z1_filetest\n\nimport (\n\ttests \"gno.land/r/tests/vm\"\n)\n\nfunc main(cur realm) {\n\tprintln(tests.Counter(cross(cur)))\n\ttests.IncCounter(cross(cur))\n\tprintln(tests.Counter(cross(cur)))\n}\n\n// Output:\n// 0\n// 1\n"},{"name":"z2_filetest.gno","body":"// PKGPATH: gno.land/r/tests/vm/filetests/z2_filetest\n\npackage z2_filetest\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\ttests \"gno.land/r/tests/vm\"\n)\n\n// When a single realm in the frames, PreviousRealm returns the user\n// When 2 or more realms in the frames, PreviousRealm returns the second to last\nfunc main(cur realm) {\n\tvar (\n\t\teoa = testutils.TestAddress(\"someone\")\n\t\t_   = chain.PackageAddress(\"gno.land/r/tests/vm\")\n\t)\n\ttesting.SetOriginCaller(eoa)\n\tprintln(\"tests.GetPreviousRealm().Address(): \", tests.GetPreviousRealm(cross(cur)).Address())\n\tprintln(\"tests.GetRSubtestsPreviousRealm().Address(): \", tests.GetRSubtestsPreviousRealm(cross(cur)).Address())\n}\n\n// Output:\n// tests.GetPreviousRealm().Address():  g1wdhk6et0dej47h6lta047h6lta047h6lrnerlk\n// tests.GetRSubtestsPreviousRealm().Address():  g1dhh6vhw9f5lmmpfz52rkf5dsk8lqzmad3fmpw7\n"},{"name":"z3_filetest.gno","body":"// PKGPATH: gno.land/r/tests_z3\npackage tests_z3\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/r/tests/vm\"\n)\n\nfunc main(cur realm) {\n\tvar (\n\t\teoa        = testutils.TestAddress(\"someone\")\n\t\trTestsAddr = chain.PackageAddress(\"gno.land/r/tests/vm\")\n\t)\n\ttesting.SetOriginCaller(eoa)\n\t// Contrarily to z2_filetest.gno we EXPECT GetPreviousRealms != eoa (#1704)\n\tif addr := vm.GetPreviousRealm(cross(cur)).Address(); addr != eoa {\n\t\tprintln(\"want vm.GetPreviousRealm().Address ==\", eoa, \"got\", addr)\n\t}\n\t// When 2 or more realms in the frames, it is also different\n\tif addr := vm.GetRSubtestsPreviousRealm(cross(cur)).Address(); addr != rTestsAddr {\n\t\tprintln(\"want GetRSubtestsPreviousRealm().Address ==\", rTestsAddr, \"got\", addr)\n\t}\n\tprintln(\"Done.\")\n}\n\n// Output:\n// Done.\n"},{"name":"z4_filetest.gno","body":"// PKGPATH: gno.land/r/tests/vm/z4_filetest\n\npackage z4_filetest\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\ttests \"gno.land/r/tests/vm\"\n)\n\nfunc main(cur realm) {\n\ttesting.SetOriginSend(chain.Coins{{Denom: \"foo\", Amount: 42}, {Denom: \"ugnot\", Amount: 100}})\n\tprintln(\"tests.BankerOriginSend: \" + tests.BankerOriginSend(cross(cur)))\n\n\t// unsafe.OriginSend() always returns the origin send regardless of call\n\t// depth.\n\tprintln(\"tests.RTestsOriginSend: \" + tests.RTestsOriginSend(cross(cur)))\n}\n\n// Output:\n// tests.BankerOriginSend: 42foo,100ugnot\n// tests.RTestsOriginSend: 42foo,100ugnot\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"Iw7I9Jqqa8JARdwEl9/A+X4k5akUDyV6h5OtCxaisXJfpGMGW+o8Xci8DEaylzzWWN0/sdqNdZpPEPQ7jioxsA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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":"mock_test.gno","body":"package uassert_test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\ntype mockTestingT struct {\n\tfmt  string\n\targs []any\n}\n\n// --- interface mock\n\nvar _ uassert.TestingT = (*mockTestingT)(nil)\n\nfunc (mockT *mockTestingT) Helper()                      { /* noop */ }\nfunc (mockT *mockTestingT) Skip(args ...any)             { /* not implmented */ }\nfunc (mockT *mockTestingT) Fail()                        { /* not implmented */ }\nfunc (mockT *mockTestingT) FailNow()                     { /* not implmented */ }\nfunc (mockT *mockTestingT) Logf(fmt string, args ...any) { /* noop */ }\n\nfunc (mockT *mockTestingT) Fatalf(fmt string, args ...any) {\n\tmockT.fmt = \"fatal: \" + fmt\n\tmockT.args = args\n}\n\nfunc (mockT *mockTestingT) Errorf(fmt string, args ...any) {\n\tmockT.fmt = \"error: \" + fmt\n\tmockT.args = args\n}\n\n// --- helpers\n\nfunc (mockT *mockTestingT) actualString() string {\n\tres := fmt.Sprintf(mockT.fmt, mockT.args...)\n\tmockT.reset()\n\treturn res\n}\n\nfunc (mockT *mockTestingT) reset() {\n\tmockT.fmt = \"\"\n\tmockT.args = nil\n}\n\nfunc (mockT *mockTestingT) equals(t *testing.T, expected string) {\n\tactual := mockT.actualString()\n\n\tif expected != actual {\n\t\tt.Errorf(\"mockT differs:\\n- expected: %s\\n- actual:   %s\\n\", expected, actual)\n\t}\n}\n\nfunc (mockT *mockTestingT) empty(t *testing.T) {\n\tif mockT.fmt != \"\" || mockT.args != nil {\n\t\tactual := mockT.actualString()\n\t\tt.Errorf(\"mockT should be empty, got %s\", actual)\n\t}\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/v0\"\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"},{"name":"uassert_test.gno","body":"package uassert_test\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\ttests \"gno.land/r/tests/vm\"\n)\n\n// cur is a zero-value realm used as a placeholder when forwarding to\n// uassert/urequire dispatch helpers that gained an `rlm realm` param.\n// These tests pass `func()` callbacks (no crossing inside the callback),\n// so rlm is ignored — a nil realm here is safe.\nvar cur realm\nvar _ uassert.TestingT = (*testing.T)(nil)\n\nfunc TestMock(t *testing.T) {\n\tmockT := new(mockTestingT)\n\tmockT.empty(t)\n\tuassert.NoError(mockT, errors.New(\"foo\"))\n\tmockT.equals(t, \"error: unexpected error: foo\")\n\tuassert.NoError(mockT, errors.New(\"foo\"), \"custom message\")\n\tmockT.equals(t, \"error: unexpected error: foo - custom message\")\n\tuassert.NoError(mockT, errors.New(\"foo\"), \"custom\", \"message\")\n\tmockT.equals(t, \"error: unexpected error: foo - custom message\")\n}\n\nfunc TestNoError(t *testing.T) {\n\tmockT := new(mockTestingT)\n\tuassert.True(t, uassert.NoError(mockT, nil))\n\tmockT.empty(t)\n\tuassert.False(t, uassert.NoError(mockT, errors.New(\"foo bar\")))\n\tmockT.equals(t, \"error: unexpected error: foo bar\")\n}\n\nfunc TestError(t *testing.T) {\n\tmockT := new(mockTestingT)\n\tuassert.True(t, uassert.Error(mockT, errors.New(\"foo bar\")))\n\tmockT.empty(t)\n\tuassert.False(t, uassert.Error(mockT, nil))\n\tmockT.equals(t, \"error: an error is expected but got nil\")\n}\n\nfunc TestErrorContains(t *testing.T) {\n\tmockT := new(mockTestingT)\n\n\t// nil error\n\tvar err error\n\tuassert.False(t, uassert.ErrorContains(mockT, err, \"\"), \"ErrorContains should return false for nil arg\")\n}\n\nfunc TestTrue(t *testing.T) {\n\tmockT := new(mockTestingT)\n\tif !uassert.True(mockT, true) {\n\t\tt.Error(\"True should return true\")\n\t}\n\tmockT.empty(t)\n\tif uassert.True(mockT, false) {\n\t\tt.Error(\"True should return false\")\n\t}\n\tmockT.equals(t, \"error: should be true\")\n}\n\nfunc TestFalse(t *testing.T) {\n\tmockT := new(mockTestingT)\n\tif !uassert.False(mockT, false) {\n\t\tt.Error(\"False should return true\")\n\t}\n\tmockT.empty(t)\n\tif uassert.False(mockT, true) {\n\t\tt.Error(\"False should return false\")\n\t}\n\tmockT.equals(t, \"error: should be false\")\n}\n\nfunc TestPanicsWithMessage(cur realm, t *testing.T) {\n\tmockT := new(mockTestingT)\n\tif !uassert.PanicsWithMessage(mockT, cur, \"panic\", func() {\n\t\tpanic(errors.New(\"panic\"))\n\t}) {\n\t\tt.Error(\"PanicsWithMessage should return true\")\n\t}\n\tmockT.empty(t)\n\n\tif uassert.PanicsWithMessage(mockT, cur, \"Panic!\", func() {\n\t\t// noop\n\t}) {\n\t\tt.Error(\"PanicsWithMessage should return false\")\n\t}\n\tmockT.equals(t, \"error: func should panic\\n\\tPanic value:\\tnil\")\n\n\tif uassert.PanicsWithMessage(mockT, cur, \"at the disco\", func() {\n\t\tpanic(errors.New(\"panic\"))\n\t}) {\n\t\tt.Error(\"PanicsWithMessage should return false\")\n\t}\n\tmockT.equals(t, \"error: func should panic with message:\\t\\\"at the disco\\\"\\n\\tActual panic value:\\t\\\"panic\\\"\")\n\n\tif uassert.PanicsWithMessage(mockT, cur, \"Panic!\", func() {\n\t\tpanic(\"panic\")\n\t}) {\n\t\tt.Error(\"PanicsWithMessage should return false\")\n\t}\n\tmockT.equals(t, \"error: func should panic with message:\\t\\\"Panic!\\\"\\n\\tActual panic value:\\t\\\"panic\\\"\")\n}\n\nfunc TestPanicsContains(cur realm, t *testing.T) {\n\tmockT := new(mockTestingT)\n\tif !uassert.PanicsContains(mockT, cur, \"panic\", func() {\n\t\tpanic(errors.New(\"panic: something happened\"))\n\t}) {\n\t\tt.Error(\"PanicsContains should return true for substring match\")\n\t}\n\tmockT.empty(t)\n\n\tif uassert.PanicsContains(mockT, cur, \"notfound\", func() {\n\t\tpanic(errors.New(\"panic: something happened\"))\n\t}) {\n\t\tt.Error(\"PanicsContains should return false for missing substring\")\n\t}\n\tmockT.equals(t, \"error: func should panic with message containing:\\t\\\"notfound\\\"\\n\\tActual panic value:\\t\\\"panic: something happened\\\"\")\n\n\tif uassert.PanicsContains(mockT, cur, \"panic\", func() {\n\t\t// noop\n\t}) {\n\t\tt.Error(\"PanicsContains should return false when no panic occurs\")\n\t}\n\tmockT.equals(t, \"error: func should panic\\n\\tPanic value:\\tnil\")\n}\n\nfunc TestAbortsWithMessage(cur realm, t *testing.T) {\n\tmockT := new(mockTestingT)\n\tif !uassert.AbortsWithMessage(mockT, cur, \"abort message\", func() {\n\t\ttests.ExecSwitch(cross(cur), func() {\n\t\t\tpanic(\"abort message\")\n\t\t})\n\t\tpanic(\"dontcare\")\n\t}) {\n\t\tt.Error(\"AbortsWithMessage should return true\")\n\t}\n\tmockT.empty(t)\n\n\tif uassert.AbortsWithMessage(mockT, cur, \"Abort!\", func() {\n\t\t// noop\n\t}) {\n\t\tt.Error(\"AbortsWithMessage should return false\")\n\t}\n\tmockT.equals(t, \"error: func should abort\")\n\n\tif uassert.AbortsWithMessage(mockT, cur, \"at the disco\", func() {\n\t\ttests.ExecSwitch(cross(cur), func() {\n\t\t\tpanic(\"abort message\")\n\t\t})\n\t\tpanic(\"dontcare\")\n\t}) {\n\t\tt.Error(\"AbortsWithMessage should return false (wrong message)\")\n\t}\n\tmockT.equals(t, \"error: func should abort with message:\\t\\\"at the disco\\\"\\n\\tActual abort value:\\t\\\"abort message\\\"\")\n\n\t// Test that non-crossing panics don't count as abort.\n\tuassert.PanicsWithMessage(mockT, cur, \"non-abort panic\", func() {\n\t\tuassert.AbortsWithMessage(mockT, cur, \"dontcare2\", func() {\n\t\t\tpanic(\"non-abort panic\")\n\t\t})\n\t\tt.Error(\"AbortsWithMessage should not have caught non-abort panic\")\n\t}, \"non-abort panic\")\n\tmockT.empty(t)\n\n\t// Test case where abort value is not a string\n\tif uassert.AbortsWithMessage(mockT, cur, \"doesn't matter\", func() {\n\t\ttests.ExecSwitch(cross(cur), func() {\n\t\t\tpanic(123) // abort with an integer\n\t\t})\n\t\tpanic(\"dontcare\")\n\t}) {\n\t\tt.Error(\"AbortsWithMessage should return false when abort value is not a string\")\n\t}\n\tmockT.equals(t, \"error: func should abort with message:\\t\\\"doesn't matter\\\"\\n\\tActual abort value:\\t\\\"123\\\"\")\n\n\t// XXX: test with Error\n}\n\nfunc TestAbortsContains(cur realm, t *testing.T) {\n\tmockT := new(mockTestingT)\n\tif !uassert.AbortsContains(mockT, cur, \"abort\", func() {\n\t\ttests.ExecSwitch(cross(cur), func() {\n\t\t\tpanic(\"abort message: something happened\")\n\t\t})\n\t\tpanic(\"dontcare\")\n\t}) {\n\t\tt.Error(\"AbortsContains should return true for substring match\")\n\t}\n\tmockT.empty(t)\n\n\tif uassert.AbortsContains(mockT, cur, \"notfound\", func() {\n\t\ttests.ExecSwitch(cross(cur), func() {\n\t\t\tpanic(\"abort message: something happened\")\n\t\t})\n\t\tpanic(\"dontcare\")\n\t}) {\n\t\tt.Error(\"AbortsContains should return false for missing substring\")\n\t}\n\tmockT.equals(t, \"error: func should abort with message containing:\\t\\\"notfound\\\"\\n\\tActual abort value:\\t\\\"abort message: something happened\\\"\")\n\n\tif uassert.AbortsContains(mockT, cur, \"abort\", func() {\n\t\t// noop\n\t}) {\n\t\tt.Error(\"AbortsContains should return false when no abort occurs\")\n\t}\n\tmockT.equals(t, \"error: func should abort\")\n}\n\nfunc TestNotAborts(cur realm, t *testing.T) {\n\tmockT := new(mockTestingT)\n\n\tif !uassert.NotPanics(mockT, cur, func() {\n\t\t// noop\n\t}) {\n\t\tt.Error(\"NotAborts should return true\")\n\t}\n\tmockT.empty(t)\n\n\tif uassert.NotPanics(mockT, cur, func() {\n\t\ttests.ExecSwitch(cross(cur), func() {\n\t\t\tpanic(\"Abort!\")\n\t\t})\n\t\tpanic(\"dontcare\")\n\t}) {\n\t\tt.Error(\"NotAborts should return false\")\n\t}\n\tmockT.equals(t, \"error: func should not abort\\n\\tAbort value:\\tAbort!\")\n}\n\nfunc TestNotPanics(cur realm, t *testing.T) {\n\tmockT := new(mockTestingT)\n\n\tif !uassert.NotPanics(mockT, cur, func() {\n\t\t// noop\n\t}) {\n\t\tt.Error(\"NotPanics should return true\")\n\t}\n\tmockT.empty(t)\n\n\tif uassert.NotPanics(mockT, cur, func() {\n\t\tpanic(\"Panic!\")\n\t}) {\n\t\tt.Error(\"NotPanics should return false\")\n\t}\n\tmockT.equals(t, \"error: func should not panic\\n\\tPanic value:\\tPanic!\")\n}\n\nfunc TestEqual(t *testing.T) {\n\tmockT := new(mockTestingT)\n\n\tcases := []struct {\n\t\texpected any\n\t\tactual   any\n\t\tresult   bool\n\t\tremark   string\n\t}{\n\t\t// expected to be equal\n\t\t{\"Hello World\", \"Hello World\", true, \"\"},\n\t\t{123, 123, true, \"\"},\n\t\t{123.5, 123.5, true, \"\"},\n\t\t{nil, nil, true, \"\"},\n\t\t{int32(123), int32(123), true, \"\"},\n\t\t{uint64(123), uint64(123), true, \"\"},\n\t\t{address(\"g12345\"), address(\"g12345\"), true, \"\"},\n\t\t// XXX: continue\n\n\t\t// not expected to be equal\n\t\t{\"Hello World\", 42, false, \"\"},\n\t\t{41, 42, false, \"\"},\n\t\t{10, uint(10), false, \"\"},\n\t\t// XXX: continue\n\n\t\t// expected to raise errors\n\t\t// XXX: todo\n\t}\n\n\tfor _, c := range cases {\n\t\tname := fmt.Sprintf(\"Equal(%v, %v)\", c.expected, c.actual)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tres := uassert.Equal(mockT, c.expected, c.actual)\n\n\t\t\tif res != c.result {\n\t\t\t\tt.Errorf(\"%s should return %v: %s - %s\", name, c.result, c.remark, mockT.actualString())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNotEqual(t *testing.T) {\n\tmockT := new(mockTestingT)\n\n\tcases := []struct {\n\t\texpected any\n\t\tactual   any\n\t\tresult   bool\n\t\tremark   string\n\t}{\n\t\t// expected to be not equal\n\t\t{\"Hello World\", \"Hello\", true, \"\"},\n\t\t{123, 124, true, \"\"},\n\t\t{123.5, 123.6, true, \"\"},\n\t\t{nil, 123, true, \"\"},\n\t\t{int32(123), int32(124), true, \"\"},\n\t\t{uint64(123), uint64(124), true, \"\"},\n\t\t{address(\"g12345\"), address(\"g67890\"), true, \"\"},\n\t\t// XXX: continue\n\n\t\t// not expected to be not equal\n\t\t{\"Hello World\", \"Hello World\", false, \"\"},\n\t\t{123, 123, false, \"\"},\n\t\t{123.5, 123.5, false, \"\"},\n\t\t{nil, nil, false, \"\"},\n\t\t{int32(123), int32(123), false, \"\"},\n\t\t{uint64(123), uint64(123), false, \"\"},\n\t\t{address(\"g12345\"), address(\"g12345\"), false, \"\"},\n\t\t// XXX: continue\n\n\t\t// expected to raise errors\n\t\t// XXX: todo\n\t}\n\n\tfor _, c := range cases {\n\t\tname := fmt.Sprintf(\"NotEqual(%v, %v)\", c.expected, c.actual)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tres := uassert.NotEqual(mockT, c.expected, c.actual)\n\n\t\t\tif res != c.result {\n\t\t\t\tt.Errorf(\"%s should return %v: %s - %s\", name, c.result, c.remark, mockT.actualString())\n\t\t\t}\n\t\t})\n\t}\n}\n\ntype myStruct struct {\n\tS string\n\tI int\n}\n\nfunc TestEmpty(t *testing.T) {\n\tmockT := new(mockTestingT)\n\n\tcases := []struct {\n\t\tobj           any\n\t\texpectedEmpty bool\n\t}{\n\t\t// expected to be empty\n\t\t{\"\", true},\n\t\t{0, true},\n\t\t{int(0), true},\n\t\t{int32(0), true},\n\t\t{int64(0), true},\n\t\t{uint(0), true},\n\t\t// XXX: continue\n\n\t\t// not expected to be empty\n\t\t{\"Hello World\", false},\n\t\t{1, false},\n\t\t{int32(1), false},\n\t\t{uint64(1), false},\n\t\t{address(\"g12345\"), false},\n\n\t\t// unsupported\n\t\t{nil, false},\n\t\t{myStruct{}, false},\n\t\t{\u0026myStruct{}, false},\n\t}\n\n\tfor _, c := range cases {\n\t\tname := fmt.Sprintf(\"Empty(%v)\", c.obj)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tres := uassert.Empty(mockT, c.obj)\n\n\t\t\tif res != c.expectedEmpty {\n\t\t\t\tt.Errorf(\"%s should return %v: %s\", name, c.expectedEmpty, mockT.actualString())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEqualWithStringDiff(t *testing.T) {\n\tcases := []struct {\n\t\tname        string\n\t\texpected    string\n\t\tactual      string\n\t\tshouldPass  bool\n\t\texpectedMsg string\n\t}{\n\t\t{\n\t\t\tname:        \"Identical strings\",\n\t\t\texpected:    \"Hello, world!\",\n\t\t\tactual:      \"Hello, world!\",\n\t\t\tshouldPass:  true,\n\t\t\texpectedMsg: \"\",\n\t\t},\n\t\t{\n\t\t\tname:        \"Different strings - simple\",\n\t\t\texpected:    \"Hello, world!\",\n\t\t\tactual:      \"Hello, World!\",\n\t\t\tshouldPass:  false,\n\t\t\texpectedMsg: \"error: uassert.Equal: strings are different\\n\\tDiff: Hello, [-w][+W]orld!\",\n\t\t},\n\t\t{\n\t\t\tname:        \"Different strings - complex\",\n\t\t\texpected:    \"The quick brown fox jumps over the lazy dog\",\n\t\t\tactual:      \"The quick brown cat jumps over the lazy dog\",\n\t\t\tshouldPass:  false,\n\t\t\texpectedMsg: \"error: uassert.Equal: strings are different\\n\\tDiff: The quick brown [-fox][+cat] jumps over the lazy dog\",\n\t\t},\n\t\t{\n\t\t\tname:        \"Different strings - prefix\",\n\t\t\texpected:    \"prefix_string\",\n\t\t\tactual:      \"string\",\n\t\t\tshouldPass:  false,\n\t\t\texpectedMsg: \"error: uassert.Equal: strings are different\\n\\tDiff: [-prefix_]string\",\n\t\t},\n\t\t{\n\t\t\tname:        \"Different strings - suffix\",\n\t\t\texpected:    \"string\",\n\t\t\tactual:      \"string_suffix\",\n\t\t\tshouldPass:  false,\n\t\t\texpectedMsg: \"error: uassert.Equal: strings are different\\n\\tDiff: string[+_suffix]\",\n\t\t},\n\t\t{\n\t\t\tname:        \"Empty string vs non-empty string\",\n\t\t\texpected:    \"\",\n\t\t\tactual:      \"non-empty\",\n\t\t\tshouldPass:  false,\n\t\t\texpectedMsg: \"error: uassert.Equal: strings are different\\n\\tDiff: [+non-empty]\",\n\t\t},\n\t\t{\n\t\t\tname:        \"Non-empty string vs empty string\",\n\t\t\texpected:    \"non-empty\",\n\t\t\tactual:      \"\",\n\t\t\tshouldPass:  false,\n\t\t\texpectedMsg: \"error: uassert.Equal: strings are different\\n\\tDiff: [-non-empty]\",\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tmockT := \u0026mockTestingT{}\n\t\t\tresult := uassert.Equal(mockT, tc.expected, tc.actual)\n\n\t\t\tif result != tc.shouldPass {\n\t\t\t\tt.Errorf(\"Expected Equal to return %v, but got %v\", tc.shouldPass, result)\n\t\t\t}\n\n\t\t\tif tc.shouldPass {\n\t\t\t\tmockT.empty(t)\n\t\t\t} else {\n\t\t\t\tmockT.equals(t, tc.expectedMsg)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNotEmpty(t *testing.T) {\n\tmockT := new(mockTestingT)\n\n\tcases := []struct {\n\t\tobj              any\n\t\texpectedNotEmpty bool\n\t}{\n\t\t// expected to be empty\n\t\t{\"\", false},\n\t\t{0, false},\n\t\t{int(0), false},\n\t\t{int32(0), false},\n\t\t{int64(0), false},\n\t\t{uint(0), false},\n\t\t{address(\"\"), false},\n\n\t\t// not expected to be empty\n\t\t{\"Hello World\", true},\n\t\t{1, true},\n\t\t{int32(1), true},\n\t\t{uint64(1), true},\n\t\t{address(\"g12345\"), true},\n\n\t\t// unsupported\n\t\t{nil, false},\n\t\t{myStruct{}, false},\n\t\t{\u0026myStruct{}, false},\n\t}\n\n\tfor _, c := range cases {\n\t\tname := fmt.Sprintf(\"NotEmpty(%v)\", c.obj)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tres := uassert.NotEmpty(mockT, c.obj)\n\n\t\t\tif res != c.expectedNotEmpty {\n\t\t\t\tt.Errorf(\"%s should return %v: %s\", name, c.expectedNotEmpty, mockT.actualString())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNil(t *testing.T) {\n\tmockT := new(mockTestingT)\n\tif !uassert.Nil(mockT, nil) {\n\t\tt.Error(\"Nil should return true\")\n\t}\n\tmockT.empty(t)\n\tif uassert.Nil(mockT, 0) {\n\t\tt.Error(\"Nil should return false\")\n\t}\n\tmockT.equals(t, \"error: should be nil\")\n\tif uassert.Nil(mockT, (*int)(nil)) {\n\t\tt.Error(\"Nil should return false\")\n\t}\n\tmockT.equals(t, \"error: should be nil\")\n}\n\nfunc TestNotNil(t *testing.T) {\n\tmockT := new(mockTestingT)\n\tif uassert.NotNil(mockT, nil) {\n\t\tt.Error(\"NotNil should return false\")\n\t}\n\tmockT.equals(t, \"error: should not be nil\")\n\tif !uassert.NotNil(mockT, 0) {\n\t\tt.Error(\"NotNil should return true\")\n\t}\n\tmockT.empty(t)\n\tif !uassert.NotNil(mockT, (*int)(nil)) {\n\t\tt.Error(\"NotNil should return true\")\n\t}\n\tmockT.empty(t)\n}\n\nfunc TestTypedNil(t *testing.T) {\n\tmockT := new(mockTestingT)\n\tif uassert.TypedNil(mockT, nil) {\n\t\tt.Error(\"TypedNil should return false\")\n\t}\n\tmockT.equals(t, \"error: should be typed-nil but got nil instead\")\n\tif uassert.TypedNil(mockT, 0) {\n\t\tt.Error(\"TypedNil should return false\")\n\t}\n\tmockT.equals(t, \"error: should be typed-nil\")\n\tif !uassert.TypedNil(mockT, (*int)(nil)) {\n\t\tt.Error(\"TypedNil should return true\")\n\t}\n\tmockT.empty(t)\n}\n\nfunc TestNotTypedNil(t *testing.T) {\n\tmockT := new(mockTestingT)\n\tif !uassert.NotTypedNil(mockT, nil) {\n\t\tt.Error(\"NotTypedNil should return true\")\n\t}\n\tmockT.empty(t)\n\tif !uassert.NotTypedNil(mockT, 0) {\n\t\tt.Error(\"NotTypedNil should return true\")\n\t}\n\tmockT.empty(t)\n\tif uassert.NotTypedNil(mockT, (*int)(nil)) {\n\t\tt.Error(\"NotTypedNil should return false\")\n\t}\n\tmockT.equals(t, \"error: should not be typed-nil\")\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"r8acPL5XxnedP3WTo00pL2ubo9a1tgI+N40nuun5VUARunIC4OhOsCuysR0CRlmNk08zHsVW17FKexiQaTxUkA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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\u003cgno-button …/\u003e anywhere, code included               | escaped (\\\u003cgno-button) → literal text\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\u003cgno-button …/\u003e anywhere, code included       | escaped (\\\u003cgno-button) [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\u003cgno-button …/\u003e anywhere, code included       | escaped (\\\u003cgno-button) [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"},{"name":"sanitize_test.gno","body":"package sanitize\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestStripBidiAndZeroWidthRe(t *testing.T) {\n\t// Re-export sanity check.\n\tif got := StripBidiAndZeroWidth(\"a\\u200Bb\"); got != \"ab\" {\n\t\tt.Errorf(\"re-export StripBidiAndZeroWidth failed: got %q\", got)\n\t}\n}\n\nfunc TestNormalizeBreaksRe(t *testing.T) {\n\tif got := NormalizeBreaks(\"a\\r\\nb\"); got != \"a\\nb\" {\n\t\tt.Errorf(\"re-export NormalizeBreaks failed: got %q\", got)\n\t}\n}\n\nfunc TestInlineText(t *testing.T) {\n\tcases := []struct{ in, want string }{\n\t\t{\"plain\", \"plain\"},\n\t\t{\"a*b\", `a\\*b`},\n\t\t{\"a|b\", \"a|b\"},                    // | NOT escaped\n\t\t{\"a=b\", \"a=b\"},                    // = NOT escaped\n\t\t{\"line1\\nline2\", \"line1 line2\"},   // newline folded\n\t\t{\"line1\\r\\nline2\", \"line1 line2\"}, // CRLF normalized then folded\n\t\t{\"a\\u2028b\", \"a b\"},               // U+2028 folded\n\t\t{\"a\\u0085b\", \"a b\"},               // NEL folded\n\t\t{\"a\\u200Bb\", \"ab\"},                // ZWSP stripped\n\t\t{\"hello *world*\", `hello \\*world\\*`},\n\t}\n\tfor _, c := range cases {\n\t\tif got := InlineText(c.in); got != c.want {\n\t\t\tt.Errorf(\"InlineText(%q) = %q, want %q\", c.in, got, c.want)\n\t\t}\n\t}\n}\n\nfunc TestBlock(t *testing.T) {\n\t// Block wraps every non-empty output with \"\\n\\n\" — CM §4.8 blank\n\t// lines — on BOTH sides, so user content is paragraph-isolated\n\t// from any realm chrome that precedes OR follows it. Bounds CM\n\t// §4.6 HTML block types 6/7 (`\u003cdiv\u003e`, `\u003ctable\u003e`, …) which are not\n\t// escaped in any mode and would otherwise consume appended chrome.\n\tcases := []struct{ in, want string }{\n\t\t{\"hello world\\n\", \"\\n\\nhello world\\n\\n\"},\n\t\t{\"# heading\\n\", \"\\n\\n\\\\# heading\\n\\n\"},\n\t\t{\"\u003e quote\\n\", \"\\n\\n\\\\\u003e quote\\n\\n\"},\n\t\t{\"| a | b |\\n\", \"\\n\\n\\\\| a | b |\\n\\n\"}, // GFM table-row escaped in strict mode\n\t\t// CM §4.6 HTML block types 1-5 — escaped (blank-line-NON-terminating).\n\t\t{\"\u003cscript\u003ex\u003c/script\u003e\\n\", \"\\n\\n\\\\\u003cscript\u003ex\u003c/script\u003e\\n\\n\"},\n\t\t{\"\u003c!-- comment --\u003e\\n\", \"\\n\\n\\\\\u003c!-- comment --\u003e\\n\\n\"},\n\t\t{\"\u003c?php x ?\u003e\\n\", \"\\n\\n\\\\\u003c?php x ?\u003e\\n\\n\"},\n\t\t{\"\u003c!DOCTYPE html\u003e\\n\", \"\\n\\n\\\\\u003c!DOCTYPE html\u003e\\n\\n\"},\n\t\t{\"```\\ncode\\n\", \"\\n\\n```\\ncode\\n```\\n\\n\"}, // fence auto-close at EOF\n\t\t{\"[x](\u0026#x6a;avascript:alert)\\n\", \"\\n\\n[x](%26#x6a;avascript:alert)\\n\\n\"},\n\t\t{\"[x](\u0026#0000152;avascript:alert)\\n\", \"\\n\\n[x](%26#0000152;avascript:alert)\\n\\n\"}, // octal reference\n\t\t// A renderer unescapes `\\#` before resolving references, so this\n\t\t// reaches it as `\u0026#x6a;` unless the `\u0026` is encoded here.\n\t\t{\"[x](\u0026\\\\#x6a;avascript:alert)\\n\", \"\\n\\n[x](%26\\\\#x6a;avascript:alert)\\n\\n\"},\n\t\t// A renderer resolves numeric references before named ones, over\n\t\t// the same buffer, so `\u0026#38;colon;` reaches the named pass as\n\t\t// `\u0026colon;` and resolves to `:`.\n\t\t{\"[x](javascript\u0026#38;colon;alert)\\n\", \"\\n\\n[x](javascript%26#38;colon;alert)\\n\\n\"},\n\t\t{\"[x](?x=1\u0026y=2)\\n\", \"\\n\\n[x](?x=1\u0026y=2)\\n\\n\"},\n\t\t// Empty / strip-to-empty inputs short-circuit (no stray blank line).\n\t\t{\"[x]: y\\n\", \"\"},\n\t\t{\"\", \"\"},\n\t}\n\tfor _, c := range cases {\n\t\tgot := Block(c.in)\n\t\tif got != c.want {\n\t\t\tt.Errorf(\"Block(%q) = %q, want %q\", c.in, got, c.want)\n\t\t}\n\t\t// Idempotency: Block(Block(in)) must be byte-identical to\n\t\t// Block(in) for every input in the table.\n\t\ttwice := Block(got)\n\t\tif twice != got {\n\t\t\tt.Errorf(\"Block not idempotent for %q: Block(once)=%q, Block(twice)=%q\", c.in, got, twice)\n\t\t}\n\t}\n}\n\nfunc TestBlockRich(t *testing.T) {\n\t// BlockRich wraps every non-empty output with \"\\n\\n\" — CM §4.8\n\t// blank lines — on BOTH sides, so user content is paragraph-\n\t// isolated from any realm chrome that precedes OR follows it.\n\tcases := []struct{ in, want string }{\n\t\t// Block-level markdown that Block escapes — preserved by BlockRich.\n\t\t{\"hello world\\n\", \"\\n\\nhello world\\n\\n\"},\n\t\t{\"# heading\\n\", \"\\n\\n# heading\\n\\n\"},\n\t\t{\"\u003e quote\\n\", \"\\n\\n\u003e quote\\n\\n\"},\n\t\t{\"- item\\n\", \"\\n\\n- item\\n\\n\"},\n\t\t{\"1. item\\n\", \"\\n\\n1. item\\n\\n\"},\n\t\t{\"---\\n\", \"\\n\\n\\\\---\\n\\n\"},                       // first-line setext-h2 → escaped by qualifying-setext pre-pass\n\t\t{\"***\\n\", \"\\n\\n***\\n\\n\"},                         // thematic break NOT setext-h2 — preserved\n\t\t{\"text\\n===\\n\", \"\\n\\ntext\\n===\\n\\n\"},             // deeper setext preserved (user-authored above)\n\t\t{\"body\\n===\\nmore\\n\", \"\\n\\nbody\\n===\\nmore\\n\\n\"}, // cross-paragraph backward attack: realm `chrome\\n\\nbody\\n===\\nmore` keeps realm in its own paragraph\n\t\t// GFM tables PRESERVED in Rich mode.\n\t\t{\"| a | b |\\n\", \"\\n\\n| a | b |\\n\\n\"},\n\t\t{\"| H |\\n|---|\\n| a |\\n\", \"\\n\\n| H |\\n|---|\\n| a |\\n\\n\"}, // full table renders as \u003ctable\u003e\n\t\t// Realm-binding defenses STILL ON.\n\t\t{\"\u003cgno-card\u003e\\n\", \"\\n\\n\\\\\u003cgno-card\u003e\\n\\n\"},\n\t\t// CM §4.6 HTML block types 1-5 — escaped in Rich mode too\n\t\t// (defense is mode-independent).\n\t\t{\"\u003cscript\u003ex\u003c/script\u003e\\n\", \"\\n\\n\\\\\u003cscript\u003ex\u003c/script\u003e\\n\\n\"},\n\t\t{\"\u003c!-- comment --\u003e\\n\", \"\\n\\n\\\\\u003c!-- comment --\u003e\\n\\n\"},\n\t\t{\"\u003c?php x ?\u003e\\n\", \"\\n\\n\\\\\u003c?php x ?\u003e\\n\\n\"},\n\t\t{\"\u003c!DOCTYPE html\u003e\\n\", \"\\n\\n\\\\\u003c!DOCTYPE html\u003e\\n\\n\"},\n\t\t{\"[t][l]\\n\", \"\\n\\n\\\\[t\\\\]\\\\[l\\\\]\\n\\n\"},\n\t\t{\"[^name]\\n\", \"\\n\\n\\\\[^name\\\\]\\n\\n\"},\n\t\t// LRD-only input strips to nothing; empty-after-escape short-\n\t\t// circuits to \"\" so realm concatenation doesn't leak a stray\n\t\t// blank line.\n\t\t{\"[x]: y\\n\", \"\"},\n\t\t{\"\", \"\"}, // empty input → empty output, no stray blank line\n\t\t{\"```\\ncode\\n\", \"\\n\\n```\\ncode\\n```\\n\\n\"},\n\t\t{\"[x](javascript\u0026colon;alert)\\n\", \"\\n\\n[x](javascript%26colon;alert)\\n\\n\"},\n\t\t// `\\;` on a named reference — unescaped to `\u0026colon;` by a renderer.\n\t\t{\"[x](javascript\u0026colon\\\\;alert)\\n\", \"\\n\\n[x](javascript%26colon\\\\;alert)\\n\\n\"},\n\t\t// Numeric reference feeding the named pass — see TestBlock.\n\t\t{\"[x](javascript\u0026#x26;colon;alert)\\n\", \"\\n\\n[x](javascript%26#x26;colon;alert)\\n\\n\"},\n\t\t{\"[x](/r/boards$help\u0026func=DeleteThread\u0026boardID=2)\\n\", \"\\n\\n[x](/r/boards$help\u0026func=DeleteThread\u0026boardID=2)\\n\\n\"},\n\t}\n\tfor _, c := range cases {\n\t\tgot := BlockRich(c.in)\n\t\tif got != c.want {\n\t\t\tt.Errorf(\"BlockRich(%q) = %q, want %q\", c.in, got, c.want)\n\t\t}\n\t\t// Idempotency: BlockRich(BlockRich(in)) must be byte-identical\n\t\t// to BlockRich(in) for every input in the table.\n\t\ttwice := BlockRich(got)\n\t\tif twice != got {\n\t\t\tt.Errorf(\"BlockRich not idempotent for %q: BlockRich(once)=%q, BlockRich(twice)=%q\", c.in, got, twice)\n\t\t}\n\t}\n}\n\nfunc TestBlockRich_LeadingBlankLineGuaranteed(t *testing.T) {\n\t// Every non-empty result begins with \"\\n\\n\" so `chrome +\n\t// BlockRich(user)` cannot place the user's first line in the same\n\t// paragraph as the realm's last line (which would let a deeper\n\t// `===` or `|---|` in user content retroactively promote realm\n\t// chrome).\n\tfor _, in := range []string{\"x\", \"x\\n\", \"# h\", \"- i\\n- j\", \"| a |\\n|---|\\n| 1 |\"} {\n\t\tgot := BlockRich(in)\n\t\tif got == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !strings.HasPrefix(got, \"\\n\\n\") {\n\t\t\tt.Errorf(\"BlockRich(%q) = %q; expected leading '\\\\n\\\\n'\", in, got)\n\t\t}\n\t}\n}\n\nfunc TestBlockRich_TrailingBlankLineGuaranteed(t *testing.T) {\n\t// Every non-empty result ends with \"\\n\\n\" so `BlockRich(user) +\n\t// chrome` cannot extend user's last paragraph into the realm's\n\t// next line (CM §5.2 lazy continuation, or a realm-supplied\n\t// `|---|` row retroactively promoting user's last line into a\n\t// `\u003cthead\u003e` header).\n\tfor _, in := range []string{\"x\", \"x\\n\", \"# h\", \"- i\\n- j\", \"| H |\\n|---|\\n| a |\"} {\n\t\tgot := BlockRich(in)\n\t\tif got == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !strings.HasSuffix(got, \"\\n\\n\") {\n\t\t\tt.Errorf(\"BlockRich(%q) = %q; expected trailing '\\\\n\\\\n'\", in, got)\n\t\t}\n\t}\n}\n\nfunc TestBlockquoteRich(t *testing.T) {\n\t// BlockquoteRich strips BlockRich's leading \"\\n\", line-prefixes\n\t// each remaining line with \"\u003e \", and emits \"\\n\" on both ends:\n\t// leading \"\\n\" prevents `chrome + BlockquoteRich(user)` from\n\t// landing the first `\u003e` mid-line; trailing \"\\n\\n\" (blank line)\n\t// prevents `BlockquoteRich(user) + chrome` from pulling chrome\n\t// into the quote via CM §5.2 lazy continuation.\n\tcases := []struct{ name, in, want string }{\n\t\t{\"plain text\", \"hello world\\n\", \"\\n\u003e hello world\\n\\n\"},\n\t\t{\"atx heading preserved\", \"# heading\\n\", \"\\n\u003e # heading\\n\\n\"},\n\t\t{\"nested blockquote\", \"\u003e nested\\n\", \"\\n\u003e \u003e nested\\n\\n\"},\n\t\t{\"list item preserved\", \"- item\\n\", \"\\n\u003e - item\\n\\n\"},\n\t\t{\"ordered list preserved\", \"1. item\\n\", \"\\n\u003e 1. item\\n\\n\"},\n\t\t{\"first-line setext escaped\", \"---\\n\", \"\\n\u003e \\\\---\\n\\n\"},\n\t\t{\"deeper setext preserved\", \"text\\n===\\n\", \"\\n\u003e text\\n\u003e ===\\n\\n\"},\n\t\t{\"thematic break asterisks preserved\", \"***\\n\", \"\\n\u003e ***\\n\\n\"},\n\t\t// Realm-binding defenses still on.\n\t\t{\"gno extension escaped\", \"\u003cgno-card\u003e\\n\", \"\\n\u003e \\\\\u003cgno-card\u003e\\n\\n\"},\n\t\t{\"ref-link use escaped\", \"[t][l]\\n\", \"\\n\u003e \\\\[t\\\\]\\\\[l\\\\]\\n\\n\"},\n\t\t{\"footnote escaped\", \"[^name]\\n\", \"\\n\u003e \\\\[^name\\\\]\\n\\n\"},\n\t\t// LRDs are stripped by BlockRich entirely; trimming the\n\t\t// resulting bare \"\\n\" leaves empty input and the helper\n\t\t// returns \"\" without emitting a blockquote.\n\t\t{\"lrd alone strips to empty\", \"[x]: y\\n\", \"\"},\n\t\t// Code fence autoclose at EOF lands inside the quote.\n\t\t{\"unclosed fence autoclosed\", \"```\\ncode\\n\", \"\\n\u003e ```\\n\u003e code\\n\u003e ```\\n\\n\"},\n\t\t// Multi-paragraph preserves the inner blank line (rendered\n\t\t// as a quoted blank line `\u003e \\n`).\n\t\t{\"multi-paragraph preserves blank\", \"a\\n\\nb\\n\", \"\\n\u003e a\\n\u003e \\n\u003e b\\n\\n\"},\n\t\t// Empty / blank-only input collapses cleanly to \"\".\n\t\t{\"empty input\", \"\", \"\"},\n\t\t// CRLF normalized through BlockRich.\n\t\t{\"crlf normalized\", \"a\\r\\nb\\n\", \"\\n\u003e a\\n\u003e b\\n\\n\"},\n\t\t// NUL replaced with U+FFFD by BlockRich.\n\t\t{\"nul replaced\", \"x\\x00y\\n\", \"\\n\u003e x\\uFFFDy\\n\\n\"},\n\t\t// Multiple trailing newlines collapse to the single trailing\n\t\t// blank line shape — output never ends with more than `\\n\\n`.\n\t\t{\"multiple trailing newlines collapse\", \"foo\\n\\n\\n\", \"\\n\u003e foo\\n\\n\"},\n\t\t// Internal tabs are preserved (BlockRich does not touch them).\n\t\t{\"internal tab preserved\", \"a\\tb\\n\", \"\\n\u003e a\\tb\\n\\n\"},\n\t\t// Whitespace-only input still yields a blockquote (the user\n\t\t// wrote literal spaces). The line-prefix loop emits `\u003e ` plus\n\t\t// the original two spaces.\n\t\t{\"whitespace-only input\", \"  \", \"\\n\u003e   \\n\\n\"},\n\t}\n\tfor _, c := range cases {\n\t\tgot := BlockquoteRich(c.in)\n\t\tif got != c.want {\n\t\t\tt.Errorf(\"%s: BlockquoteRich(%q) = %q, want %q\", c.name, c.in, got, c.want)\n\t\t}\n\t}\n}\n\nfunc TestBlockquoteRich_DoubleWrapNestsQuote(t *testing.T) {\n\t// Not idempotent: calling twice nests the quote one level\n\t// deeper. The aggressive TrimRight in BlockquoteRich also\n\t// collapses the inner trailing blank line, so the second pass\n\t// produces a clean `\u003e \u003e foo` nesting (no `\u003e ` quoted blank in\n\t// the middle) plus the outer trailing blank line.\n\tonce := BlockquoteRich(\"foo\\n\")\n\ttwice := BlockquoteRich(once)\n\tif want := \"\\n\u003e \u003e foo\\n\\n\"; twice != want {\n\t\tt.Errorf(\"BlockquoteRich(BlockquoteRich(%q)) = %q, want %q\", \"foo\\n\", twice, want)\n\t}\n}\n\nfunc TestBlockquoteRich_LeadingNewlineGuaranteed(t *testing.T) {\n\t// Every non-empty result starts with \"\\n\" so realm concatenation\n\t// like `chrome + BlockquoteRich(user)` doesn't place `\u003e` mid-line.\n\tfor _, in := range []string{\"x\", \"x\\n\", \"# h\", \"- i\\n- j\"} {\n\t\tgot := BlockquoteRich(in)\n\t\tif got == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif got[0] != '\\n' {\n\t\t\tt.Errorf(\"BlockquoteRich(%q) = %q; expected leading '\\\\n'\", in, got)\n\t\t}\n\t}\n}\n\nfunc TestBlockquoteRich_TrailingBlankLineGuaranteed(t *testing.T) {\n\t// Every non-empty result ends with \"\\n\\n\" so realm concatenation\n\t// like `BlockquoteRich(user) + \"more text\"` doesn't pull \"more\n\t// text\" into the quote via CM §5.2 lazy continuation.\n\tfor _, in := range []string{\"x\", \"x\\n\", \"# h\", \"- i\\n- j\", \"para\\n\\nmore\"} {\n\t\tgot := BlockquoteRich(in)\n\t\tif got == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !strings.HasSuffix(got, \"\\n\\n\") {\n\t\t\tt.Errorf(\"BlockquoteRich(%q) = %q; expected trailing '\\\\n\\\\n'\", in, got)\n\t\t}\n\t}\n}\n\nfunc TestBlockquoteRich_NoStrayEmptyQuotedLine(t *testing.T) {\n\t// BlockRich's own leading \"\\n\" must be stripped before\n\t// line-prefixing — otherwise the output would start with a\n\t// useless `\u003e \\n` empty quoted line.\n\tgot := BlockquoteRich(\"foo\\n\")\n\tif strings.HasPrefix(got, \"\\n\u003e \\n\") {\n\t\tt.Errorf(\"BlockquoteRich leaked BlockRich's leading '\\\\n' as `\u003e \\\\n`: %q\", got)\n\t}\n}\n\nfunc TestNeuterLeadingSetextIfQualifying(t *testing.T) {\n\tcases := []struct{ name, in, want string }{\n\t\t{\"leading-setext-h1\", \"===\\nbody\\n\", \"\\\\===\\nbody\\n\"},\n\t\t{\"leading-setext-h2\", \"---\\nbody\\n\", \"\\\\---\\nbody\\n\"},\n\t\t{\"leading-setext-indented\", \"   ===\\nbody\\n\", \"   \\\\===\\nbody\\n\"},\n\t\t{\"leading-setext-indented-4\", \"    ===\\nbody\\n\", \"    ===\\nbody\\n\"}, // indented code\n\t\t{\"leading-setext-trailing-ws\", \"===   \\nbody\\n\", \"\\\\===   \\nbody\\n\"},\n\t\t{\"leading-setext-mixed-chars\", \"=-=-\\nbody\\n\", \"=-=-\\nbody\\n\"},         // mixed; not setext\n\t\t{\"leading-setext-has-content\", \"=== text\\nbody\\n\", \"=== text\\nbody\\n\"}, // mixed content\n\t\t{\"setext-deeper-untouched\", \"title\\n===\\nfoo\\n\", \"title\\n===\\nfoo\\n\"},\n\t\t{\"leading-thematic-asterisk\", \"***\\nbody\\n\", \"***\\nbody\\n\"}, // not setext, untouched\n\t\t{\"leading-thematic-underscore\", \"___\\nbody\\n\", \"___\\nbody\\n\"},\n\t\t{\"leading-blank-then-setext\", \"\\n===\\nbody\\n\", \"\\n\\\\===\\nbody\\n\"},\n\t\t{\"leading-blank-ws-then-setext\", \"   \\n===\\nbody\\n\", \"   \\n\\\\===\\nbody\\n\"},\n\t\t{\"text-first\", \"hello\\n===\\n\", \"hello\\n===\\n\"}, // text before === — user-authored\n\t\t{\"empty\", \"\", \"\"},\n\t\t{\"all-blank\", \"   \\n\\n\", \"   \\n\\n\"},\n\t\t{\"only-equals\", \"===\", \"\\\\===\"},\n\t}\n\tfor _, c := range cases {\n\t\tif got := neuterLeadingSetextIfQualifying(c.in); got != c.want {\n\t\t\tt.Errorf(\"%s: neuterLeadingSetextIfQualifying(%q) = %q, want %q\", c.name, c.in, got, c.want)\n\t\t}\n\t}\n}\n\nfunc TestLinkTitle(t *testing.T) {\n\tcases := []struct{ in, want string }{\n\t\t{`he said \"hi\"`, `he said \\\"hi\\\"`},\n\t\t{`it's nice`, `it\\'s nice`},\n\t\t{\"line1\\nline2\", \"line1 line2\"},\n\t}\n\tfor _, c := range cases {\n\t\tif got := LinkTitle(c.in); got != c.want {\n\t\t\tt.Errorf(\"LinkTitle(%q) = %q, want %q\", c.in, got, c.want)\n\t\t}\n\t}\n}\n\nfunc TestTableCell(t *testing.T) {\n\tcases := []struct{ in, want string }{\n\t\t{\"plain cell\", \"plain cell\"},\n\t\t{\"a|b\", `a\\|b`},\n\t\t{\"a\\tb\", \"a b\"},\n\t\t{\"a*b|c\", `a\\*b\\|c`},\n\t}\n\tfor _, c := range cases {\n\t\tif got := TableCell(c.in); got != c.want {\n\t\t\tt.Errorf(\"TableCell(%q) = %q, want %q\", c.in, got, c.want)\n\t\t}\n\t}\n}\n\nfunc TestHTMLEscape(t *testing.T) {\n\tcases := []struct{ in, want string }{\n\t\t{\"plain\", \"plain\"},\n\t\t{\"\u003cscript\u003e\", \"\u0026lt;script\u0026gt;\"},\n\t\t{`a \u0026 b`, \"a \u0026amp; b\"},\n\t\t{`\"quoted\"`, \"\u0026#34;quoted\u0026#34;\"},\n\t}\n\tfor _, c := range cases {\n\t\tif got := HTMLEscape(c.in); got != c.want {\n\t\t\tt.Errorf(\"HTMLEscape(%q) = %q, want %q\", c.in, got, c.want)\n\t\t}\n\t}\n}\n\nfunc TestURL(t *testing.T) {\n\tcases := []struct{ in, want string }{\n\t\t{\"https://example.com/x\", \"https://example.com/x\"},\n\t\t{\"http://example.com\", \"http://example.com\"},\n\t\t{\"mailto:a@b.com\", \"mailto:a@b.com\"},\n\t\t{\"mailto:a@b.com?body=phish\", \"\"},            // any query is a prefill-phishing vector\n\t\t{\"mailto:a@b.com?BODY=phish\", \"\"},            // header name case is irrelevant\n\t\t{\"mailto:a@b.com?Body=phish\", \"\"},            // mixed case\n\t\t{\"mailto:a@b.com?%62ody=phish\", \"\"},          // percent-encoded header (%62 == b)\n\t\t{\"mailto:a@b.com?subject=hi\u0026body=phish\", \"\"}, // body as second param\n\t\t{\"mailto:a@b.com?subject=hi\u0026BODY=phish\", \"\"}, // mixed case second param\n\t\t{\"mailto:a@b.com?subject=hello\", \"\"},         // subject alone still rejected\n\t\t{\"mailto:a@b.com?cc=x@y.com\", \"\"},            // cc rejected\n\t\t{\"mailto:a@b.com?bcc=x@y.com\", \"\"},           // bcc rejected\n\t\t{\"mailto:a@b.com?\", \"\"},                      // bare trailing query marker rejected\n\t\t{\"mailto:a@b.com\u0026#x3f;body=phish\", \"\"},       // hex char-ref ? — renderer decodes to ?body=\n\t\t{\"mailto:a@b.com\u0026#63;body=phish\", \"\"},        // decimal char-ref ? — same bypass\n\t\t{\"mailto:a@b.com\u0026quest;body=phish\", \"\"},      // named char-ref ? — same bypass\n\t\t{\"mailto:a@b.com\u0026body=phish\", \"\"},            // bare \u0026 gateway rejected\n\t\t{\"//evil.com\", \"\"},                           // protocol-relative rejected\n\t\t{\"javascript:alert(1)\", \"\"},                  // bad scheme\n\t\t{\"/r/foo\", \"/r/foo\"},                         // relative\n\t\t{\"./local\", \"./local\"},\n\t\t{\"#section\", \"#section\"}, // fragment-only\n\t\t{\"\", \"\"},\n\t\t{\"   \", \"\"},\n\t\t{\"https://a.com/path with space\", \"https://a.com/path%20with%20space\"},\n\t}\n\tfor _, c := range cases {\n\t\tif got := URL(c.in); got != c.want {\n\t\t\tt.Errorf(\"URL(%q) = %q, want %q\", c.in, got, c.want)\n\t\t}\n\t}\n}\n\nfunc TestImageURL(t *testing.T) {\n\tcases := []struct{ in, want string }{\n\t\t{\"https://example.com/img.png\", \"https://example.com/img.png\"},\n\t\t{\"mailto:a@b.com\", \"\"}, // mailto rejected for images\n\t\t{\"data:image/svg+xml,\u003csvg/\u003e\", \"data:image/svg+xml,%3Csvg/%3E\"},\n\t\t{\"data:image/png;base64,XXX\", \"data:image/png;base64,XXX\"},\n\t\t{\"data:text/html,\u003cscript\u003e\", \"\"}, // bad data subset\n\t\t{\"javascript:alert(1)\", \"\"},\n\t\t{\"\", \"\"},\n\t}\n\tfor _, c := range cases {\n\t\tif got := ImageURL(c.in); got != c.want {\n\t\t\tt.Errorf(\"ImageURL(%q) = %q, want %q\", c.in, got, c.want)\n\t\t}\n\t}\n}\n\nfunc TestUserName(t *testing.T) {\n\tcases := []struct{ in, want string }{\n\t\t{\"alice\", \"alice\"},\n\t\t{\"alice123\", \"alice123\"},\n\t\t{\"alice_bob-cat\", \"alice_bob-cat\"},\n\t\t{\"Alice\", \"\"},  // uppercase first\n\t\t{\"1alice\", \"\"}, // digit first\n\t\t{\"\", \"\"},\n\t\t{\"a\\u200Blice\", \"alice\"}, // bidi stripped, then matches\n\t}\n\tfor _, c := range cases {\n\t\tif got := UserName(c.in); got != c.want {\n\t\t\tt.Errorf(\"UserName(%q) = %q, want %q\", c.in, got, c.want)\n\t\t}\n\t}\n}\n\nfunc TestBechString(t *testing.T) {\n\taddrG := \"g1abc123def456ghi789jkl012mno345p\"\n\tcases := []struct {\n\t\ts, prefix string\n\t\twant      string\n\t}{\n\t\t{addrG, \"g\", addrG},\n\t\t{addrG, \"\", addrG},  // any prefix\n\t\t{addrG, \"gpub\", \"\"}, // wrong prefix\n\t\t{\"gpub1abc123def456ghijklmn\", \"gpub\", \"gpub1abc123def456ghijklmn\"},\n\t\t{\"gpub1abc123def456ghijklmn\", \"\", \"gpub1abc123def456ghijklmn\"},\n\t\t{\"b1xyz789abc123def456\", \"\", \"b1xyz789abc123def456\"}, // any-prefix mode allows b1...\n\t\t{\"g1ABC\", \"g\", \"\"}, // uppercase rejected\n\t\t{\"x\", \"g\", \"\"},\n\t\t{\"\", \"g\", \"\"},\n\t}\n\tfor _, c := range cases {\n\t\tif got := BechString(c.s, c.prefix); got != c.want {\n\t\t\tt.Errorf(\"BechString(%q,%q) = %q, want %q\", c.s, c.prefix, got, c.want)\n\t\t}\n\t}\n}\n\nfunc TestFootnoteLabel(t *testing.T) {\n\tcases := []struct{ in, want string }{\n\t\t{\"note1\", \"note1\"},\n\t\t{\"Note_A-1\", \"Note_A-1\"},\n\t\t{\"with space\", \"\"},\n\t\t{\"\", \"\"},\n\t}\n\tfor _, c := range cases {\n\t\tif got := FootnoteLabel(c.in); got != c.want {\n\t\t\tt.Errorf(\"FootnoteLabel(%q) = %q, want %q\", c.in, got, c.want)\n\t\t}\n\t}\n}\n\nfunc TestLanguageName(t *testing.T) {\n\tcases := []struct{ in, want string }{\n\t\t{\"go\", \"go\"},\n\t\t{\"c++\", \"c++\"},\n\t\t{\"python3\", \"python3\"},\n\t\t{\"objective-c\", \"objective-c\"},\n\t\t{\"with space\", \"\"},\n\t\t{\"\", \"\"},\n\t}\n\tfor _, c := range cases {\n\t\tif got := LanguageName(c.in); got != c.want {\n\t\t\tt.Errorf(\"LanguageName(%q) = %q, want %q\", c.in, got, c.want)\n\t\t}\n\t}\n}\n\nfunc TestNestedPrefix(t *testing.T) {\n\tcases := []struct{ in, want string }{\n\t\t{\"\", \"\"},\n\t\t{\"  \", \"  \"},\n\t\t{\"\\t\", \"\\t\"},\n\t\t{\"\u003e \", \"\u003e \"},\n\t\t{\"\u003e \u003e \", \"\u003e \u003e \"},\n\t\t{\"## \", \"\"}, // markdown-active prefix rejected\n\t\t{\"- \", \"\"},\n\t}\n\tfor _, c := range cases {\n\t\tif got := NestedPrefix(c.in); got != c.want {\n\t\t\tt.Errorf(\"NestedPrefix(%q) = %q, want %q\", c.in, got, c.want)\n\t\t}\n\t}\n}\n\nfunc TestCodeFence(t *testing.T) {\n\tif got := CodeFence(\"```\", 3); got != \"````\" {\n\t\tt.Errorf(\"CodeFence: got %q, want %q\", got, \"````\")\n\t}\n\tif got := CodeFence(\"\", 3); got != \"```\" {\n\t\tt.Errorf(\"CodeFence empty: got %q, want %q\", got, \"```\")\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"ms2DE/snvTpxBjKODeFxe3aDvoBBP8J2w4nO8tY5AtYUcGML9yOV8gSjDH8VXN1SO7ZJB32W8rTV3Qi9+GKtxg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","package":{"name":"md","path":"gno.land/p/moul/md/v0","files":[{"name":"example_test.gno","body":"package md\n\nimport \"strings\"\n\nfunc ExampleHeaders() {\n\tprintln(H1(\"Header 1\"))\n\tprintln(H2(\"Header 2\"))\n\tprintln(H3(\"Header 3\"))\n\tprintln(H4(\"Header 4\"))\n\tprintln(H5(\"Header 5\"))\n\tprintln(H6(\"Header 6\"))\n\n\t// Output:\n\t// # Header 1\n\t//\n\t// ## Header 2\n\t//\n\t// ### Header 3\n\t//\n\t// #### Header 4\n\t//\n\t// ##### Header 5\n\t//\n\t// ###### Header 6\n}\n\nfunc ExampleStyles() {\n\tprintln(Bold(\"bold\"))\n\tprintln(Italic(\"italic\"))\n\tprintln(Strikethrough(\"strikethrough\"))\n\n\t// Output:\n\t// **bold**\n\t// *italic*\n\t// ~~strikethrough~~\n}\n\nfunc ExampleLists() {\n\tprintln(BulletList([]string{\n\t\t\"Item 1\",\n\t\t\"Item 2\\nMore details for item 2\",\n\t}))\n\tprintln(OrderedList([]string{\"Step 1\", \"Step 2\"}))\n\tprintln(TodoList([]string{\"Task 1\", \"Task 2\\nSubtask 2\"}, []bool{true, false}))\n\tprintln(Nested(BulletList([]string{\"Parent Item\", OrderedList([]string{\"Child 1\", \"Child 2\"})}), \"  \"))\n\n\t// Output:\n\t// - Item 1\n\t// - Item 2\n\t//   More details for item 2\n\t//\n\t// 1. Step 1\n\t// 2. Step 2\n\t//\n\t// - [x] Task 1\n\t// - [ ] Task 2\n\t//   Subtask 2\n\t//\n\t//   - Parent Item\n\t//   - 1. Child 1\n\t//     2. Child 2\n}\n\nfunc ExampleBlocks() {\n\tprint(Paragraph(\"This is a paragraph.\"))\n\t// Blockquote adds multiple newlines which Output doesn't allow, so trim\n\tprintln(strings.TrimSpace(Blockquote(\"This is a blockquote\\nSpanning multiple lines\")))\n\n\t// Output:\n\t// This is a paragraph.\n\t//\n\t// \u003e This is a blockquote\n\t// \u003e Spanning multiple lines\n}\n\nfunc ExampleCode() {\n\tprintln(InlineCode(\"inline `code`\"))\n\tprintln(CodeBlock(\"line1\\nline2\"))\n\tprintln(LanguageCodeBlock(\"go\", \"func main() {\\nprintln(\\\"Hello, world!\\\")\\n}\"))\n\tprintln(HorizontalRule())\n\n\t// Output:\n\t// `` inline `code` ``\n\t// ```\n\t// line1\n\t// line2\n\t// ```\n\t//\n\t// ```go\n\t// func main() {\n\t// println(\"Hello, world!\")\n\t// }\n\t// ```\n\t//\n\t// ---\n}\n\nfunc ExampleReferences() {\n\tprintln(Link(\"Gno\", \"http://gno.land\"))\n\tprintln(Image(\"Alt Text\", \"http://example.com/image.png\"))\n\tprintln(InlineImageWithLink(\"Alt Text\", \"http://example.com/image.png\", \"http://example.com\"))\n\tprintln(FootnoteDefinition(\"ref\", \"This is a footnote\"))\n\t// LinkReferenceDefinition adds multiple newlines which Output doesn't allow, so trim\n\tprintln(strings.TrimSpace(LinkReferenceDefinition(\"r-example\", \"/r/example\", \"\")))\n\n\t// Output:\n\t//\n\t// [Gno](http://gno.land)\n\t// ![Alt Text](http://example.com/image.png)\n\t// [![Alt Text](http://example.com/image.png)](http://example.com)\n\t// [^ref]:\n\t//     This is a footnote\n\t//\n\t// [r-example]: /r/example\n}\n\nfunc ExampleColumns() {\n\tprintln(\"4 columns in one gno-columns tag:\")\n\tprintln(Columns([]string{\n\t\t\"Column1\\ncontent1\",\n\t\t\"Column2\\ncontent2\",\n\t\t\"Column3\\ncontent3\",\n\t\t\"Column4\\ncontent4\",\n\t}, true))\n\n\t// Should be automatically placed in multiple column tags\n\tprintln(\"3 cols per row without padding:\")\n\tprintln(ColumnsN([]string{\n\t\t\"Row1Column1\\ncontent1\",\n\t\t\"Row1Column2\\ncontent2\",\n\t\t\"Row1Column3\\ncontent3\",\n\t\t\"Row2Column1\\ncontent1\",\n\t\t\"Row2Column2\\ncontent2\",\n\t\t\"Row2Column3\\ncontent3\",\n\t\t\"Row3Column1\\ncontent1\",\n\t\t\"Row3Column2\\ncontent2\",\n\t\t\"Row3Column3\\ncontent3\",\n\t}, 3, false))\n\n\t// Should be padded, up to 4 cols\n\tprintln(\"2 padded to 4:\")\n\tprintln(ColumnsN([]string{\n\t\t\"Column1\\ncontent1\",\n\t\t\"Column2\\ncontent2\",\n\t}, 4, true))\n\n\t// Output:\n\t// 4 columns in one gno-columns tag:\n\t// \u003cgno-columns\u003e\n\t// Column1\n\t// content1\n\t// \u003cgno-columns-sep\u003e\n\t// Column2\n\t// content2\n\t// \u003cgno-columns-sep\u003e\n\t// Column3\n\t// content3\n\t// \u003cgno-columns-sep\u003e\n\t// Column4\n\t// content4\n\t// \u003c/gno-columns\u003e\n\t//\n\t// 3 cols per row without padding:\n\t// \u003cgno-columns\u003e\n\t// Row1Column1\n\t// content1\n\t// \u003cgno-columns-sep\u003e\n\t// Row1Column2\n\t// content2\n\t// \u003cgno-columns-sep\u003e\n\t// Row1Column3\n\t// content3\n\t// \u003c/gno-columns\u003e\n\t// \u003cgno-columns\u003e\n\t// Row2Column1\n\t// content1\n\t// \u003cgno-columns-sep\u003e\n\t// Row2Column2\n\t// content2\n\t// \u003cgno-columns-sep\u003e\n\t// Row2Column3\n\t// content3\n\t// \u003c/gno-columns\u003e\n\t// \u003cgno-columns\u003e\n\t// Row3Column1\n\t// content1\n\t// \u003cgno-columns-sep\u003e\n\t// Row3Column2\n\t// content2\n\t// \u003cgno-columns-sep\u003e\n\t// Row3Column3\n\t// content3\n\t// \u003c/gno-columns\u003e\n\t//\n\t// 2 padded to 4:\n\t// \u003cgno-columns\u003e\n\t// Column1\n\t// content1\n\t// \u003cgno-columns-sep\u003e\n\t// Column2\n\t// content2\n\t// \u003cgno-columns-sep\u003e\n\t//\n\t// \u003cgno-columns-sep\u003e\n\t//\n\t// \u003c/gno-columns\u003e\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/md/v0\"\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"},{"name":"md_test.gno","body":"package md_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/moul/md/v0\"\n)\n\nfunc TestHelpers(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tfunction func() string\n\t\texpected string\n\t}{\n\t\t{\"Bold\", func() string { return md.Bold(\"foo\") }, \"**foo**\"},\n\t\t{\"Italic\", func() string { return md.Italic(\"foo\") }, \"*foo*\"},\n\t\t{\"Strikethrough\", func() string { return md.Strikethrough(\"foo\") }, \"~~foo~~\"},\n\t\t{\"H1\", func() string { return md.H1(\"foo\") }, \"# foo\\n\"},\n\t\t{\"HorizontalRule\", md.HorizontalRule, \"---\\n\"},\n\t\t{\"InlineCode\", func() string { return md.InlineCode(\"foo\") }, \"`foo`\"},\n\t\t{\"CodeBlock\", func() string { return md.CodeBlock(\"foo\") }, \"```\\nfoo\\n```\\n\"},\n\t\t{\"LanguageCodeBlock\", func() string { return md.LanguageCodeBlock(\"go\", \"foo\") }, \"```go\\nfoo\\n```\\n\"},\n\t\t{\"Link\", func() string { return md.Link(\"foo\", \"http://example.com\") }, \"[foo](http://example.com)\"},\n\t\t{\"UserLink\", func() string { return md.UserLink(\"moul\") }, \"[@moul](/u/moul)\"},\n\t\t{\"Image\", func() string { return md.Image(\"foo\", \"http://example.com\") }, \"![foo](http://example.com)\"},\n\t\t{\"InlineImageWithLink\", func() string {\n\t\t\treturn md.InlineImageWithLink(\"alt\", \"http://img.example.com/x.png\", \"http://link.example.com\")\n\t\t}, \"[![alt](http://img.example.com/x.png)](http://link.example.com)\"},\n\t\t{\"FootnoteDefinition\", func() string { return md.FootnoteDefinition(\"foo\", \"bar\") }, \"[^foo]:\\n    bar\\n\"},\n\t\t{\"LinkReferenceDefinition\", func() string {\n\t\t\treturn md.LinkReferenceDefinition(\"foo\", \"http://example.com\", \"\")\n\t\t}, \"\\n\\n[foo]: http://example.com\\n\\n\"},\n\t\t{\"Paragraph\", func() string { return md.Paragraph(\"foo\") }, \"foo\\n\\n\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := tt.function()\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"%s() = %q, want %q\", tt.name, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLists(t *testing.T) {\n\tt.Run(\"BulletList\", func(t *testing.T) {\n\t\titems := []string{\"foo\", \"bar\"}\n\t\texpected := \"- foo\\n- bar\\n\"\n\t\tresult := md.BulletList(items)\n\t\tif result != expected {\n\t\t\tt.Errorf(\"BulletList(%q) = %q, want %q\", items, result, expected)\n\t\t}\n\t})\n\n\tt.Run(\"OrderedList\", func(t *testing.T) {\n\t\titems := []string{\"foo\", \"bar\"}\n\t\texpected := \"1. foo\\n2. bar\\n\"\n\t\tresult := md.OrderedList(items)\n\t\tif result != expected {\n\t\t\tt.Errorf(\"OrderedList(%q) = %q, want %q\", items, result, expected)\n\t\t}\n\t})\n\n\tt.Run(\"TodoList\", func(t *testing.T) {\n\t\titems := []string{\"foo\", \"bar\\nmore bar\"}\n\t\tdone := []bool{true, false}\n\t\texpected := \"- [x] foo\\n- [ ] bar\\n  more bar\\n\"\n\t\tresult := md.TodoList(items, done)\n\t\tif result != expected {\n\t\t\tt.Errorf(\"TodoList(%q, %q) = %q, want %q\", items, done, result, expected)\n\t\t}\n\t})\n}\n\nfunc TestUserLink(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\"username\", \"moul\", \"[@moul](/u/moul)\"},\n\t\t// \"g1blah\" data part is too short for a bech32 address (min 6\n\t\t// chars after \"1\"); UserLink validates as a username instead.\n\t\t{\"short g1-prefixed string treated as username\", \"g1blah\", \"[@g1blah](/u/g1blah)\"},\n\t\t// \"user_name\" is a valid username (charset includes _ and -);\n\t\t// the validator returns it verbatim, so no markdown escape of _.\n\t\t{\"username with underscore\", \"user_name\", \"[@user_name](/u/user_name)\"},\n\t\t// \"g1abc123\" has a 6-char data part — valid bech32 shape.\n\t\t{\"address with 6-char data\", \"g1abc123\", \"[g1abc123](/u/g1abc123)\"},\n\t\t// invalid identifier (spaces, uppercase, etc.) → empty.\n\t\t{\"rejected: spaces\", \"hello world\", \"\"},\n\t\t{\"rejected: uppercase first char\", \"Alice\", \"\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := md.UserLink(tt.input)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"UserLink(%q) = %q, want %q\", tt.input, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNested(t *testing.T) {\n\tt.Run(\"Nested Single Level\", func(t *testing.T) {\n\t\tcontent := \"- foo\\n- bar\"\n\t\texpected := \"  - foo\\n  - bar\"\n\t\tresult := md.Nested(content, \"  \")\n\t\tif result != expected {\n\t\t\tt.Errorf(\"Nested(%q) = %q, want %q\", content, result, expected)\n\t\t}\n\t})\n\n\tt.Run(\"Nested Double Level\", func(t *testing.T) {\n\t\tcontent := \"  - foo\\n  - bar\"\n\t\texpected := \"    - foo\\n    - bar\"\n\t\tresult := md.Nested(content, \"  \")\n\t\tif result != expected {\n\t\t\tt.Errorf(\"Nested(%q) = %q, want %q\", content, result, expected)\n\t\t}\n\t})\n}\n\nfunc TestColumns(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []string\n\t\tpadded   bool\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"no columns\",\n\t\t\tinput:    []string{},\n\t\t\tpadded:   false,\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"no columns padded\",\n\t\t\tinput:    []string{},\n\t\t\tpadded:   true,\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:   \"one column\",\n\t\t\tinput:  []string{\"Column 1\"},\n\t\t\tpadded: false,\n\t\t\texpected: `\u003cgno-columns\u003e\nColumn 1\n\u003c/gno-columns\u003e\n`,\n\t\t},\n\t\t{\n\t\t\tname:   \"one column padded\",\n\t\t\tinput:  []string{\"Column 1\"},\n\t\t\tpadded: true,\n\t\t\texpected: `\u003cgno-columns\u003e\nColumn 1\n\u003cgno-columns-sep\u003e\n\n\u003cgno-columns-sep\u003e\n\n\u003cgno-columns-sep\u003e\n\n\u003c/gno-columns\u003e\n`,\n\t\t},\n\t\t{\n\t\t\tname:   \"two columns\",\n\t\t\tinput:  []string{\"Column 1\", \"Column 2\"},\n\t\t\tpadded: false,\n\t\t\texpected: `\u003cgno-columns\u003e\nColumn 1\n\u003cgno-columns-sep\u003e\nColumn 2\n\u003c/gno-columns\u003e\n`,\n\t\t},\n\t\t{\n\t\t\tname:   \"two columns padded\",\n\t\t\tinput:  []string{\"Column 1\", \"Column 2\"},\n\t\t\tpadded: true,\n\t\t\texpected: `\u003cgno-columns\u003e\nColumn 1\n\u003cgno-columns-sep\u003e\nColumn 2\n\u003cgno-columns-sep\u003e\n\n\u003cgno-columns-sep\u003e\n\n\u003c/gno-columns\u003e\n`,\n\t\t},\n\t\t{\n\t\t\tname:   \"four columns\",\n\t\t\tinput:  []string{\"A\", \"B\", \"C\", \"D\"},\n\t\t\tpadded: false,\n\t\t\texpected: `\u003cgno-columns\u003e\nA\n\u003cgno-columns-sep\u003e\nB\n\u003cgno-columns-sep\u003e\nC\n\u003cgno-columns-sep\u003e\nD\n\u003c/gno-columns\u003e\n`,\n\t\t},\n\t\t{\n\t\t\tname:   \"four columns padded\",\n\t\t\tinput:  []string{\"A\", \"B\", \"C\", \"D\"},\n\t\t\tpadded: true,\n\t\t\texpected: `\u003cgno-columns\u003e\nA\n\u003cgno-columns-sep\u003e\nB\n\u003cgno-columns-sep\u003e\nC\n\u003cgno-columns-sep\u003e\nD\n\u003c/gno-columns\u003e\n`,\n\t\t},\n\t\t{\n\t\t\tname:   \"more than four columns\",\n\t\t\tinput:  []string{\"1\", \"2\", \"3\", \"4\", \"5\"},\n\t\t\tpadded: false,\n\t\t\texpected: `\u003cgno-columns\u003e\n1\n\u003cgno-columns-sep\u003e\n2\n\u003cgno-columns-sep\u003e\n3\n\u003cgno-columns-sep\u003e\n4\n\u003cgno-columns-sep\u003e\n5\n\u003c/gno-columns\u003e\n`,\n\t\t},\n\t\t{\n\t\t\tname:   \"more than four columns padded\",\n\t\t\tinput:  []string{\"1\", \"2\", \"3\", \"4\", \"5\"},\n\t\t\tpadded: true,\n\t\t\texpected: `\u003cgno-columns\u003e\n1\n\u003cgno-columns-sep\u003e\n2\n\u003cgno-columns-sep\u003e\n3\n\u003cgno-columns-sep\u003e\n4\n\u003cgno-columns-sep\u003e\n5\n\u003cgno-columns-sep\u003e\n\n\u003cgno-columns-sep\u003e\n\n\u003cgno-columns-sep\u003e\n\n\u003c/gno-columns\u003e\n`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := md.Columns(tt.input, tt.padded)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"Columns(%v, %v) =\\n%q\\nwant:\\n%q\", tt.input, tt.padded, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestColumnsN(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tcontent    []string\n\t\tcolsPerRow int\n\t\tpadded     bool\n\t\texpected   string\n\t}{\n\t\t{\n\t\t\tname:       \"empty input\",\n\t\t\tcontent:    []string{},\n\t\t\tcolsPerRow: 2,\n\t\t\tpadded:     false,\n\t\t\texpected:   \"\",\n\t\t},\n\t\t{\n\t\t\tname:       \"colsPerRow \u003c= 0\",\n\t\t\tcontent:    []string{\"A\", \"B\", \"C\"},\n\t\t\tcolsPerRow: 0,\n\t\t\tpadded:     false,\n\t\t\texpected: `\u003cgno-columns\u003e\nA\n\u003cgno-columns-sep\u003e\nB\n\u003cgno-columns-sep\u003e\nC\n\u003c/gno-columns\u003e\n`,\n\t\t},\n\t\t{\n\t\t\tname:       \"exact full row, no padding\",\n\t\t\tcontent:    []string{\"A\", \"B\"},\n\t\t\tcolsPerRow: 2,\n\t\t\tpadded:     false,\n\t\t\texpected: `\u003cgno-columns\u003e\nA\n\u003cgno-columns-sep\u003e\nB\n\u003c/gno-columns\u003e\n`,\n\t\t},\n\t\t{\n\t\t\tname:       \"partial last row, no padding\",\n\t\t\tcontent:    []string{\"A\", \"B\", \"C\"},\n\t\t\tcolsPerRow: 2,\n\t\t\tpadded:     false,\n\t\t\texpected: `\u003cgno-columns\u003e\nA\n\u003cgno-columns-sep\u003e\nB\n\u003c/gno-columns\u003e\n\u003cgno-columns\u003e\nC\n\u003c/gno-columns\u003e\n`,\n\t\t},\n\t\t{\n\t\t\tname:       \"partial last row, with padding\",\n\t\t\tcontent:    []string{\"A\", \"B\", \"C\"},\n\t\t\tcolsPerRow: 2,\n\t\t\tpadded:     true,\n\t\t\texpected: `\u003cgno-columns\u003e\nA\n\u003cgno-columns-sep\u003e\nB\n\u003c/gno-columns\u003e\n\u003cgno-columns\u003e\nC\n\u003cgno-columns-sep\u003e\n\n\u003c/gno-columns\u003e\n`,\n\t\t},\n\t\t{\n\t\t\tname:       \"padded with more empty cells\",\n\t\t\tcontent:    []string{\"X\"},\n\t\t\tcolsPerRow: 3,\n\t\t\tpadded:     true,\n\t\t\texpected: `\u003cgno-columns\u003e\nX\n\u003cgno-columns-sep\u003e\n\n\u003cgno-columns-sep\u003e\n\n\u003c/gno-columns\u003e\n`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := md.ColumnsN(tt.content, tt.colsPerRow, tt.padded)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"ColumnsN(%v, %d, %v) =\\n%q\\nwant:\\n%q\", tt.content, tt.colsPerRow, tt.padded, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"AtJJlIASqWfbwVwYXIWI9jFGmX2zV7+Rc8MsVMY+MPhHdbbzL5bIDlxGBSXvZ4cGsi8J9i2+8XdYmYNgvRBYaA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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/pager/v0` - pagination helper for trees and lists.\n- `gno.land/p/nt/avl/rotree/v0` - 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 - Choose storage types by access pattern](https://docs.gno.land/resources/effective-gno#choose-storage-types-by-access-pattern)\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":"node_test.gno","body":"package avl\n\nimport (\n\t\"sort\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nfunc TestTraverseByOffset(t *testing.T) {\n\tconst testStrings = `Alfa\nAlfred\nAlpha\nAlphabet\nBeta\nBeth\nBook\nBrowser`\n\ttt := []struct {\n\t\tname string\n\t\tasc  bool\n\t}{\n\t\t{\"ascending\", true},\n\t\t{\"descending\", false},\n\t}\n\n\tfor _, tt := range tt {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// use sl to insert the values, and reversed to match the values\n\t\t\t// we do this to ensure that the order of TraverseByOffset is independent\n\t\t\t// from the insertion order\n\t\t\tsl := strings.Split(testStrings, \"\\n\")\n\t\t\tsort.Strings(sl)\n\t\t\treversed := append([]string{}, sl...)\n\t\t\treverseSlice(reversed)\n\n\t\t\tif !tt.asc {\n\t\t\t\tsl, reversed = reversed, sl\n\t\t\t}\n\n\t\t\tr := NewNode(reversed[0], nil)\n\t\t\tfor _, v := range reversed[1:] {\n\t\t\t\tr, _ = r.Set(v, nil)\n\t\t\t}\n\n\t\t\tvar result []string\n\t\t\tfor i := 0; i \u003c len(sl); i++ {\n\t\t\t\tr.TraverseByOffset(i, 1, tt.asc, true, func(n *Node) bool {\n\t\t\t\t\tresult = append(result, n.Key())\n\t\t\t\t\treturn false\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif !slicesEqual(sl, result) {\n\t\t\t\tt.Errorf(\"want %v got %v\", sl, result)\n\t\t\t}\n\n\t\t\tfor l := 2; l \u003c= len(sl); l++ {\n\t\t\t\t// \"slices\"\n\t\t\t\tfor i := 0; i \u003c= len(sl); i++ {\n\t\t\t\t\tmax := i + l\n\t\t\t\t\tif max \u003e len(sl) {\n\t\t\t\t\t\tmax = len(sl)\n\t\t\t\t\t}\n\t\t\t\t\texp := sl[i:max]\n\t\t\t\t\tactual := []string{}\n\n\t\t\t\t\tr.TraverseByOffset(i, l, tt.asc, true, func(tr *Node) bool {\n\t\t\t\t\t\tactual = append(actual, tr.Key())\n\t\t\t\t\t\treturn false\n\t\t\t\t\t})\n\t\t\t\t\tif !slicesEqual(exp, actual) {\n\t\t\t\t\t\tt.Errorf(\"want %v got %v\", exp, actual)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTraverseByOffsetNegativeOffset(t *testing.T) {\n\t// a negative offset must be treated as 0, not silently drop the right\n\t// subtree. building {\"a\",\"b\",\"c\",\"d\"} yields a tree whose root has two\n\t// inner children, exercising the traverseByOffset delta computation.\n\tkeys := []string{\"a\", \"b\", \"c\", \"d\"}\n\tr := NewNode(keys[0], nil)\n\tfor _, k := range keys[1:] {\n\t\tr, _ = r.Set(k, nil)\n\t}\n\n\tfor _, offset := range []int{-1, -2, -100} {\n\t\tvar got []string\n\t\tr.TraverseByOffset(offset, len(keys), true, true, func(n *Node) bool {\n\t\t\tgot = append(got, n.Key())\n\t\t\treturn false\n\t\t})\n\t\tif !slicesEqual(keys, got) {\n\t\t\tt.Errorf(\"offset %d: want %v got %v\", offset, keys, got)\n\t\t}\n\t}\n}\n\nfunc TestHas(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []string\n\t\thasKey   string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\t\"has key in non-empty tree\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"B\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"does not have key in non-empty tree\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"F\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"has key in single-node tree\",\n\t\t\t[]string{\"A\"},\n\t\t\t\"A\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"does not have key in single-node tree\",\n\t\t\t[]string{\"A\"},\n\t\t\t\"B\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"does not have key in empty tree\",\n\t\t\t[]string{},\n\t\t\t\"A\",\n\t\t\tfalse,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\tresult := tree.Has(tt.hasKey)\n\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"Expected %v, got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGet(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tinput        []string\n\t\tgetKey       string\n\t\texpectIdx    int\n\t\texpectVal    any\n\t\texpectExists bool\n\t}{\n\t\t{\n\t\t\t\"get existing key\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"B\",\n\t\t\t1,\n\t\t\tnil,\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"get non-existent key (smaller)\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"@\",\n\t\t\t0,\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"get non-existent key (larger)\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"F\",\n\t\t\t5,\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"get from empty tree\",\n\t\t\t[]string{},\n\t\t\t\"A\",\n\t\t\t0,\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\tidx, val, exists := tree.Get(tt.getKey)\n\n\t\t\tif idx != tt.expectIdx {\n\t\t\t\tt.Errorf(\"Expected index %d, got %d\", tt.expectIdx, idx)\n\t\t\t}\n\n\t\t\tif val != tt.expectVal {\n\t\t\t\tt.Errorf(\"Expected value %v, got %v\", tt.expectVal, val)\n\t\t\t}\n\n\t\t\tif exists != tt.expectExists {\n\t\t\t\tt.Errorf(\"Expected exists %t, got %t\", tt.expectExists, exists)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetByIndex(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tinput       []string\n\t\tidx         int\n\t\texpectKey   string\n\t\texpectVal   any\n\t\texpectPanic bool\n\t}{\n\t\t{\n\t\t\t\"get by valid index\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t2,\n\t\t\t\"C\",\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"get by valid index (smallest)\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t0,\n\t\t\t\"A\",\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"get by valid index (largest)\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t4,\n\t\t\t\"E\",\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"get by invalid index (negative)\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t-1,\n\t\t\t\"\",\n\t\t\tnil,\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"get by invalid index (out of range)\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t5,\n\t\t\t\"\",\n\t\t\tnil,\n\t\t\ttrue,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\tif tt.expectPanic {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r == nil {\n\t\t\t\t\t\tt.Errorf(\"Expected a panic but didn't get one\")\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t}\n\n\t\t\tkey, val := tree.GetByIndex(tt.idx)\n\n\t\t\tif !tt.expectPanic {\n\t\t\t\tif key != tt.expectKey {\n\t\t\t\t\tt.Errorf(\"Expected key %s, got %s\", tt.expectKey, key)\n\t\t\t\t}\n\n\t\t\t\tif val != tt.expectVal {\n\t\t\t\t\tt.Errorf(\"Expected value %v, got %v\", tt.expectVal, val)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRemove(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tinput     []string\n\t\tremoveKey string\n\t\texpected  []string\n\t}{\n\t\t{\n\t\t\t\"remove leaf node\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"D\"},\n\t\t\t\"B\",\n\t\t\t[]string{\"A\", \"C\", \"D\"},\n\t\t},\n\t\t{\n\t\t\t\"remove node with one child\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"D\"},\n\t\t\t\"A\",\n\t\t\t[]string{\"B\", \"C\", \"D\"},\n\t\t},\n\t\t{\n\t\t\t\"remove node with two children\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"C\",\n\t\t\t[]string{\"A\", \"B\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"remove root node\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"C\",\n\t\t\t[]string{\"A\", \"B\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"remove non-existent key\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"F\",\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\ttree, _, _, _ = tree.Remove(tt.removeKey)\n\n\t\t\tresult := make([]string, 0)\n\t\t\ttree.Iterate(\"\", \"\", func(n *Node) bool {\n\t\t\t\tresult = append(result, n.Key())\n\t\t\t\treturn false\n\t\t\t})\n\n\t\t\tif !slicesEqual(tt.expected, result) {\n\t\t\t\tt.Errorf(\"want %v got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTraverse(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\t\"empty tree\",\n\t\t\t[]string{},\n\t\t\t[]string{},\n\t\t},\n\t\t{\n\t\t\t\"single node tree\",\n\t\t\t[]string{\"A\"},\n\t\t\t[]string{\"A\"},\n\t\t},\n\t\t{\n\t\t\t\"small tree\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"large tree\",\n\t\t\t[]string{\"H\", \"D\", \"L\", \"B\", \"F\", \"J\", \"N\", \"A\", \"C\", \"E\", \"G\", \"I\", \"K\", \"M\", \"O\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\", \"F\", \"G\", \"H\", \"I\", \"J\", \"K\", \"L\", \"M\", \"N\", \"O\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\tt.Run(\"iterate\", func(t *testing.T) {\n\t\t\t\tvar result []string\n\t\t\t\ttree.Iterate(\"\", \"\", func(n *Node) bool {\n\t\t\t\t\tresult = append(result, n.Key())\n\t\t\t\t\treturn false\n\t\t\t\t})\n\t\t\t\tif !slicesEqual(tt.expected, result) {\n\t\t\t\t\tt.Errorf(\"want %v got %v\", tt.expected, result)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tt.Run(\"ReverseIterate\", func(t *testing.T) {\n\t\t\t\tvar result []string\n\t\t\t\ttree.ReverseIterate(\"\", \"\", func(n *Node) bool {\n\t\t\t\t\tresult = append(result, n.Key())\n\t\t\t\t\treturn false\n\t\t\t\t})\n\t\t\t\texpected := make([]string, len(tt.expected))\n\t\t\t\tcopy(expected, tt.expected)\n\t\t\t\tfor i, j := 0, len(expected)-1; i \u003c j; i, j = i+1, j-1 {\n\t\t\t\t\texpected[i], expected[j] = expected[j], expected[i]\n\t\t\t\t}\n\t\t\t\tif !slicesEqual(expected, result) {\n\t\t\t\t\tt.Errorf(\"want %v got %v\", expected, result)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tt.Run(\"TraverseInRange\", func(t *testing.T) {\n\t\t\t\tvar result []string\n\t\t\t\tstart, end := \"C\", \"M\"\n\t\t\t\ttree.TraverseInRange(start, end, true, true, func(n *Node) bool {\n\t\t\t\t\tresult = append(result, n.Key())\n\t\t\t\t\treturn false\n\t\t\t\t})\n\t\t\t\texpected := make([]string, 0)\n\t\t\t\tfor _, key := range tt.expected {\n\t\t\t\t\tif key \u003e= start \u0026\u0026 key \u003c end {\n\t\t\t\t\t\texpected = append(expected, key)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif !slicesEqual(expected, result) {\n\t\t\t\t\tt.Errorf(\"want %v got %v\", expected, result)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tt.Run(\"early termination\", func(t *testing.T) {\n\t\t\t\tif len(tt.input) == 0 {\n\t\t\t\t\treturn // Skip for empty tree\n\t\t\t\t}\n\n\t\t\t\tvar result []string\n\t\t\t\tvar count int\n\t\t\t\ttree.Iterate(\"\", \"\", func(n *Node) bool {\n\t\t\t\t\tcount++\n\t\t\t\t\tresult = append(result, n.Key())\n\t\t\t\t\treturn true // Stop after first item\n\t\t\t\t})\n\n\t\t\t\tif count != 1 {\n\t\t\t\t\tt.Errorf(\"Expected callback to be called exactly once, got %d calls\", count)\n\t\t\t\t}\n\t\t\t\tif len(result) != 1 {\n\t\t\t\t\tt.Errorf(\"Expected exactly one result, got %d items\", len(result))\n\t\t\t\t}\n\t\t\t\tif len(result) \u003e 0 \u0026\u0026 result[0] != tt.expected[0] {\n\t\t\t\t\tt.Errorf(\"Expected first item to be %v, got %v\", tt.expected[0], result[0])\n\t\t\t\t}\n\t\t\t})\n\t\t})\n\t}\n}\n\nfunc TestRotateWhenHeightDiffers(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\t\"right rotation when left subtree is higher\",\n\t\t\t[]string{\"E\", \"C\", \"A\", \"B\", \"D\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"left rotation when right subtree is higher\",\n\t\t\t[]string{\"A\", \"C\", \"E\", \"D\", \"F\"},\n\t\t\t[]string{\"A\", \"C\", \"D\", \"E\", \"F\"},\n\t\t},\n\t\t{\n\t\t\t\"left-right rotation\",\n\t\t\t[]string{\"E\", \"A\", \"C\", \"B\", \"D\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"right-left rotation\",\n\t\t\t[]string{\"A\", \"E\", \"C\", \"B\", \"D\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\t// perform rotation or balance\n\t\t\ttree = tree.balance()\n\n\t\t\t// check tree structure\n\t\t\tvar result []string\n\t\t\ttree.Iterate(\"\", \"\", func(n *Node) bool {\n\t\t\t\tresult = append(result, n.Key())\n\t\t\t\treturn false\n\t\t\t})\n\n\t\t\tif !slicesEqual(tt.expected, result) {\n\t\t\t\tt.Errorf(\"want %v got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRotateAndBalance(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\t\"right rotation\",\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"left rotation\",\n\t\t\t[]string{\"E\", \"D\", \"C\", \"B\", \"A\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"left-right rotation\",\n\t\t\t[]string{\"C\", \"A\", \"E\", \"B\", \"D\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"right-left rotation\",\n\t\t\t[]string{\"C\", \"E\", \"A\", \"D\", \"B\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\ttree = tree.balance()\n\n\t\t\tvar result []string\n\t\t\ttree.Iterate(\"\", \"\", func(n *Node) bool {\n\t\t\t\tresult = append(result, n.Key())\n\t\t\t\treturn false\n\t\t\t})\n\n\t\t\tif !slicesEqual(tt.expected, result) {\n\t\t\t\tt.Errorf(\"want %v got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRemoveFromEmptyTree(t *testing.T) {\n\tvar tree *Node\n\tnewTree, _, val, removed := tree.Remove(\"NonExistent\")\n\tif newTree != nil {\n\t\tt.Errorf(\"Removing from an empty tree should still be nil tree.\")\n\t}\n\tif val != nil || removed {\n\t\tt.Errorf(\"Expected no value and removed=false when removing from empty tree.\")\n\t}\n}\n\nfunc TestBalanceAfterRemoval(t *testing.T) {\n\ttests := []struct {\n\t\tname            string\n\t\tinsertKeys      []string\n\t\tremoveKey       string\n\t\texpectedBalance int\n\t}{\n\t\t{\n\t\t\tname:            \"balance after removing right node\",\n\t\t\tinsertKeys:      []string{\"B\", \"A\", \"D\", \"C\", \"E\"},\n\t\t\tremoveKey:       \"E\",\n\t\t\texpectedBalance: 0,\n\t\t},\n\t\t{\n\t\t\tname:            \"balance after removing left node\",\n\t\t\tinsertKeys:      []string{\"D\", \"B\", \"E\", \"A\", \"C\"},\n\t\t\tremoveKey:       \"A\",\n\t\t\texpectedBalance: 0,\n\t\t},\n\t\t{\n\t\t\tname:            \"ensure no lean after removal\",\n\t\t\tinsertKeys:      []string{\"C\", \"B\", \"E\", \"A\", \"D\", \"F\"},\n\t\t\tremoveKey:       \"F\",\n\t\t\texpectedBalance: -1,\n\t\t},\n\t\t{\n\t\t\tname:            \"descending order insert, remove middle node\",\n\t\t\tinsertKeys:      []string{\"E\", \"D\", \"C\", \"B\", \"A\"},\n\t\t\tremoveKey:       \"C\",\n\t\t\texpectedBalance: 0,\n\t\t},\n\t\t{\n\t\t\tname:            \"ascending order insert, remove middle node\",\n\t\t\tinsertKeys:      []string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t\tremoveKey:       \"C\",\n\t\t\texpectedBalance: 0,\n\t\t},\n\t\t{\n\t\t\tname:            \"duplicate key insert, remove the duplicated key\",\n\t\t\tinsertKeys:      []string{\"C\", \"B\", \"C\", \"A\", \"D\"},\n\t\t\tremoveKey:       \"C\",\n\t\t\texpectedBalance: 1,\n\t\t},\n\t\t{\n\t\t\tname:            \"complex rotation case\",\n\t\t\tinsertKeys:      []string{\"H\", \"B\", \"A\", \"C\", \"E\", \"D\", \"F\", \"G\"},\n\t\t\tremoveKey:       \"B\",\n\t\t\texpectedBalance: 0,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.insertKeys {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\ttree, _, _, _ = tree.Remove(tt.removeKey)\n\n\t\t\tbalance := tree.calcBalance()\n\t\t\tif balance != tt.expectedBalance {\n\t\t\t\tt.Errorf(\"Expected balance factor %d, got %d\", tt.expectedBalance, balance)\n\t\t\t}\n\n\t\t\tif balance \u003c -1 || balance \u003e 1 {\n\t\t\t\tt.Errorf(\"Tree is unbalanced with factor %d\", balance)\n\t\t\t}\n\n\t\t\tif errMsg := checkSubtreeBalance(t, tree); errMsg != \"\" {\n\t\t\t\tt.Errorf(\"AVL property violation after removal: %s\", errMsg)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBSTProperty(t *testing.T) {\n\tvar tree *Node\n\tkeys := []string{\"D\", \"B\", \"F\", \"A\", \"C\", \"E\", \"G\"}\n\tfor _, key := range keys {\n\t\ttree, _ = tree.Set(key, nil)\n\t}\n\n\tvar result []string\n\tinorderTraversal(t, tree, \u0026result)\n\n\tfor i := 1; i \u003c len(result); i++ {\n\t\tif result[i] \u003c result[i-1] {\n\t\t\tt.Errorf(\"BST property violated: %s \u003c %s (index %d)\",\n\t\t\t\tresult[i], result[i-1], i)\n\t\t}\n\t}\n}\n\n// inorderTraversal performs an inorder traversal of the tree and returns the keys in a list.\nfunc inorderTraversal(t *testing.T, node *Node, result *[]string) {\n\tt.Helper()\n\n\tif node == nil {\n\t\treturn\n\t}\n\t// leaf\n\tif node.height == 0 {\n\t\t*result = append(*result, node.key)\n\t\treturn\n\t}\n\tinorderTraversal(t, node.leftNode, result)\n\tinorderTraversal(t, node.rightNode, result)\n}\n\n// checkSubtreeBalance checks if all nodes under the given node satisfy the AVL tree conditions.\n// The balance factor of all nodes must be ∈ [-1, +1]\nfunc checkSubtreeBalance(t *testing.T, node *Node) string {\n\tt.Helper()\n\n\tif node == nil {\n\t\treturn \"\"\n\t}\n\n\tif node.IsLeaf() {\n\t\t// leaf node must be height=0, size=1\n\t\tif node.height != 0 {\n\t\t\treturn ufmt.Sprintf(\"Leaf node %s has height %d, expected 0\", node.Key(), node.height)\n\t\t}\n\t\tif node.size != 1 {\n\t\t\treturn ufmt.Sprintf(\"Leaf node %s has size %d, expected 1\", node.Key(), node.size)\n\t\t}\n\t\treturn \"\"\n\t}\n\n\t// check balance factor for current node\n\tbalanceFactor := node.calcBalance()\n\tif balanceFactor \u003c -1 || balanceFactor \u003e 1 {\n\t\treturn ufmt.Sprintf(\"Node %s is unbalanced: balanceFactor=%d\", node.Key(), balanceFactor)\n\t}\n\n\t// check height / size relationship for children\n\tleft, right := node.getLeftNode(), node.getRightNode()\n\texpectedHeight := maxInt8(left.height, right.height) + 1\n\tif node.height != expectedHeight {\n\t\treturn ufmt.Sprintf(\"Node %s has incorrect height %d, expected %d\", node.Key(), node.height, expectedHeight)\n\t}\n\texpectedSize := left.Size() + right.Size()\n\tif node.size != expectedSize {\n\t\treturn ufmt.Sprintf(\"Node %s has incorrect size %d, expected %d\", node.Key(), node.size, expectedSize)\n\t}\n\n\t// recursively check the left/right subtree\n\tif errMsg := checkSubtreeBalance(t, left); errMsg != \"\" {\n\t\treturn errMsg\n\t}\n\tif errMsg := checkSubtreeBalance(t, right); errMsg != \"\" {\n\t\treturn errMsg\n\t}\n\n\treturn \"\"\n}\n\nfunc slicesEqual(w1, w2 []string) bool {\n\tif len(w1) != len(w2) {\n\t\treturn false\n\t}\n\tfor i := 0; i \u003c len(w1); i++ {\n\t\tif w1[i] != w2[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc reverseSlice(ss []string) {\n\tfor i := 0; i \u003c len(ss)/2; i++ {\n\t\tj := len(ss) - 1 - i\n\t\tss[i], ss[j] = ss[j], ss[i]\n\t}\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"},{"name":"tree_test.gno","body":"package avl\n\nimport \"testing\"\n\nfunc TestNewTree(t *testing.T) {\n\ttree := NewTree()\n\tif tree.node != nil {\n\t\tt.Error(\"Expected tree.node to be nil\")\n\t}\n}\n\nfunc TestTreeSize(t *testing.T) {\n\ttree := NewTree()\n\tif tree.Size() != 0 {\n\t\tt.Error(\"Expected empty tree size to be 0\")\n\t}\n\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\tif tree.Size() != 2 {\n\t\tt.Error(\"Expected tree size to be 2\")\n\t}\n}\n\nfunc TestTreeHas(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\n\tif !tree.Has(\"key1\") {\n\t\tt.Error(\"Expected tree to have key1\")\n\t}\n\n\tif tree.Has(\"key2\") {\n\t\tt.Error(\"Expected tree to not have key2\")\n\t}\n}\n\nfunc TestTreeGet(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\n\tvalue := tree.Get(\"key1\")\n\tif value != \"value1\" {\n\t\tt.Error(\"Expected Get to return value1\")\n\t}\n\n\tvalue = tree.Get(\"key2\")\n\tif value != nil {\n\t\tt.Error(\"Expected Get to return nil for non-existent key\")\n\t}\n}\n\nfunc TestTreeGetByIndex(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\n\tkey, value := tree.GetByIndex(0)\n\tif key != \"key1\" || value != \"value1\" {\n\t\tt.Error(\"Expected GetByIndex(0) to return key1 and value1\")\n\t}\n\n\tkey, value = tree.GetByIndex(1)\n\tif key != \"key2\" || value != \"value2\" {\n\t\tt.Error(\"Expected GetByIndex(1) to return key2 and value2\")\n\t}\n\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Error(\"Expected GetByIndex to panic for out-of-range index\")\n\t\t}\n\t}()\n\ttree.GetByIndex(2)\n}\n\nfunc TestTreeRemove(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\n\tvalue, removed := tree.Remove(\"key1\")\n\tif !removed || value != \"value1\" || tree.Size() != 0 {\n\t\tt.Error(\"Expected Remove to remove key-value pair\")\n\t}\n\n\t_, removed = tree.Remove(\"key2\")\n\tif removed {\n\t\tt.Error(\"Expected Remove to return false for non-existent key\")\n\t}\n}\n\nfunc TestTreeIterate(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\ttree.Set(\"key3\", \"value3\")\n\n\tvar keys []string\n\ttree.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\tkeys = append(keys, key)\n\t\treturn false\n\t})\n\n\texpectedKeys := []string{\"key1\", \"key2\", \"key3\"}\n\tif !slicesEqual(keys, expectedKeys) {\n\t\tt.Errorf(\"Expected keys %v, got %v\", expectedKeys, keys)\n\t}\n}\n\nfunc TestTreeReverseIterate(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\ttree.Set(\"key3\", \"value3\")\n\n\tvar keys []string\n\ttree.ReverseIterate(\"\", \"\", func(key string, value any) bool {\n\t\tkeys = append(keys, key)\n\t\treturn false\n\t})\n\n\texpectedKeys := []string{\"key3\", \"key2\", \"key1\"}\n\tif !slicesEqual(keys, expectedKeys) {\n\t\tt.Errorf(\"Expected keys %v, got %v\", expectedKeys, keys)\n\t}\n}\n\nfunc TestTreeIterateByOffset(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\ttree.Set(\"key3\", \"value3\")\n\n\tvar keys []string\n\ttree.IterateByOffset(1, 2, func(key string, value any) bool {\n\t\tkeys = append(keys, key)\n\t\treturn false\n\t})\n\n\texpectedKeys := []string{\"key2\", \"key3\"}\n\tif !slicesEqual(keys, expectedKeys) {\n\t\tt.Errorf(\"Expected keys %v, got %v\", expectedKeys, keys)\n\t}\n}\n\nfunc TestTreeReverseIterateByOffset(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\ttree.Set(\"key3\", \"value3\")\n\n\tvar keys []string\n\ttree.ReverseIterateByOffset(1, 2, func(key string, value any) bool {\n\t\tkeys = append(keys, key)\n\t\treturn false\n\t})\n\n\texpectedKeys := []string{\"key2\", \"key1\"}\n\tif !slicesEqual(keys, expectedKeys) {\n\t\tt.Errorf(\"Expected keys %v, got %v\", expectedKeys, keys)\n\t}\n}\n\nfunc TestTreeReverseIterateByOffsetVaried(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"a\", 1)\n\ttree.Set(\"b\", 2)\n\ttree.Set(\"c\", 3)\n\ttree.Set(\"d\", 4)\n\ttree.Set(\"e\", 5)\n\n\t// All keys in reverse: e d c b a\n\tcases := []struct {\n\t\toffset int\n\t\tlimit  int\n\t\twant   []string\n\t}{\n\t\t{0, 5, []string{\"e\", \"d\", \"c\", \"b\", \"a\"}},\n\t\t{0, 1, []string{\"e\"}},\n\t\t{0, 3, []string{\"e\", \"d\", \"c\"}},\n\t\t{1, 2, []string{\"d\", \"c\"}},\n\t\t{2, 2, []string{\"c\", \"b\"}},\n\t\t{3, 5, []string{\"b\", \"a\"}},\n\t\t{4, 1, []string{\"a\"}},\n\t\t{4, 10, []string{\"a\"}},\n\t\t{5, 1, nil},\n\t\t{0, 0, nil},\n\t\t{10, 1, nil},\n\t}\n\n\tfor _, tc := range cases {\n\t\tvar got []string\n\t\ttree.ReverseIterateByOffset(tc.offset, tc.limit, func(key string, value any) bool {\n\t\t\tgot = append(got, key)\n\t\t\treturn false\n\t\t})\n\t\tif !slicesEqual(got, tc.want) {\n\t\t\tt.Errorf(\"ReverseIterateByOffset(%d, %d): got %v, want %v\",\n\t\t\t\ttc.offset, tc.limit, got, tc.want)\n\t\t}\n\t}\n\n\t// Early termination: stop after 2 items from offset 1.\n\tvar got []string\n\ttree.ReverseIterateByOffset(1, 5, func(key string, value any) bool {\n\t\tgot = append(got, key)\n\t\treturn len(got) \u003e= 2\n\t})\n\twant := []string{\"d\", \"c\"}\n\tif !slicesEqual(got, want) {\n\t\tt.Errorf(\"ReverseIterateByOffset early stop: got %v, want %v\", got, want)\n\t}\n}\n"},{"name":"z_0_filetest.gno","body":"// PKGPATH: gno.land/r/test\npackage test\n\nimport (\n\t\"gno.land/p/nt/avl/v0\"\n)\n\nvar node *avl.Node\n\nfunc init() {\n\tnode = avl.NewNode(\"key0\", \"value0\")\n\t// node, _ = node.Set(\"key0\", \"value0\")\n}\n\nfunc main(cur realm) {\n\tvar updated bool\n\tnode, updated = node.Set(\"key1\", \"value1\")\n\t// println(node, updated)\n\tprintln(updated, node.Size())\n}\n\n// Output:\n// false 2\n"},{"name":"z_1_filetest.gno","body":"// PKGPATH: gno.land/r/test\npackage test\n\nimport (\n\t\"gno.land/p/nt/avl/v0\"\n)\n\nvar node *avl.Node\n\nfunc init() {\n\tnode = avl.NewNode(\"key0\", \"value0\")\n\tnode, _ = node.Set(\"key1\", \"value1\")\n}\n\nfunc main(cur realm) {\n\tvar updated bool\n\tnode, updated = node.Set(\"key2\", \"value2\")\n\t// println(node, updated)\n\tprintln(updated, node.Size())\n}\n\n// Output:\n// false 3\n"},{"name":"z_2_filetest.gno","body":"// PKGPATH: gno.land/r/test\npackage test\n\nimport (\n\t\"gno.land/p/nt/avl/v0\"\n)\n\nvar tree avl.Tree\n\nfunc init() {\n\ttree.Set(\"key0\", \"value0\")\n\ttree.Set(\"key1\", \"value1\")\n}\n\nfunc main(cur realm) {\n\tvar updated bool\n\tupdated = tree.Set(\"key2\", \"value2\")\n\tprintln(updated, tree.Size())\n}\n\n// Output:\n// false 3\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"qvkUiutstBGR0MY05XKWjPBqD+GbaBnCCqH1t2jyXzZKx/vmJAKkAzXPu7x9shl9gwNobiGXF1V7/TqRLmIGxA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"rotree","path":"gno.land/p/nt/avl/rotree/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/avl/rotree/v0\"\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"},{"name":"rotree_test.gno","body":"package rotree\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/avl/v0\"\n)\n\nfunc TestExample(t *testing.T) {\n\t// User represents our internal data structure\n\ttype User struct {\n\t\tID       string\n\t\tName     string\n\t\tBalance  int\n\t\tInternal string // sensitive internal data\n\t}\n\n\t// Create and populate the original tree with user pointers\n\ttree := avl.NewTree()\n\ttree.Set(\"alice\", \u0026User{\n\t\tID:       \"1\",\n\t\tName:     \"Alice\",\n\t\tBalance:  100,\n\t\tInternal: \"sensitive_data_1\",\n\t})\n\ttree.Set(\"bob\", \u0026User{\n\t\tID:       \"2\",\n\t\tName:     \"Bob\",\n\t\tBalance:  200,\n\t\tInternal: \"sensitive_data_2\",\n\t})\n\n\t// Define a makeEntrySafeFn that:\n\t// 1. Creates a defensive copy of the User struct\n\t// 2. Omits sensitive internal data\n\tmakeEntrySafeFn := func(v any) any {\n\t\toriginalUser := v.(*User)\n\t\treturn \u0026User{\n\t\t\tID:       originalUser.ID,\n\t\t\tName:     originalUser.Name,\n\t\t\tBalance:  originalUser.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\troTree := Wrap(tree, makeEntrySafeFn)\n\n\t// Test retrieving and verifying a user\n\tt.Run(\"Get User\", func(t *testing.T) {\n\t\t// Get user from read-only tree\n\t\tvalue := roTree.Get(\"alice\")\n\t\tif value == nil {\n\t\t\tt.Fatal(\"User 'alice' not found\")\n\t\t}\n\n\t\tuser := value.(*User)\n\n\t\t// Verify user data is correct\n\t\tif user.Name != \"Alice\" || user.Balance != 100 {\n\t\t\tt.Errorf(\"Unexpected user data: got name=%s balance=%d\", user.Name, user.Balance)\n\t\t}\n\n\t\t// Verify sensitive data is not exposed\n\t\tif user.Internal != \"\" {\n\t\t\tt.Error(\"Sensitive data should not be exposed\")\n\t\t}\n\n\t\t// Verify it's a different instance than the original\n\t\toriginalValue := tree.Get(\"alice\")\n\t\toriginalUser := originalValue.(*User)\n\t\tif user == originalUser {\n\t\t\tt.Error(\"Read-only tree should return a copy, not the original pointer\")\n\t\t}\n\t})\n\n\t// Test iterating over users\n\tt.Run(\"Iterate Users\", func(t *testing.T) {\n\t\tcount := 0\n\t\troTree.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\t\tuser := value.(*User)\n\t\t\t// Verify each user has empty Internal field\n\t\t\tif user.Internal != \"\" {\n\t\t\t\tt.Error(\"Sensitive data exposed during iteration\")\n\t\t\t}\n\t\t\tcount++\n\t\t\treturn false\n\t\t})\n\n\t\tif count != 2 {\n\t\t\tt.Errorf(\"Expected 2 users, got %d\", count)\n\t\t}\n\t})\n\n\t// Verify that modifications to the returned user don't affect the original\n\tt.Run(\"Modification Safety\", func(t *testing.T) {\n\t\tvalue := roTree.Get(\"alice\")\n\t\tuser := value.(*User)\n\n\t\t// Try to modify the returned user\n\t\tuser.Balance = 999\n\t\tuser.Internal = \"hacked\"\n\n\t\t// Verify original is unchanged\n\t\toriginalValue := tree.Get(\"alice\")\n\t\toriginalUser := originalValue.(*User)\n\t\tif originalUser.Balance != 100 || originalUser.Internal != \"sensitive_data_1\" {\n\t\t\tt.Error(\"Original user data was modified\")\n\t\t}\n\t})\n}\n\nfunc TestReadOnlyTree(t *testing.T) {\n\t// Example of a makeEntrySafeFn that appends \"_readonly\" to demonstrate transformation\n\tmakeEntrySafeFn := func(value any) any {\n\t\treturn value.(string) + \"_readonly\"\n\t}\n\n\ttree := avl.NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\ttree.Set(\"key3\", \"value3\")\n\n\troTree := Wrap(tree, makeEntrySafeFn)\n\n\ttests := []struct {\n\t\tname     string\n\t\tkey      string\n\t\texpected any\n\t}{\n\t\t{\"ExistingKey1\", \"key1\", \"value1_readonly\"},\n\t\t{\"ExistingKey2\", \"key2\", \"value2_readonly\"},\n\t\t{\"NonExistingKey\", \"key4\", nil},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvalue := roTree.Get(tt.key)\n\t\t\tif value != tt.expected {\n\t\t\t\tt.Errorf(\"For key %s, expected %v, got %v\", tt.key, tt.expected, value)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Add example tests showing different makeEntrySafeFn implementations\nfunc TestMakeEntrySafeFnVariants(t *testing.T) {\n\ttree := avl.NewTree()\n\ttree.Set(\"slice\", []int{1, 2, 3})\n\ttree.Set(\"map\", map[string]int{\"a\": 1})\n\n\ttests := []struct {\n\t\tname            string\n\t\tmakeEntrySafeFn func(any) any\n\t\tkey             string\n\t\tvalidate        func(t *testing.T, value any)\n\t}{\n\t\t{\n\t\t\tname: \"Defensive Copy Slice\",\n\t\t\tmakeEntrySafeFn: func(v any) any {\n\t\t\t\toriginal := v.([]int)\n\t\t\t\treturn append([]int{}, original...)\n\t\t\t},\n\t\t\tkey: \"slice\",\n\t\t\tvalidate: func(t *testing.T, value any) {\n\t\t\t\tslice := value.([]int)\n\t\t\t\t// Modify the returned slice\n\t\t\t\tslice[0] = 999\n\t\t\t\t// Verify original is unchanged\n\t\t\t\toriginalValue := tree.Get(\"slice\")\n\t\t\t\toriginal := originalValue.([]int)\n\t\t\t\tif original[0] != 1 {\n\t\t\t\t\tt.Error(\"Original slice was modified\")\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t// Add more test cases for different makeEntrySafeFn implementations\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\troTree := Wrap(tree, tt.makeEntrySafeFn)\n\t\t\tvalue := roTree.Get(tt.key)\n\t\t\tif value == nil {\n\t\t\t\tt.Fatal(\"Key not found\")\n\t\t\t}\n\t\t\ttt.validate(t, value)\n\t\t})\n\t}\n}\n\nfunc TestNilMakeEntrySafeFn(t *testing.T) {\n\t// Create a tree with some test data\n\ttree := avl.NewTree()\n\toriginalValue := []int{1, 2, 3}\n\ttree.Set(\"test\", originalValue)\n\n\t// Create a ReadOnlyTree with nil makeEntrySafeFn\n\troTree := Wrap(tree, nil)\n\n\t// Test that we get back the original value\n\tvalue := roTree.Get(\"test\")\n\tif value == nil {\n\t\tt.Fatal(\"Key not found\")\n\t}\n\n\t// Verify it's the exact same slice (not a copy)\n\tretrievedSlice := value.([]int)\n\tif \u0026retrievedSlice[0] != \u0026originalValue[0] {\n\t\tt.Error(\"Expected to get back the original slice reference\")\n\t}\n\n\t// Test through iteration as well\n\troTree.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\tretrievedSlice := value.([]int)\n\t\tif \u0026retrievedSlice[0] != \u0026originalValue[0] {\n\t\t\tt.Error(\"Expected to get back the original slice reference in iteration\")\n\t\t}\n\t\treturn false\n\t})\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"uBj/Fjc4G7zZKhY1Bi4rQ1Y7od9TSfc4gYmI190HOs8bzKz0xvEJxawyeIuLrc0zYxPTOyz3dBq1QInNYYsuKA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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":"cford32_test.gno","body":"package cford32\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nfunc TestCompactRoundtrip(t *testing.T) {\n\tbuf := make([]byte, 13)\n\tprev := make([]byte, 13)\n\tfor i := uint64(0); i \u003c (1 \u003c\u003c 10); i++ {\n\t\tres := AppendCompact(i, buf[:0])\n\t\tback, err := Uint64(res)\n\t\ttestEqual(t, \"Uint64(%q) = (%d, %v), want %v\", string(res), back, err, nil)\n\t\ttestEqual(t, \"Uint64(%q) = %d, want %v\", string(res), back, i)\n\n\t\ttestEqual(t, \"bytes.Compare(prev, res) = %d, want %d\", bytes.Compare(prev, res), -1)\n\t\tprev, buf = res, prev\n\t}\n\tfor i := uint64(1\u003c\u003c34 - 1024); i \u003c (1\u003c\u003c34 + 1024); i++ {\n\t\tres := AppendCompact(i, buf[:0])\n\t\tback, err := Uint64(res)\n\t\t// println(string(res))\n\t\ttestEqual(t, \"Uint64(%q) = (%d, %v), want %v\", string(res), back, err, nil)\n\t\ttestEqual(t, \"Uint64(%q) = %d, want %v\", string(res), back, i)\n\n\t\ttestEqual(t, \"bytes.Compare(prev, res) = %d, want %d\", bytes.Compare(prev, res), -1)\n\t\tprev, buf = res, prev\n\t}\n\tfor i := uint64(1\u003c\u003c64 - 5000); i != 0; i++ {\n\t\tres := AppendCompact(i, buf[:0])\n\t\tback, err := Uint64(res)\n\t\ttestEqual(t, \"Uint64(%q) = (%d, %v), want %v\", string(res), back, err, nil)\n\t\ttestEqual(t, \"Uint64(%q) = %d, want %v\", string(res), back, i)\n\n\t\ttestEqual(t, \"bytes.Compare(prev, res) = %d, want %d\", bytes.Compare(prev, res), -1)\n\t\tprev, buf = res, prev\n\t}\n}\n\nfunc BenchmarkCompact(b *testing.B) {\n\tbuf := make([]byte, 13)\n\tfor i := 0; i \u003c b.N; i++ {\n\t\t_ = AppendCompact(uint64(i), buf[:0])\n\t}\n}\n\nfunc TestUint64(t *testing.T) {\n\ttt := []struct {\n\t\tval    string\n\t\toutput uint64\n\t\terr    string\n\t}{\n\t\t{\"0000001\", 1, \"\"},\n\t\t{\"OoOoOoL\", 1, \"\"},\n\t\t{\"OoUoOoL\", 0, CorruptInputError(2).Error()},\n\t\t{\"!123123\", 0, CorruptInputError(0).Error()},\n\t\t{\"Loooooo\", 1073741824, \"\"},\n\t\t{\"goooooo\", 0, CorruptInputError(0).Error()},\n\t\t{\"goooooooooooo\", 0, \"\"},\n\t\t{\"goooooooooolo\", 32, \"\"},\n\t\t{\"fzzzzzz\", (1 \u003c\u003c 34) - 1, \"\"},\n\t\t{\"g00000fzzzzzz\", (1 \u003c\u003c 34) - 1, \"\"},\n\t\t{\"g000000\", 0, CorruptInputError(0).Error()},\n\t\t{\"g00000g000000\", (1 \u003c\u003c 34), \"\"},\n\t}\n\n\tfor _, tc := range tt {\n\t\tt.Run(tc.val, func(t *testing.T) {\n\t\t\tres, err := Uint64([]byte(tc.val))\n\t\t\tif tc.err != \"\" {\n\t\t\t\t_ = uassert.Error(t, err) \u0026\u0026\n\t\t\t\t\tuassert.Equal(t, tc.err, err.Error())\n\t\t\t} else {\n\t\t\t\t_ = uassert.NoError(t, err) \u0026\u0026\n\t\t\t\t\tuassert.Equal(t, tc.output, res)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRandomCompactRoundtrip(t *testing.T) {\n\tfor i := 0; i \u003c 1\u003c\u003c12; i++ {\n\t\tvalue := rand.Uint64()\n\t\tencoded := PutCompact(value)\n\t\tdecoded, err := Uint64(encoded)\n\t\tuassert.NoError(t, err)\n\t\tuassert.Equal(t, value, decoded)\n\t}\n}\n\ntype testpair struct {\n\tdecoded, encoded string\n}\n\nvar pairs = []testpair{\n\t{\"\", \"\"},\n\t{\"f\", \"CR\"},\n\t{\"fo\", \"CSQG\"},\n\t{\"foo\", \"CSQPY\"},\n\t{\"foob\", \"CSQPYRG\"},\n\t{\"fooba\", \"CSQPYRK1\"},\n\t{\"foobar\", \"CSQPYRK1E8\"},\n\n\t{\"sure.\", \"EDTQ4S9E\"},\n\t{\"sure\", \"EDTQ4S8\"},\n\t{\"sur\", \"EDTQ4\"},\n\t{\"su\", \"EDTG\"},\n\t{\"leasure.\", \"DHJP2WVNE9JJW\"},\n\t{\"easure.\", \"CNGQ6XBJCMQ0\"},\n\t{\"asure.\", \"C5SQAWK55R\"},\n}\n\nvar bigtest = testpair{\n\t\"Twas brillig, and the slithy toves\",\n\t\"AHVP2WS0C9S6JV3CD5KJR831DSJ20X38CMG76V39EHM7J83MDXV6AWR\",\n}\n\nfunc testEqual(t *testing.T, msg string, args ...any) bool {\n\tt.Helper()\n\tif args[len(args)-2] != args[len(args)-1] {\n\t\tt.Errorf(msg, args...)\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc TestEncode(t *testing.T) {\n\tfor _, p := range pairs {\n\t\tgot := EncodeToString([]byte(p.decoded))\n\t\ttestEqual(t, \"Encode(%q) = %q, want %q\", p.decoded, got, p.encoded)\n\t\tdst := AppendEncode([]byte(\"lead\"), []byte(p.decoded))\n\t\ttestEqual(t, `AppendEncode(\"lead\", %q) = %q, want %q`, p.decoded, string(dst), \"lead\"+p.encoded)\n\t}\n}\n\nfunc TestEncoder(t *testing.T) {\n\tfor _, p := range pairs {\n\t\tbb := \u0026strings.Builder{}\n\t\tencoder := NewEncoder(bb)\n\t\tencoder.Write([]byte(p.decoded))\n\t\tencoder.Close()\n\t\ttestEqual(t, \"Encode(%q) = %q, want %q\", p.decoded, bb.String(), p.encoded)\n\t}\n}\n\nfunc TestEncoderBuffering(t *testing.T) {\n\tinput := []byte(bigtest.decoded)\n\tfor bs := 1; bs \u003c= 12; bs++ {\n\t\tbb := \u0026strings.Builder{}\n\t\tencoder := NewEncoder(bb)\n\t\tfor pos := 0; pos \u003c len(input); pos += bs {\n\t\t\tend := pos + bs\n\t\t\tif end \u003e len(input) {\n\t\t\t\tend = len(input)\n\t\t\t}\n\t\t\tn, err := encoder.Write(input[pos:end])\n\t\t\ttestEqual(t, \"Write(%q) gave error %v, want %v\", input[pos:end], err, error(nil))\n\t\t\ttestEqual(t, \"Write(%q) gave length %v, want %v\", input[pos:end], n, end-pos)\n\t\t}\n\t\terr := encoder.Close()\n\t\ttestEqual(t, \"Close gave error %v, want %v\", err, error(nil))\n\t\ttestEqual(t, \"Encoding/%d of %q = %q, want %q\", bs, bigtest.decoded, bb.String(), bigtest.encoded)\n\t}\n}\n\nfunc TestDecode(t *testing.T) {\n\tfor _, p := range pairs {\n\t\tdbuf := make([]byte, DecodedLen(len(p.encoded)))\n\t\tcount, err := decode(dbuf, []byte(p.encoded))\n\t\ttestEqual(t, \"Decode(%q) = error %v, want %v\", p.encoded, err, error(nil))\n\t\ttestEqual(t, \"Decode(%q) = length %v, want %v\", p.encoded, count, len(p.decoded))\n\t\ttestEqual(t, \"Decode(%q) = %q, want %q\", p.encoded, string(dbuf[0:count]), p.decoded)\n\n\t\tdbuf, err = DecodeString(p.encoded)\n\t\ttestEqual(t, \"DecodeString(%q) = error %v, want %v\", p.encoded, err, error(nil))\n\t\ttestEqual(t, \"DecodeString(%q) = %q, want %q\", p.encoded, string(dbuf), p.decoded)\n\n\t\t// XXX: https://github.com/gnolang/gno/issues/1570\n\t\tdst, err := AppendDecode(append([]byte(nil), []byte(\"lead\")...), []byte(p.encoded))\n\t\ttestEqual(t, \"AppendDecode(%q) = error %v, want %v\", p.encoded, err, error(nil))\n\t\ttestEqual(t, `AppendDecode(\"lead\", %q) = %q, want %q`, p.encoded, string(dst), \"lead\"+p.decoded)\n\n\t\tdst2, err := AppendDecode(dst[:0:len(p.decoded)], []byte(p.encoded))\n\t\ttestEqual(t, \"AppendDecode(%q) = error %v, want %v\", p.encoded, err, error(nil))\n\t\ttestEqual(t, `AppendDecode(\"\", %q) = %q, want %q`, p.encoded, string(dst2), p.decoded)\n\t\t// XXX: https://github.com/gnolang/gno/issues/1569\n\t\t// old used \u0026dst2[0] != \u0026dst[0] as a check.\n\t\tif len(dst) \u003e 0 \u0026\u0026 len(dst2) \u003e 0 \u0026\u0026 cap(dst2) != len(p.decoded) {\n\t\t\tt.Errorf(\"unexpected capacity growth: got %d, want %d\", cap(dst2), len(p.decoded))\n\t\t}\n\t}\n}\n\n// A minimal variation on strings.Reader.\n// Here, we return a io.EOF immediately on Read if the read has reached the end\n// of the reader. It's used to simplify TestDecoder.\ntype stringReader struct {\n\ts string\n\ti int64\n}\n\nfunc (r *stringReader) Read(b []byte) (n int, err error) {\n\tif r.i \u003e= int64(len(r.s)) {\n\t\treturn 0, io.EOF\n\t}\n\tn = copy(b, r.s[r.i:])\n\tr.i += int64(n)\n\tif r.i \u003e= int64(len(r.s)) {\n\t\treturn n, io.EOF\n\t}\n\treturn\n}\n\nfunc TestDecoder(t *testing.T) {\n\tfor _, p := range pairs {\n\t\tdecoder := NewDecoder(\u0026stringReader{p.encoded, 0})\n\t\tdbuf := make([]byte, DecodedLen(len(p.encoded)))\n\t\tcount, err := decoder.Read(dbuf)\n\t\tif err != nil \u0026\u0026 err != io.EOF {\n\t\t\tt.Fatal(\"Read failed\", err)\n\t\t}\n\t\ttestEqual(t, \"Read from %q = length %v, want %v\", p.encoded, count, len(p.decoded))\n\t\ttestEqual(t, \"Decoding of %q = %q, want %q\", p.encoded, string(dbuf[0:count]), p.decoded)\n\t\tif err != io.EOF {\n\t\t\t_, err = decoder.Read(dbuf)\n\t\t}\n\t\ttestEqual(t, \"Read from %q = %v, want %v\", p.encoded, err, io.EOF)\n\t}\n}\n\ntype badReader struct {\n\tdata   []byte\n\terrs   []error\n\tcalled int\n\tlimit  int\n}\n\n// Populates p with data, returns a count of the bytes written and an\n// error.  The error returned is taken from badReader.errs, with each\n// invocation of Read returning the next error in this slice, or io.EOF,\n// if all errors from the slice have already been returned.  The\n// number of bytes returned is determined by the size of the input buffer\n// the test passes to decoder.Read and will be a multiple of 8, unless\n// badReader.limit is non zero.\nfunc (b *badReader) Read(p []byte) (int, error) {\n\tlim := len(p)\n\tif b.limit != 0 \u0026\u0026 b.limit \u003c lim {\n\t\tlim = b.limit\n\t}\n\tif len(b.data) \u003c lim {\n\t\tlim = len(b.data)\n\t}\n\tfor i := range p[:lim] {\n\t\tp[i] = b.data[i]\n\t}\n\tb.data = b.data[lim:]\n\terr := io.EOF\n\tif b.called \u003c len(b.errs) {\n\t\terr = b.errs[b.called]\n\t}\n\tb.called++\n\treturn lim, err\n}\n\n// TestIssue20044 tests that decoder.Read behaves correctly when the caller\n// supplied reader returns an error.\nfunc TestIssue20044(t *testing.T) {\n\tbadErr := errors.New(\"bad reader error\")\n\ttestCases := []struct {\n\t\tr       badReader\n\t\tres     string\n\t\terr     error\n\t\tdbuflen int\n\t}{\n\t\t// Check valid input data accompanied by an error is processed and the error is propagated.\n\t\t{\n\t\t\tr:   badReader{data: []byte(\"d1jprv3fexqq4v34\"), errs: []error{badErr}},\n\t\t\tres: \"helloworld\", err: badErr,\n\t\t},\n\t\t// Check a read error accompanied by input data consisting of newlines only is propagated.\n\t\t{\n\t\t\tr:   badReader{data: []byte(\"\\n\\n\\n\\n\\n\\n\\n\\n\"), errs: []error{badErr, nil}},\n\t\t\tres: \"\", err: badErr,\n\t\t},\n\t\t// Reader will be called twice.  The first time it will return 8 newline characters.  The\n\t\t// second time valid base32 encoded data and an error.  The data should be decoded\n\t\t// correctly and the error should be propagated.\n\t\t{\n\t\t\tr:   badReader{data: []byte(\"\\n\\n\\n\\n\\n\\n\\n\\nd1jprv3fexqq4v34\"), errs: []error{nil, badErr}},\n\t\t\tres: \"helloworld\", err: badErr, dbuflen: 8,\n\t\t},\n\t\t// Reader returns invalid input data (too short) and an error.  Verify the reader\n\t\t// error is returned.\n\t\t{\n\t\t\tr:   badReader{data: []byte(\"c\"), errs: []error{badErr}},\n\t\t\tres: \"\", err: badErr,\n\t\t},\n\t\t// Reader returns invalid input data (too short) but no error.  Verify io.ErrUnexpectedEOF\n\t\t// is returned.\n\t\t// NOTE(thehowl): I don't think this should applyto us?\n\t\t/* {\n\t\t\tr:   badReader{data: []byte(\"c\"), errs: []error{nil}},\n\t\t\tres: \"\", err: io.ErrUnexpectedEOF,\n\t\t},*/\n\t\t// Reader returns invalid input data and an error.  Verify the reader and not the\n\t\t// decoder error is returned.\n\t\t{\n\t\t\tr:   badReader{data: []byte(\"cu\"), errs: []error{badErr}},\n\t\t\tres: \"\", err: badErr,\n\t\t},\n\t\t// Reader returns valid data and io.EOF.  Check data is decoded and io.EOF is propagated.\n\t\t{\n\t\t\tr:   badReader{data: []byte(\"csqpyrk1\"), errs: []error{io.EOF}},\n\t\t\tres: \"fooba\", err: io.EOF,\n\t\t},\n\t\t// Check errors are properly reported when decoder.Read is called multiple times.\n\t\t// decoder.Read will be called 8 times, badReader.Read will be called twice, returning\n\t\t// valid data both times but an error on the second call.\n\t\t{\n\t\t\tr:   badReader{data: []byte(\"dhjp2wvne9jjwc9g\"), errs: []error{nil, badErr}},\n\t\t\tres: \"leasure.10\", err: badErr, dbuflen: 1,\n\t\t},\n\t\t// Check io.EOF is properly reported when decoder.Read is called multiple times.\n\t\t// decoder.Read will be called 8 times, badReader.Read will be called twice, returning\n\t\t// valid data both times but io.EOF on the second call.\n\t\t{\n\t\t\tr:   badReader{data: []byte(\"dhjp2wvne9jjw\"), errs: []error{nil, io.EOF}},\n\t\t\tres: \"leasure.\", err: io.EOF, dbuflen: 1,\n\t\t},\n\t\t// The following two test cases check that errors are propagated correctly when more than\n\t\t// 8 bytes are read at a time.\n\t\t{\n\t\t\tr:   badReader{data: []byte(\"dhjp2wvne9jjw\"), errs: []error{io.EOF}},\n\t\t\tres: \"leasure.\", err: io.EOF, dbuflen: 11,\n\t\t},\n\t\t{\n\t\t\tr:   badReader{data: []byte(\"dhjp2wvne9jjwc9g\"), errs: []error{badErr}},\n\t\t\tres: \"leasure.10\", err: badErr, dbuflen: 11,\n\t\t},\n\t\t// Check that errors are correctly propagated when the reader returns valid bytes in\n\t\t// groups that are not divisible by 8.  The first read will return 11 bytes and no\n\t\t// error.  The second will return 7 and an error.  The data should be decoded correctly\n\t\t// and the error should be propagated.\n\t\t// NOTE(thehowl): again, this is on the assumption that this is padded, and it's not.\n\t\t/* {\n\t\t\tr:   badReader{data: []byte(\"dhjp2wvne9jjw\"), errs: []error{nil, badErr}, limit: 11},\n\t\t\tres: \"leasure.\", err: badErr,\n\t\t}, */\n\t}\n\n\tfor idx, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"%d-%s\", idx, string(tc.res)), func(t *testing.T) {\n\t\t\tinput := tc.r.data\n\t\t\tdecoder := NewDecoder(\u0026tc.r)\n\t\t\tvar dbuflen int\n\t\t\tif tc.dbuflen \u003e 0 {\n\t\t\t\tdbuflen = tc.dbuflen\n\t\t\t} else {\n\t\t\t\tdbuflen = DecodedLen(len(input))\n\t\t\t}\n\t\t\tdbuf := make([]byte, dbuflen)\n\t\t\tvar err error\n\t\t\tvar res []byte\n\t\t\tfor err == nil {\n\t\t\t\tvar n int\n\t\t\t\tn, err = decoder.Read(dbuf)\n\t\t\t\tif n \u003e 0 {\n\t\t\t\t\tres = append(res, dbuf[:n]...)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttestEqual(t, \"Decoding of %q = %q, want %q\", string(input), string(res), tc.res)\n\t\t\ttestEqual(t, \"Decoding of %q err = %v, expected %v\", string(input), err, tc.err)\n\t\t})\n\t}\n}\n\n// TestDecoderError verifies decode errors are propagated when there are no read\n// errors.\nfunc TestDecoderError(t *testing.T) {\n\tfor _, readErr := range []error{io.EOF, nil} {\n\t\tinput := \"ucsqpyrk1u\"\n\t\tdbuf := make([]byte, DecodedLen(len(input)))\n\t\tbr := badReader{data: []byte(input), errs: []error{readErr}}\n\t\tdecoder := NewDecoder(\u0026br)\n\t\tn, err := decoder.Read(dbuf)\n\t\ttestEqual(t, \"Read after EOF, n = %d, expected %d\", n, 0)\n\t\tif _, ok := err.(CorruptInputError); !ok {\n\t\t\tt.Errorf(\"Corrupt input error expected.  Found %T\", err)\n\t\t}\n\t}\n}\n\n// TestReaderEOF ensures decoder.Read behaves correctly when input data is\n// exhausted.\nfunc TestReaderEOF(t *testing.T) {\n\tfor _, readErr := range []error{io.EOF, nil} {\n\t\tinput := \"MZXW6YTB\"\n\t\tbr := badReader{data: []byte(input), errs: []error{nil, readErr}}\n\t\tdecoder := NewDecoder(\u0026br)\n\t\tdbuf := make([]byte, DecodedLen(len(input)))\n\t\tn, err := decoder.Read(dbuf)\n\t\ttestEqual(t, \"Decoding of %q err = %v, expected %v\", input, err, error(nil))\n\t\tn, err = decoder.Read(dbuf)\n\t\ttestEqual(t, \"Read after EOF, n = %d, expected %d\", n, 0)\n\t\ttestEqual(t, \"Read after EOF, err = %v, expected %v\", err, io.EOF)\n\t\tn, err = decoder.Read(dbuf)\n\t\ttestEqual(t, \"Read after EOF, n = %d, expected %d\", n, 0)\n\t\ttestEqual(t, \"Read after EOF, err = %v, expected %v\", err, io.EOF)\n\t}\n}\n\nfunc TestDecoderBuffering(t *testing.T) {\n\tfor bs := 1; bs \u003c= 12; bs++ {\n\t\tdecoder := NewDecoder(strings.NewReader(bigtest.encoded))\n\t\tbuf := make([]byte, len(bigtest.decoded)+12)\n\t\tvar total int\n\t\tvar n int\n\t\tvar err error\n\t\tfor total = 0; total \u003c len(bigtest.decoded) \u0026\u0026 err == nil; {\n\t\t\tn, err = decoder.Read(buf[total : total+bs])\n\t\t\ttotal += n\n\t\t}\n\t\tif err != nil \u0026\u0026 err != io.EOF {\n\t\t\tt.Errorf(\"Read from %q at pos %d = %d, unexpected error %v\", bigtest.encoded, total, n, err)\n\t\t}\n\t\ttestEqual(t, \"Decoding/%d of %q = %q, want %q\", bs, bigtest.encoded, string(buf[0:total]), bigtest.decoded)\n\t}\n}\n\nfunc TestDecodeCorrupt(t *testing.T) {\n\ttestCases := []struct {\n\t\tinput  string\n\t\toffset int // -1 means no corruption.\n\t}{\n\t\t{\"\", -1},\n\t\t{\"iIoOlL\", -1},\n\t\t{\"!!!!\", 0},\n\t\t{\"uxp10\", 0},\n\t\t{\"x===\", 1},\n\t\t{\"AA=A====\", 2},\n\t\t{\"AAA=AAAA\", 3},\n\t\t// Much fewer cases compared to Go as there are much fewer cases where input\n\t\t// can be \"corrupted\".\n\t}\n\tfor _, tc := range testCases {\n\t\tdbuf := make([]byte, DecodedLen(len(tc.input)))\n\t\t_, err := Decode(dbuf, []byte(tc.input))\n\t\tif tc.offset == -1 {\n\t\t\tif err != nil {\n\t\t\t\tt.Error(\"Decoder wrongly detected corruption in\", tc.input)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tswitch err := err.(type) {\n\t\tcase CorruptInputError:\n\t\t\ttestEqual(t, \"Corruption in %q at offset %v, want %v\", tc.input, int(err), tc.offset)\n\t\tdefault:\n\t\t\tt.Error(\"Decoder failed to detect corruption in\", tc)\n\t\t}\n\t}\n}\n\nfunc TestBig(t *testing.T) {\n\tn := 3*1000 + 1\n\traw := make([]byte, n)\n\tconst alpha = \"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n\tfor i := 0; i \u003c n; i++ {\n\t\traw[i] = alpha[i%len(alpha)]\n\t}\n\tencoded := new(bytes.Buffer)\n\tw := NewEncoder(encoded)\n\tnn, err := w.Write(raw)\n\tif nn != n || err != nil {\n\t\tt.Fatalf(\"Encoder.Write(raw) = %d, %v want %d, nil\", nn, err, n)\n\t}\n\terr = w.Close()\n\tif err != nil {\n\t\tt.Fatalf(\"Encoder.Close() = %v want nil\", err)\n\t}\n\tdecoded, err := io.ReadAll(NewDecoder(encoded))\n\tif err != nil {\n\t\tt.Fatalf(\"io.ReadAll(NewDecoder(...)): %v\", err)\n\t}\n\n\tif !bytes.Equal(raw, decoded) {\n\t\tvar i int\n\t\tfor i = 0; i \u003c len(decoded) \u0026\u0026 i \u003c len(raw); i++ {\n\t\t\tif decoded[i] != raw[i] {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tt.Errorf(\"Decode(Encode(%d-byte string)) failed at offset %d\", n, i)\n\t}\n}\n\nfunc testStringEncoding(t *testing.T, expected string, examples []string) {\n\tfor _, e := range examples {\n\t\tbuf, err := DecodeString(e)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Decode(%q) failed: %v\", e, err)\n\t\t\tcontinue\n\t\t}\n\t\tif s := string(buf); s != expected {\n\t\t\tt.Errorf(\"Decode(%q) = %q, want %q\", e, s, expected)\n\t\t}\n\t}\n}\n\nfunc TestNewLineCharacters(t *testing.T) {\n\t// Each of these should decode to the string \"sure\", without errors.\n\texamples := []string{\n\t\t\"EDTQ4S8\",\n\t\t\"EDTQ4S8\\r\",\n\t\t\"EDTQ4S8\\n\",\n\t\t\"EDTQ4S8\\r\\n\",\n\t\t\"EDTQ4S\\r\\n8\",\n\t\t\"EDT\\rQ4S\\n8\",\n\t\t\"edt\\nq4s\\r8\",\n\t\t\"edt\\nq4s8\",\n\t\t\"EDTQ4S\\n8\",\n\t}\n\ttestStringEncoding(t, \"sure\", examples)\n}\n\nfunc BenchmarkEncode(b *testing.B) {\n\tdata := make([]byte, 8192)\n\tbuf := make([]byte, EncodedLen(len(data)))\n\tb.SetBytes(int64(len(data)))\n\tfor i := 0; i \u003c b.N; i++ {\n\t\tEncode(buf, data)\n\t}\n}\n\nfunc BenchmarkEncodeToString(b *testing.B) {\n\tdata := make([]byte, 8192)\n\tb.SetBytes(int64(len(data)))\n\tfor i := 0; i \u003c b.N; i++ {\n\t\tEncodeToString(data)\n\t}\n}\n\nfunc BenchmarkDecode(b *testing.B) {\n\tdata := make([]byte, EncodedLen(8192))\n\tEncode(data, make([]byte, 8192))\n\tbuf := make([]byte, 8192)\n\tb.SetBytes(int64(len(data)))\n\tfor i := 0; i \u003c b.N; i++ {\n\t\tDecode(buf, data)\n\t}\n}\n\nfunc BenchmarkDecodeString(b *testing.B) {\n\tdata := EncodeToString(make([]byte, 8192))\n\tb.SetBytes(int64(len(data)))\n\tfor i := 0; i \u003c b.N; i++ {\n\t\tDecodeString(data)\n\t}\n}\n\n/* TODO: rewrite without using goroutines\nfunc TestBufferedDecodingSameError(t *testing.T) {\n\ttestcases := []struct {\n\t\tprefix            string\n\t\tchunkCombinations [][]string\n\t\texpected          error\n\t}{\n\t\t// Normal case, this is valid input\n\t\t{\"helloworld\", [][]string{\n\t\t\t{\"D1JP\", \"RV3F\", \"EXQQ\", \"4V34\"},\n\t\t\t{\"D1JPRV3FEXQQ4V34\"},\n\t\t\t{\"D1J\", \"PRV\", \"3FE\", \"XQQ\", \"4V3\", \"4\"},\n\t\t\t{\"D1JPRV3FEXQQ4V\", \"34\"},\n\t\t}, nil},\n\n\t\t// Normal case, this is valid input\n\t\t{\"fooba\", [][]string{\n\t\t\t{\"CSQPYRK1\"},\n\t\t\t{\"CSQPYRK\", \"1\"},\n\t\t\t{\"CSQPYR\", \"K1\"},\n\t\t\t{\"CSQPY\", \"RK1\"},\n\t\t\t{\"CSQPY\", \"RK\", \"1\"},\n\t\t\t{\"CSQPY\", \"RK1\"},\n\t\t\t{\"CSQP\", \"YR\", \"K1\"},\n\t\t}, nil},\n\n\t\t// NOTE: many test cases have been removed as we don't return ErrUnexpectedEOF.\n\t}\n\n\tfor _, testcase := range testcases {\n\t\tfor _, chunks := range testcase.chunkCombinations {\n\t\t\tpr, pw := io.Pipe()\n\n\t\t\t// Write the encoded chunks into the pipe\n\t\t\tgo func() {\n\t\t\t\tfor _, chunk := range chunks {\n\t\t\t\t\tpw.Write([]byte(chunk))\n\t\t\t\t}\n\t\t\t\tpw.Close()\n\t\t\t}()\n\n\t\t\tdecoder := NewDecoder(pr)\n\t\t\tback, err := io.ReadAll(decoder)\n\n\t\t\tif err != testcase.expected {\n\t\t\t\tt.Errorf(\"Expected %v, got %v; case %s %+v\", testcase.expected, err, testcase.prefix, chunks)\n\t\t\t}\n\t\t\tif testcase.expected == nil {\n\t\t\t\ttestEqual(t, \"Decode from NewDecoder(chunkReader(%v)) = %q, want %q\", chunks, string(back), testcase.prefix)\n\t\t\t}\n\t\t}\n\t}\n}\n*/\n\nfunc TestEncodedLen(t *testing.T) {\n\ttype test struct {\n\t\tn    int\n\t\twant int64\n\t}\n\ttests := []test{\n\t\t{0, 0},\n\t\t{1, 2},\n\t\t{2, 4},\n\t\t{3, 5},\n\t\t{4, 7},\n\t\t{5, 8},\n\t\t{6, 10},\n\t\t{7, 12},\n\t\t{10, 16},\n\t\t{11, 18},\n\t}\n\t// check overflow\n\ttests = append(tests, test{(math.MaxInt-4)/8 + 1, 1844674407370955162})\n\ttests = append(tests, test{math.MaxInt/8*5 + 4, math.MaxInt})\n\tfor _, tt := range tests {\n\t\tif got := EncodedLen(tt.n); int64(got) != tt.want {\n\t\t\tt.Errorf(\"EncodedLen(%d): got %d, want %d\", tt.n, got, tt.want)\n\t\t}\n\t}\n}\n\nfunc TestDecodedLen(t *testing.T) {\n\ttype test struct {\n\t\tn    int\n\t\twant int64\n\t}\n\ttests := []test{\n\t\t{0, 0},\n\t\t{2, 1},\n\t\t{4, 2},\n\t\t{5, 3},\n\t\t{7, 4},\n\t\t{8, 5},\n\t\t{10, 6},\n\t\t{12, 7},\n\t\t{16, 10},\n\t\t{18, 11},\n\t}\n\t// check overflow\n\ttests = append(tests, test{math.MaxInt/5 + 1, 1152921504606846976})\n\ttests = append(tests, test{math.MaxInt, 5764607523034234879})\n\tfor _, tt := range tests {\n\t\tif got := DecodedLen(tt.n); int64(got) != tt.want {\n\t\t\tt.Errorf(\"DecodedLen(%d): got %d, want %d\", tt.n, got, tt.want)\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 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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"jP83mS5weqmNMSiXH2Htrnh7FD2G/HI0JCj1CWE08E1XafllthSS/afyYjKLgDtgaLMcJBnRg2pkeUe1dRBmNA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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"},{"name":"seqid_test.gno","body":"package seqid\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestID(t *testing.T) {\n\tvar i ID\n\n\tfor j := 0; j \u003c 100; j++ {\n\t\ti.Next()\n\t}\n\tif i != 100 {\n\t\tt.Fatalf(\"invalid: wanted %d got %d\", 100, i)\n\t}\n}\n\nfunc TestID_Overflow(t *testing.T) {\n\ti := ID(maxID)\n\n\tdefer func() {\n\t\terr := recover()\n\t\tif !strings.Contains(fmt.Sprint(err), \"next ID overflows\") {\n\t\t\tt.Errorf(\"did not overflow\")\n\t\t}\n\t}()\n\n\ti.Next()\n}\n\nfunc TestID_Binary(cur realm, t *testing.T) {\n\tvar i ID\n\tprev := i.Binary()\n\n\tfor j := 0; j \u003c 1000; j++ {\n\t\tcur := i.Next().Binary()\n\t\tif cur \u003c= prev {\n\t\t\tt.Fatalf(\"cur %x \u003e prev %x\", cur, prev)\n\t\t}\n\t\tprev = cur\n\t}\n}\n\nfunc TestID_String(cur realm, t *testing.T) {\n\tvar i ID\n\tprev := i.String()\n\n\tfor j := 0; j \u003c 1000; j++ {\n\t\tcur := i.Next().String()\n\t\tif cur \u003c= prev {\n\t\t\tt.Fatalf(\"cur %s \u003e prev %s\", cur, prev)\n\t\t}\n\t\tprev = cur\n\t}\n\n\t// Test for when cford32 switches over to the long encoding.\n\ti = 1\u003c\u003c34 - 512\n\tfor j := 0; j \u003c 1024; j++ {\n\t\tcur := i.Next().String()\n\t\t// println(cur)\n\t\tif cur \u003c= prev {\n\t\t\tt.Fatalf(\"cur %s \u003e prev %s\", cur, prev)\n\t\t}\n\t\tprev = cur\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"XnUmmbUM6vKFIhzXqPtCbDikN1CLjWZq+EHznFAqvWkFsTSV8p6vZEiAkpjzXBrLAoigewwVZeZJIXFj1oWSTw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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"},{"name":"urequire_test.gno","body":"package urequire\n\nimport \"testing\"\n\nfunc TestPackage(t *testing.T) {\n\tEqual(t, 42, 42)\n\n\t// XXX: find a way to unit test this package thoroughly,\n\t// especially the t.FailNow() behavior on assertion failure.\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"CKQoi7uyIPAfZ14LP71s94t4BqWXN9YXNctGjsFfb2EKf83nmbGqPBrvE6bz/sZlUE8/e2yAaa3YE0GcJ2AAOA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"pager","path":"gno.land/p/nt/avl/pager/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/avl/pager/v0\"\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/rotree/v0\"\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"},{"name":"pager_test.gno","body":"package pager\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestPager_GetPage(t *testing.T) {\n\t// Create a new AVL tree and populate it with some key-value pairs.\n\ttree := avl.NewTree()\n\ttree.Set(\"a\", 1)\n\ttree.Set(\"b\", 2)\n\ttree.Set(\"c\", 3)\n\ttree.Set(\"d\", 4)\n\ttree.Set(\"e\", 5)\n\n\tt.Run(\"normal ordering\", func(t *testing.T) {\n\t\t// Create a new pager.\n\t\tpager := NewPager(tree, 10, false)\n\n\t\t// Define test cases.\n\t\ttests := []struct {\n\t\t\tpageNumber int\n\t\t\tpageSize   int\n\t\t\texpected   []Item\n\t\t}{\n\t\t\t{1, 2, []Item{{Key: \"a\", Value: 1}, {Key: \"b\", Value: 2}}},\n\t\t\t{2, 2, []Item{{Key: \"c\", Value: 3}, {Key: \"d\", Value: 4}}},\n\t\t\t{3, 2, []Item{{Key: \"e\", Value: 5}}},\n\t\t\t{1, 3, []Item{{Key: \"a\", Value: 1}, {Key: \"b\", Value: 2}, {Key: \"c\", Value: 3}}},\n\t\t\t{2, 3, []Item{{Key: \"d\", Value: 4}, {Key: \"e\", Value: 5}}},\n\t\t\t{1, 5, []Item{{Key: \"a\", Value: 1}, {Key: \"b\", Value: 2}, {Key: \"c\", Value: 3}, {Key: \"d\", Value: 4}, {Key: \"e\", Value: 5}}},\n\t\t\t{2, 5, []Item{}},\n\t\t}\n\n\t\tfor _, tt := range tests {\n\t\t\tpage := pager.GetPageWithSize(tt.pageNumber, tt.pageSize)\n\n\t\t\tuassert.Equal(t, len(tt.expected), len(page.Items))\n\n\t\t\tfor i, item := range page.Items {\n\t\t\t\tuassert.Equal(t, tt.expected[i].Key, item.Key)\n\t\t\t\tuassert.Equal(t, tt.expected[i].Value, item.Value)\n\t\t\t}\n\t\t}\n\t})\n\n\tt.Run(\"reversed ordering\", func(t *testing.T) {\n\t\t// Create a new pager.\n\t\tpager := NewPager(tree, 10, true)\n\n\t\t// Define test cases.\n\t\ttests := []struct {\n\t\t\tpageNumber int\n\t\t\tpageSize   int\n\t\t\texpected   []Item\n\t\t}{\n\t\t\t{1, 2, []Item{{Key: \"e\", Value: 5}, {Key: \"d\", Value: 4}}},\n\t\t\t{2, 2, []Item{{Key: \"c\", Value: 3}, {Key: \"b\", Value: 2}}},\n\t\t\t{3, 2, []Item{{Key: \"a\", Value: 1}}},\n\t\t\t{1, 3, []Item{{Key: \"e\", Value: 5}, {Key: \"d\", Value: 4}, {Key: \"c\", Value: 3}}},\n\t\t\t{2, 3, []Item{{Key: \"b\", Value: 2}, {Key: \"a\", Value: 1}}},\n\t\t\t{1, 5, []Item{{Key: \"e\", Value: 5}, {Key: \"d\", Value: 4}, {Key: \"c\", Value: 3}, {Key: \"b\", Value: 2}, {Key: \"a\", Value: 1}}},\n\t\t\t{2, 5, []Item{}},\n\t\t}\n\n\t\tfor _, tt := range tests {\n\t\t\tpage := pager.GetPageWithSize(tt.pageNumber, tt.pageSize)\n\n\t\t\tuassert.Equal(t, len(tt.expected), len(page.Items))\n\n\t\t\tfor i, item := range page.Items {\n\t\t\t\tuassert.Equal(t, tt.expected[i].Key, item.Key)\n\t\t\t\tuassert.Equal(t, tt.expected[i].Value, item.Value)\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc TestPager_GetPageByPath(t *testing.T) {\n\t// Create a new AVL tree and populate it with some key-value pairs.\n\ttree := avl.NewTree()\n\tfor i := 0; i \u003c 50; i++ {\n\t\ttree.Set(ufmt.Sprintf(\"key%d\", i), i)\n\t}\n\n\t// Create a new pager.\n\tpager := NewPager(tree, 10, false)\n\n\t// Define test cases.\n\ttests := []struct {\n\t\trawURL       string\n\t\texpectedPage int\n\t\texpectedSize int\n\t}{\n\t\t{\"/r/foo:bar/baz?size=10\u0026page=1\", 1, 10},\n\t\t{\"/r/foo:bar/baz?size=10\u0026page=2\", 2, 10},\n\t\t{\"/r/foo:bar/baz?page=3\", 3, pager.DefaultPageSize},\n\t\t{\"/r/foo:bar/baz?size=20\", 1, 20},\n\t\t{\"/r/foo:bar/baz\", 1, pager.DefaultPageSize},\n\t}\n\n\tfor _, tt := range tests {\n\t\tpage, err := pager.GetPageByPath(tt.rawURL)\n\t\turequire.NoError(t, err, ufmt.Sprintf(\"GetPageByPath(%s) returned error: %v\", tt.rawURL, err))\n\n\t\tuassert.Equal(t, tt.expectedPage, page.PageNumber)\n\t\tuassert.Equal(t, tt.expectedSize, page.PageSize)\n\t}\n}\n\nfunc TestPage_Picker(t *testing.T) {\n\t// Create a new AVL tree and populate it with some key-value pairs.\n\ttree := avl.NewTree()\n\ttree.Set(\"a\", 1)\n\ttree.Set(\"b\", 2)\n\ttree.Set(\"c\", 3)\n\ttree.Set(\"d\", 4)\n\ttree.Set(\"e\", 5)\n\n\t// Create a new pager.\n\tpager := NewPager(tree, 10, false)\n\n\t// Define test cases.\n\ttests := []struct {\n\t\tpageNumber int\n\t\tpageSize   int\n\t\tpath       string\n\t\texpected   string\n\t}{\n\t\t{1, 2, \"/test\", \"**1** | [2](?page=2) | [3](?page=3)\"},\n\t\t{2, 2, \"/test\", \"[1](?page=1) | **2** | [3](?page=3)\"},\n\t\t{3, 2, \"/test\", \"[1](?page=1) | [2](?page=2) | **3**\"},\n\t\t{1, 2, \"/test?foo=bar\", \"**1** | [2](?page=2\u0026foo=bar) | [3](?page=3\u0026foo=bar)\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tpage := pager.GetPageWithSize(tt.pageNumber, tt.pageSize)\n\n\t\tui := page.Picker(tt.path)\n\t\tuassert.Equal(t, tt.expected, ui)\n\t}\n}\n\nfunc TestPager_UI_WithManyPages(t *testing.T) {\n\t// Create a new AVL tree and populate it with many key-value pairs.\n\ttree := avl.NewTree()\n\tfor i := 0; i \u003c 100; i++ {\n\t\ttree.Set(ufmt.Sprintf(\"key%d\", i), i)\n\t}\n\n\t// Create a new pager.\n\tpager := NewPager(tree, 10, false)\n\n\t// Define test cases for a large number of pages.\n\ttests := []struct {\n\t\tpageNumber int\n\t\tpageSize   int\n\t\tpath       string\n\t\texpected   string\n\t}{\n\t\t{1, 10, \"/test\", \"**1** | [2](?page=2) | [3](?page=3) | … | [10](?page=10)\"},\n\t\t{2, 10, \"/test\", \"[1](?page=1) | **2** | [3](?page=3) | [4](?page=4) | … | [10](?page=10)\"},\n\t\t{3, 10, \"/test\", \"[1](?page=1) | [2](?page=2) | **3** | [4](?page=4) | [5](?page=5) | … | [10](?page=10)\"},\n\t\t{4, 10, \"/test\", \"[1](?page=1) | [2](?page=2) | [3](?page=3) | **4** | [5](?page=5) | [6](?page=6) | … | [10](?page=10)\"},\n\t\t{5, 10, \"/test\", \"[1](?page=1) | … | [3](?page=3) | [4](?page=4) | **5** | [6](?page=6) | [7](?page=7) | … | [10](?page=10)\"},\n\t\t{6, 10, \"/test\", \"[1](?page=1) | … | [4](?page=4) | [5](?page=5) | **6** | [7](?page=7) | [8](?page=8) | … | [10](?page=10)\"},\n\t\t{7, 10, \"/test\", \"[1](?page=1) | … | [5](?page=5) | [6](?page=6) | **7** | [8](?page=8) | [9](?page=9) | [10](?page=10)\"},\n\t\t{8, 10, \"/test\", \"[1](?page=1) | … | [6](?page=6) | [7](?page=7) | **8** | [9](?page=9) | [10](?page=10)\"},\n\t\t{9, 10, \"/test\", \"[1](?page=1) | … | [7](?page=7) | [8](?page=8) | **9** | [10](?page=10)\"},\n\t\t{10, 10, \"/test\", \"[1](?page=1) | … | [8](?page=8) | [9](?page=9) | **10**\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tpage := pager.GetPageWithSize(tt.pageNumber, tt.pageSize)\n\n\t\tui := page.Picker(tt.path)\n\t\tuassert.Equal(t, tt.expected, ui)\n\t}\n}\n\nfunc TestPager_ParseQuery(t *testing.T) {\n\t// Create a new AVL tree and populate it with some key-value pairs.\n\ttree := avl.NewTree()\n\ttree.Set(\"a\", 1)\n\ttree.Set(\"b\", 2)\n\ttree.Set(\"c\", 3)\n\ttree.Set(\"d\", 4)\n\ttree.Set(\"e\", 5)\n\n\t// Create a new pager.\n\tpager := NewPager(tree, 10, false)\n\n\t// Define test cases.\n\ttests := []struct {\n\t\trawURL        string\n\t\texpectedPage  int\n\t\texpectedSize  int\n\t\texpectedError bool\n\t}{\n\t\t{\"/r/foo:bar/baz?size=2\u0026page=1\", 1, 2, false},\n\t\t{\"/r/foo:bar/baz?size=3\u0026page=2\", 2, 3, false},\n\t\t{\"/r/foo:bar/baz?size=5\u0026page=3\", 3, 5, false},\n\t\t{\"/r/foo:bar/baz?page=2\", 2, pager.DefaultPageSize, false},\n\t\t{\"/r/foo:bar/baz?size=3\", 1, 3, false},\n\t\t{\"/r/foo:bar/baz\", 1, pager.DefaultPageSize, false},\n\t\t{\"/r/foo:bar/baz?size=0\u0026page=0\", 1, pager.DefaultPageSize, false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tpage, size, err := pager.ParseQuery(tt.rawURL)\n\t\tif tt.expectedError {\n\t\t\tuassert.Error(t, err, ufmt.Sprintf(\"ParseQuery(%s) expected error but got none\", tt.rawURL))\n\t\t} else {\n\t\t\turequire.NoError(t, err, ufmt.Sprintf(\"ParseQuery(%s) returned error: %v\", tt.rawURL, err))\n\t\t\tuassert.Equal(t, tt.expectedPage, page, ufmt.Sprintf(\"ParseQuery(%s) returned page %d, expected %d\", tt.rawURL, page, tt.expectedPage))\n\t\t\tuassert.Equal(t, tt.expectedSize, size, ufmt.Sprintf(\"ParseQuery(%s) returned size %d, expected %d\", tt.rawURL, size, tt.expectedSize))\n\t\t}\n\t}\n}\n\nfunc TestPage_PickerQueryParamPreservation(t *testing.T) {\n\ttree := avl.NewTree()\n\tfor i := 1; i \u003c= 6; i++ {\n\t\ttree.Set(ufmt.Sprintf(\"key%d\", i), i)\n\t}\n\n\tpager := NewPager(tree, 2, false)\n\n\ttests := []struct {\n\t\tname       string\n\t\tpageNumber int\n\t\tpath       string\n\t\texpected   string\n\t}{\n\t\t{\n\t\t\tname:       \"single query param\",\n\t\t\tpageNumber: 1,\n\t\t\tpath:       \"/test?foo=bar\",\n\t\t\texpected:   \"**1** | [2](?page=2\u0026foo=bar) | [3](?page=3\u0026foo=bar)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"multiple query params\",\n\t\t\tpageNumber: 2,\n\t\t\tpath:       \"/test?foo=bar\u0026baz=qux\",\n\t\t\texpected:   \"[1](?page=1\u0026baz=qux\u0026foo=bar) | **2** | [3](?page=3\u0026baz=qux\u0026foo=bar)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"overwrite existing page param\",\n\t\t\tpageNumber: 1,\n\t\t\tpath:       \"/test?param1=value1\u0026page=999\u0026param2=value2\",\n\t\t\texpected:   \"**1** | [2](?page=2\u0026param1=value1\u0026param2=value2) | [3](?page=3\u0026param1=value1\u0026param2=value2)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"empty query string\",\n\t\t\tpageNumber: 2,\n\t\t\tpath:       \"/test\",\n\t\t\texpected:   \"[1](?page=1) | **2** | [3](?page=3)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"query string with only page param\",\n\t\t\tpageNumber: 2,\n\t\t\tpath:       \"/test?page=2\",\n\t\t\texpected:   \"[1](?page=1) | **2** | [3](?page=3)\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tpage := pager.GetPageWithSize(tt.pageNumber, 2)\n\t\t\tresult := page.Picker(tt.path)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"\\nwant: %s\\ngot:  %s\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n"},{"name":"z_filetest.gno","body":"package main\n\nimport (\n\t\"gno.land/p/nt/avl/pager/v0\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nfunc main() {\n\t// Create a new AVL tree and populate it with some key-value pairs.\n\tvar id seqid.ID\n\ttree := avl.NewTree()\n\tfor i := 0; i \u003c 42; i++ {\n\t\ttree.Set(id.Next().String(), i)\n\t}\n\n\t// Create a new pager.\n\tpager := pager.NewPager(tree, 7, false)\n\n\tfor pn := -1; pn \u003c 8; pn++ {\n\t\tpage := pager.GetPage(pn)\n\n\t\tprintln(ufmt.Sprintf(\"## Page %d of %d\", page.PageNumber, page.TotalPages))\n\t\tfor idx, item := range page.Items {\n\t\t\tprintln(ufmt.Sprintf(\"- idx=%d key=%s value=%d\", idx, item.Key, item.Value))\n\t\t}\n\t\tprintln(page.Picker(\"/\"))\n\t\tprintln()\n\t}\n}\n\n// Output:\n// ## Page 0 of 6\n// _0_ | [1](?page=1) | [2](?page=2) | … | [6](?page=6)\n//\n// ## Page 0 of 6\n// _0_ | [1](?page=1) | [2](?page=2) | … | [6](?page=6)\n//\n// ## Page 1 of 6\n// - idx=0 key=0000001 value=0\n// - idx=1 key=0000002 value=1\n// - idx=2 key=0000003 value=2\n// - idx=3 key=0000004 value=3\n// - idx=4 key=0000005 value=4\n// - idx=5 key=0000006 value=5\n// - idx=6 key=0000007 value=6\n// **1** | [2](?page=2) | [3](?page=3) | … | [6](?page=6)\n//\n// ## Page 2 of 6\n// - idx=0 key=0000008 value=7\n// - idx=1 key=0000009 value=8\n// - idx=2 key=000000a value=9\n// - idx=3 key=000000b value=10\n// - idx=4 key=000000c value=11\n// - idx=5 key=000000d value=12\n// - idx=6 key=000000e value=13\n// [1](?page=1) | **2** | [3](?page=3) | [4](?page=4) | … | [6](?page=6)\n//\n// ## Page 3 of 6\n// - idx=0 key=000000f value=14\n// - idx=1 key=000000g value=15\n// - idx=2 key=000000h value=16\n// - idx=3 key=000000j value=17\n// - idx=4 key=000000k value=18\n// - idx=5 key=000000m value=19\n// - idx=6 key=000000n value=20\n// [1](?page=1) | [2](?page=2) | **3** | [4](?page=4) | [5](?page=5) | [6](?page=6)\n//\n// ## Page 4 of 6\n// - idx=0 key=000000p value=21\n// - idx=1 key=000000q value=22\n// - idx=2 key=000000r value=23\n// - idx=3 key=000000s value=24\n// - idx=4 key=000000t value=25\n// - idx=5 key=000000v value=26\n// - idx=6 key=000000w value=27\n// [1](?page=1) | [2](?page=2) | [3](?page=3) | **4** | [5](?page=5) | [6](?page=6)\n//\n// ## Page 5 of 6\n// - idx=0 key=000000x value=28\n// - idx=1 key=000000y value=29\n// - idx=2 key=000000z value=30\n// - idx=3 key=0000010 value=31\n// - idx=4 key=0000011 value=32\n// - idx=5 key=0000012 value=33\n// - idx=6 key=0000013 value=34\n// [1](?page=1) | … | [3](?page=3) | [4](?page=4) | **5** | [6](?page=6)\n//\n// ## Page 6 of 6\n// - idx=0 key=0000014 value=35\n// - idx=1 key=0000015 value=36\n// - idx=2 key=0000016 value=37\n// - idx=3 key=0000017 value=38\n// - idx=4 key=0000018 value=39\n// - idx=5 key=0000019 value=40\n// - idx=6 key=000001a value=41\n// [1](?page=1) | … | [4](?page=4) | [5](?page=5) | **6**\n//\n// ## Page 7 of 6\n// [1](?page=1) | … | [5](?page=5) | [6](?page=6) | _7_\n//\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"059r9L6hyKPmKzSv2n+HrXMDzGnk2Ckv2eQPNwcpspsg1eja5xt3hWn2cn4gtXvrqSmcwnjN2eoPtAhBSE06Dg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"grc20","path":"gno.land/p/nt/grc20/v1","files":[{"name":"README.md","body":"# `grc20` v1 - GRC20 tokens with `uint256` amounts\n\n`gno.land/p/nt/grc20/v1` is `gno.land/p/nt/grc20/v0` with amounts typed as\nthe native `uint256` instead of `int64`. Everything else (the `Teller`\ninterface, `PrivateLedger`, the teller constructors, `IsCanonicalTeller`,\ntoken identity and events) works the same way, so read v0's documentation and\nthe GRC20 section of `docs/resources/gno-security-guide.md` for the model.\n\nv0 stays as it is for the tokens already built on it. New tokens, and tokens\nthat bridge 256-bit amounts (e.g. from Ethereum), should use v1. The two\nversions are separate types: a v1 `*Token` is not a v0 `*Token`, and each has\nits own registry (`r/nt/grc20reg/v0`, `r/nt/grc20reg/v1`).\n\n## Differences from v0\n\n| | v0 | v1 |\n|---|---|---|\n| Amounts, balances, allowances, total supply | `int64` | `uint256` |\n| Supply cap (`ErrMintOverflow`) | `math.MaxInt64` | `math.MaxUint256` |\n| Negative amounts | rejected with `ErrInvalidAmount` | not representable; `ErrInvalidAmount` is gone |\n| Arithmetic | `overflow.Add64p`/`Sub64p` | `overflow.Addu256`/`Subu256` (checked, never wraps) |\n| Zero balances and allowances | a zero mint, transfer or approval stores `0` | never stored: absent means zero (`Approve(..., 0)` removes the entry) |\n| `KnownAccounts()` | can count a zero balance left by a zero mint or transfer | accounts with a non-zero balance |\n| Event `value` | decimal via `strconv.Itoa` | decimal via `strconv.FormatUint256` |\n| Amount formatting | none | `FormatAmount(amount, decimals)`, `Token.FormatAmount` |\n\n## Calling a v1 token\n\n`maketx call` arguments for a `uint256` parameter are canonical decimal\nstrings (no sign, no leading zeros, at most 78 digits), so\n`-args 1000000` works the same as for an `int64` parameter, and values above\n2^63 are accepted. Realm code passes `uint256` values; converting from an\n`int64` is `uint256(x)` after checking `x \u003e= 0`, and converting down needs an\nexplicit range check (`x \u003c= math.MaxInt64`).\n\nChain coins stay `int64`: a realm that wraps coins into a v1 token (such as\n`r/gnoland/wugnot`) widens on deposit and range-checks on withdrawal.\n"},{"name":"amount_test.gno","body":"package grc20\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n)\n\n// equal256 and require256 compare 256-bit amounts natively; uassert and\n// urequire do not support uint256 (see p/nt/ufmt's README).\nfunc equal256(t *testing.T, want, got uint256, msgs ...string) bool {\n\tt.Helper()\n\tif want == got {\n\t\treturn true\n\t}\n\tt.Errorf(\"%swant %s, got %s\", msgPrefix(msgs), strconv.FormatUint256(want, 10), strconv.FormatUint256(got, 10))\n\treturn false\n}\n\nfunc require256(t *testing.T, want, got uint256, msgs ...string) {\n\tt.Helper()\n\tif want != got {\n\t\tt.Fatalf(\"%swant %s, got %s\", msgPrefix(msgs), strconv.FormatUint256(want, 10), strconv.FormatUint256(got, 10))\n\t}\n}\n\nfunc msgPrefix(msgs []string) string {\n\tif len(msgs) == 0 {\n\t\treturn \"\"\n\t}\n\treturn msgs[0] + \": \"\n}\n\n// fmtAmounts renders name=amount pairs, in decimal, separated by spaces.\nfunc fmtAmounts(pairs ...any) string {\n\ts := \"\"\n\tfor i := 0; i \u003c len(pairs); i += 2 {\n\t\tif s != \"\" {\n\t\t\ts += \" \"\n\t\t}\n\t\ts += pairs[i].(string) + \"=\" + strconv.FormatUint256(pairs[i+1].(uint256), 10)\n\t}\n\treturn s\n}\n"},{"name":"caller_teller_sub_realm_filetest.gno","body":"// PKGPATH: gno.land/r/demo/grc20subrealm\n\n// A frame-relative teller must keep working when the token's OWN realm\n// operates under an identity minted by its own cur.Sub(). The sub's synthesized\n// pkgpath is \"\u003chost\u003e#vault\", which is not origRealm verbatim, so a raw string\n// comparison would refuse the token's own realm — a false positive that would\n// break the one property the home binding promises unconditionally.\npackage grc20subrealm\n\nimport (\n\t\"chain\"\n\n\t\"gno.land/p/nt/grc20/v1\"\n)\n\nvar (\n\ttoken  *grc20.Token\n\tledger *grc20.PrivateLedger\n\tteller grc20.Teller\n)\n\nfunc init(cur realm) {\n\ttoken, ledger = grc20.NewToken(\"SubRealm\", \"SUBR\", 4, 0, cur)\n\tteller = ledger.CallerTeller()\n}\n\nfunc main(cur realm) {\n\tpayer := cur.Previous().Address() // whom the frame-relative teller debits\n\tbob := chain.PackageAddress(\"bob\")\n\tledger.Mint(payer, 1_000)\n\n\t// Control: the primary identity moves the caller's funds.\n\tif err := teller.Transfer(0, cur, bob, 100); err != nil {\n\t\tpanic(\"primary identity rejected: \" + err.Error())\n\t}\n\n\t// Same realm, same teller, under this realm's own \"vault\" sub identity.\n\tif err := teller.Transfer(0, cur.Sub(\"vault\"), bob, 100); err != nil {\n\t\tpanic(\"own sub identity rejected: \" + err.Error())\n\t}\n\n\tif got := token.BalanceOf(bob); got != 200 {\n\t\tpanic(\"unexpected bob balance\")\n\t}\n\n\t// The mirror image: a token CREATED from a sub frame must still be usable\n\t// from its host realm. origRealm drops the subpath at construction, so the\n\t// token belongs to the host rather than being stranded in the sub.\n\tsubTok, subLedger := grc20.NewToken(\"FromSub\", \"FSUB\", 4, 1, cur.Sub(\"vault\"))\n\tsubTeller := subLedger.CallerTeller()\n\tsubLedger.Mint(payer, 500)\n\tif err := subTeller.Transfer(0, cur, bob, 50); err != nil {\n\t\tpanic(\"host realm rejected for a sub-created token: \" + err.Error())\n\t}\n\tif got := subTok.BalanceOf(bob); got != 50 {\n\t\tpanic(\"unexpected bob balance on the sub-created token\")\n\t}\n\n\tprintln(\"ok\")\n}\n\n// Output:\n// ok\n"},{"name":"doc.gno","body":"// Package grc20 implements GRC20 fungible tokens whose amounts are native\n// uint256 values.\n//\n// It has the API and the security model of gno.land/p/nt/grc20/v0\n// (Teller, PrivateLedger, CallerTeller/RealmTeller/ReadonlyTeller,\n// IsCanonicalTeller), with every amount, balance, allowance and the total\n// supply typed uint256 instead of int64. All arithmetic is checked with\n// math/overflow: wrapping is the language default for uint256, and a token\n// ledger must never wrap. Mint refuses (ErrMintOverflow) any amount that\n// would take the total supply past math.MaxUint256.\n//\n// Chain coins (chain.Coin, chain/banker) stay int64; code that converts\n// between them and grc20 amounts must range-check the conversion.\npackage grc20\n"},{"name":"event_provenance_filetest.gno","body":"// PKGPATH: gno.land/r/demo/grc20provenance\n\n// This filetest is the reference for how an indexer attributes a grc20 event to\n// the realm that issued the token. It is the cross-realm counterpart to\n// newtoken_event_filetest.gno, which covers the within-realm duplicate signal.\n//\n// Two fields matter, and neither is sufficient alone:\n//\n//   - pkg_path is stamped by the VM from the package whose code calls\n//     chain.Emit. Every genuine grc20 event therefore carries\n//     gno.land/p/nt/grc20/v1 — a constant, so it identifies the library\n//     but never the token.\n//   - the \"token\" attribute carries Token.ID(), whose leading component is\n//     rlm.PkgPath() captured under IsCurrent() in NewToken. A realm cannot\n//     produce a Token whose id claims a different realm.\n//\n// Chained, they are sufficient: pkg_path proves the event came from grc20's\n// code, and grc20's code only ever writes an IsCurrent-verified realm prefix\n// into \"token\". So for any event bearing grc20's pkg_path, the realm prefix in\n// \"token\" is unforgeable.\n//\n// The realm below is an impostor trying to pass its own transfers off as\n// another realm's token, by both routes available to it, and failing at both.\npackage grc20provenance\n\nimport (\n\t\"chain\"\n\n\t\"gno.land/p/nt/grc20/v1\"\n)\n\n// Stand-in for a token issued and registered by some other realm. Written as a\n// literal rather than imported: a filetest under a /p/ package that imports an\n// /r/ realm which itself imports that /p/ package forms a genesis dependency\n// cycle (grc20 -\u003e realm -\u003e grc20), which breaks package loading chain-wide.\nconst canonical = \"gno.land/r/demo/canonicaltoken.FOO.0000000\"\n\nfunc main(cur realm) {\n\tprintln(\"canonical id:\", canonical)\n\n\t// Route 1: ask grc20 for a token using the canonical token's own\n\t// name/symbol/decimals/id. NewToken derives the id prefix from the live\n\t// crossing frame, so the id names *this* realm. No argument changes that.\n\timpostor, impostorLedger := grc20.NewToken(\"Foo\", \"FOO\", 4, 0, cur)\n\tprintln(\"impostor id: \", impostor.ID())\n\tprintln(\"impostor claims canonical id:\", impostor.ID() == canonical)\n\n\t// A genuine grc20 event from the impostor. It carries grc20's pkg_path —\n\t// identical to the canonical token's events — but the \"token\" prefix is\n\t// this realm, so an indexer attributes it here.\n\timpostorLedger.Mint(cur.Address(), 999_999)\n\n\t// Route 2: skip grc20 and hand-emit an event carrying the canonical id.\n\t// The attributes are fully attacker-chosen, but pkg_path is not: the VM\n\t// stamps this realm. An indexer that accepts Transfer only from grc20's\n\t// pkg_path drops this event.\n\tchain.Emit(\n\t\t\"Transfer\",\n\t\t\"token\", canonical,\n\t\t\"from\", \"\",\n\t\t\"to\", cur.Address().String(),\n\t\t\"value\", \"999999\",\n\t)\n}\n\n// Output:\n// canonical id: gno.land/r/demo/canonicaltoken.FOO.0000000\n// impostor id:  gno.land/r/demo/grc20provenance.FOO.0000000\n// impostor claims canonical id: false\n\n// Events:\n// [\n//   {\n//     \"type\": \"NewToken\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20provenance.FOO.0000000\"\n//       },\n//       {\n//         \"key\": \"name\",\n//         \"value\": \"Foo\"\n//       },\n//       {\n//         \"key\": \"symbol\",\n//         \"value\": \"FOO\"\n//       },\n//       {\n//         \"key\": \"decimals\",\n//         \"value\": \"4\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v1\"\n//   },\n//   {\n//     \"type\": \"Transfer\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20provenance.FOO.0000000\"\n//       },\n//       {\n//         \"key\": \"from\",\n//         \"value\": \"\"\n//       },\n//       {\n//         \"key\": \"to\",\n//         \"value\": \"g1vz4y807zq7p3s63dmyng9g80l58nmuc3lgmfjs\"\n//       },\n//       {\n//         \"key\": \"value\",\n//         \"value\": \"999999\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v1\"\n//   },\n//   {\n//     \"type\": \"Transfer\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/canonicaltoken.FOO.0000000\"\n//       },\n//       {\n//         \"key\": \"from\",\n//         \"value\": \"\"\n//       },\n//       {\n//         \"key\": \"to\",\n//         \"value\": \"g1vz4y807zq7p3s63dmyng9g80l58nmuc3lgmfjs\"\n//       },\n//       {\n//         \"key\": \"value\",\n//         \"value\": \"999999\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/r/demo/grc20provenance\"\n//   }\n// ]\n"},{"name":"examples_test.gno","body":"package grc20\n\n// XXX: write Examples\n\nfunc ExampleInit()                            {}\nfunc ExampleExposeBankForMaketxRunOrImports() {}\nfunc ExampleCustomTellerImpl()                {}\nfunc ExampleAllowance()                       {}\nfunc ExampleRealmBanker()                     {}\nfunc ExamplePreviousRealmBanker()             {}\nfunc ExampleAccountBanker()                   {}\nfunc ExampleTransfer()                        {}\nfunc ExampleApprove()                         {}\nfunc ExampleTransferFrom()                    {}\nfunc ExampleMint()                            {}\nfunc ExampleBurn()                            {}\n\n// ...\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/grc20/v1\"\ngno = \"0.9\"\n"},{"name":"mock.gno","body":"package grc20\n\n// XXX: func Mock(t *Token)\n"},{"name":"newtoken_event_filetest.gno","body":"// PKGPATH: gno.land/r/demo/grc20dupsignal\n\n// This filetest is the reference for the detection rule the NewToken event\n// enables. The realm below reuses seqid 0 twice, so both tokens end up with the\n// same Token.ID() and their Transfer events are indistinguishable — the\n// long-standing event-provenance ambiguity.\n//\n// What changes is that the ambiguity is now *announced*. Two NewToken events\n// carry the same \"token\" value, which is a complete signal: NewToken is the only\n// way a Token can exist (Token's fields are unexported), so an indexer watching\n// this event sees every token that will ever emit. On the second duplicate\n// announcement it should flag the realm and stop trusting its token events.\npackage grc20dupsignal\n\nimport (\n\t\"gno.land/p/nt/grc20/v1\"\n)\n\nfunc main(cur realm) {\n\tfirst, firstLedger := grc20.NewToken(\"Same\", \"DUP\", 6, 0, cur)\n\tsecond, secondLedger := grc20.NewToken(\"Same\", \"DUP\", 6, 0, cur)\n\n\tprintln(\"same id:\", first.ID() == second.ID())\n\n\t// Two independent ledgers, one identifier: these Transfers cannot be\n\t// attributed to either object from the event stream alone.\n\tfirstLedger.Mint(cur.Address(), 1)\n\tsecondLedger.Mint(cur.Address(), 999999)\n}\n\n// Output:\n// same id: true\n\n// Events:\n// [\n//   {\n//     \"type\": \"NewToken\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20dupsignal.DUP.0000000\"\n//       },\n//       {\n//         \"key\": \"name\",\n//         \"value\": \"Same\"\n//       },\n//       {\n//         \"key\": \"symbol\",\n//         \"value\": \"DUP\"\n//       },\n//       {\n//         \"key\": \"decimals\",\n//         \"value\": \"6\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v1\"\n//   },\n//   {\n//     \"type\": \"NewToken\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20dupsignal.DUP.0000000\"\n//       },\n//       {\n//         \"key\": \"name\",\n//         \"value\": \"Same\"\n//       },\n//       {\n//         \"key\": \"symbol\",\n//         \"value\": \"DUP\"\n//       },\n//       {\n//         \"key\": \"decimals\",\n//         \"value\": \"6\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v1\"\n//   },\n//   {\n//     \"type\": \"Transfer\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20dupsignal.DUP.0000000\"\n//       },\n//       {\n//         \"key\": \"from\",\n//         \"value\": \"\"\n//       },\n//       {\n//         \"key\": \"to\",\n//         \"value\": \"g1k29lxz0d8gx7m94n032ulum875p7gg0kduus5m\"\n//       },\n//       {\n//         \"key\": \"value\",\n//         \"value\": \"1\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v1\"\n//   },\n//   {\n//     \"type\": \"Transfer\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20dupsignal.DUP.0000000\"\n//       },\n//       {\n//         \"key\": \"from\",\n//         \"value\": \"\"\n//       },\n//       {\n//         \"key\": \"to\",\n//         \"value\": \"g1k29lxz0d8gx7m94n032ulum875p7gg0kduus5m\"\n//       },\n//       {\n//         \"key\": \"value\",\n//         \"value\": \"999999\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v1\"\n//   }\n// ]\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).\n//\n// SECURITY: this accessor hangs off *PrivateLedger, not *Token, and that is\n// load-bearing. A frame-relative teller debits whoever crossed into the realm\n// holding it, so it is only ever meaningful inside the token's own realm,\n// whose wrappers act for a caller who knowingly invoked the token. Anywhere\n// else it is a confused deputy. The *Token pointer is published (exported\n// vars, grc20factory, grc20reg) but the ledger is not — NewToken hands it to\n// the creating realm and nowhere else — so a foreign realm cannot mint one.\n//\n// Construction privacy alone is not enough: a realm may legally build a teller\n// and then export the VALUE. The write methods therefore also verify that the\n// invoking realm is the token's own (see guardHome), which makes a leaked\n// teller inert everywhere but home.\nfunc (ledger *PrivateLedger) CallerTeller() Teller {\n\tif ledger == nil {\n\t\tpanic(\"Ledger 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\thomeGuard: true,\n\t\tToken:     ledger.token,\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\n// guardHome confines a frame-relative (homeGuard) teller to the token's own\n// realm. Construction privacy stops a foreign realm from minting one;\n// this stops a minted one from travelling, which is what happens when a realm\n// legally builds a teller and then exports the value.\n//\n// The check is on the invoking realm's path alone — deliberately NOT on\n// whether the resolved actor is an end user. Keying on the actor only blocks\n// the case where the debited party is the signing user, which leaves two doors\n// open: a realm can be charged by a realm it calls, and TransferFrom resolves\n// the *spender* from the frame, so a realm reached from an honest hub spends\n// that hub's allowance against any owner who granted one. Both are the same\n// defect as the original one level up — frame-relative resolution means\n// whoever you call can act as you — and neither is reachable once the teller\n// only works at home.\n//\n// The host is compared after stripping any \"#subpath\" synthesized by\n// realm.Sub, so the token's own sub-realms are not falsely rejected.\n//\n// A foreign realm that needs to move a user's funds uses the ordinary route:\n// Approve, then RealmTeller().TransferFrom, which is eagerly bound to that\n// realm's own address and allowance-gated.\n//\n// The leading int keeps this a plain method.\nfunc (ft *fnTeller) guardHome(_ int, rlm realm) error {\n\tif !ft.homeGuard {\n\t\treturn nil\n\t}\n\thost, _, _ := chain.SplitPkgSubPath(rlm.PkgPath())\n\tif host != ft.Token.origRealm {\n\t\treturn ErrForeignCallerTeller\n\t}\n\treturn nil\n}\n\nfunc (ft *fnTeller) Transfer(_ int, rlm realm, to address, amount uint256) error {\n\tif ft.accountFn == nil {\n\t\treturn ErrReadonly\n\t}\n\tif !rlm.IsCurrent() {\n\t\treturn ErrSpoofedRealm\n\t}\n\tif err := ft.guardHome(0, rlm); err != nil {\n\t\treturn err\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 uint256) error {\n\tif ft.accountFn == nil {\n\t\treturn ErrReadonly\n\t}\n\tif !rlm.IsCurrent() {\n\t\treturn ErrSpoofedRealm\n\t}\n\tif err := ft.guardHome(0, rlm); err != nil {\n\t\treturn err\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 uint256) error {\n\tif ft.accountFn == nil {\n\t\treturn ErrReadonly\n\t}\n\tif !rlm.IsCurrent() {\n\t\treturn ErrSpoofedRealm\n\t}\n\tif err := ft.guardHome(0, rlm); err != nil {\n\t\treturn err\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":"tellers_test.gno","body":"package grc20\n\nimport (\n\t\"math\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestCallerTellerImpl(cur realm, t *testing.T) {\n\ttok, ledger := newTestToken(\"Dummy\", \"DUMMY\", 4, 0, cur)\n\tteller := ledger.CallerTeller()\n\turequire.False(t, tok == nil)\n\tvar _ Teller = teller\n}\n\n// The sub-realm case (guardHome must not lock out the token's own realm when it\n// operates under an identity minted by its own cur.Sub()) needs a real realm\n// frame, since cur.Sub() refuses to mint from a p/ package. It lives in\n// filetests/caller_teller_sub_realm_filetest.gno.\n\n// evilWrap embeds Teller; the seal-marker pattern (if we had one) would be\n// satisfied via promotion, but IsCanonicalTeller rejects it because *evilWrap\n// is nominally distinct from *fnTeller.\ntype evilWrap struct {\n\tTeller\n}\n\nfunc TestIsCanonicalTeller(cur realm, t *testing.T) {\n\t_, ledger := newTestToken(\"Dummy\", \"DUMMY\", 4, 0, cur)\n\tlegit := ledger.CallerTeller()\n\n\tuassert.True(t, IsCanonicalTeller(legit),\n\t\t\"canonical *fnTeller from CallerTeller must pass\")\n\n\tevil := \u0026evilWrap{Teller: legit}\n\tuassert.False(t, IsCanonicalTeller(evil),\n\t\t\"foreign type embedding a canonical Teller must be rejected\")\n}\n\nfunc TestTeller(cur realm, t *testing.T) {\n\tvar (\n\t\talice = testutils.TestAddress(\"alice\")\n\t\tbob   = testutils.TestAddress(\"bob\")\n\t\tcarl  = testutils.TestAddress(\"carl\")\n\t)\n\n\ttoken, ledger := newTestToken(\"Dummy\", \"DUMMY\", 6, 0, cur)\n\n\tcheckBalances := func(aliceEB, bobEB, carlEB uint256) {\n\t\tt.Helper()\n\t\texp := fmtAmounts(\"alice\", aliceEB, \"bob\", bobEB, \"carl\", carlEB)\n\t\tgot := fmtAmounts(\"alice\", token.BalanceOf(alice), \"bob\", token.BalanceOf(bob), \"carl\", token.BalanceOf(carl))\n\t\tuassert.Equal(t, got, exp, \"invalid balances\")\n\t}\n\tcheckAllowances := func(abEB, acEB, baEB, bcEB, caEB, cbEB uint256) {\n\t\tt.Helper()\n\t\texp := fmtAmounts(\"ab\", abEB, \"ac\", acEB, \"ba\", baEB, \"bc\", bcEB, \"ca\", caEB, \"cb\", cbEB)\n\t\tgot := fmtAmounts(\n\t\t\t\"ab\", token.Allowance(alice, bob), \"ac\", token.Allowance(alice, carl),\n\t\t\t\"ba\", token.Allowance(bob, alice), \"bc\", token.Allowance(bob, carl),\n\t\t\t\"ca\", token.Allowance(carl, alice), \"cb\", token.Allowance(carl, bob),\n\t\t)\n\t\tuassert.Equal(t, got, exp, \"invalid allowances\")\n\t}\n\n\tcheckBalances(0, 0, 0)\n\tcheckAllowances(0, 0, 0, 0, 0, 0)\n\n\turequire.NoError(t, ledger.Mint(alice, 1000))\n\turequire.NoError(t, ledger.Mint(alice, 100))\n\tcheckBalances(1100, 0, 0)\n\tcheckAllowances(0, 0, 0, 0, 0, 0)\n\n\turequire.NoError(t, ledger.Approve(alice, bob, 99999999))\n\tcheckBalances(1100, 0, 0)\n\tcheckAllowances(99999999, 0, 0, 0, 0, 0)\n\n\turequire.NoError(t, ledger.Approve(alice, bob, 400))\n\tcheckBalances(1100, 0, 0)\n\tcheckAllowances(400, 0, 0, 0, 0, 0)\n\n\turequire.Error(t, ledger.TransferFrom(alice, bob, carl, 100000000))\n\tcheckBalances(1100, 0, 0)\n\tcheckAllowances(400, 0, 0, 0, 0, 0)\n\n\turequire.NoError(t, ledger.TransferFrom(alice, bob, carl, 100))\n\tcheckBalances(1000, 0, 100)\n\tcheckAllowances(300, 0, 0, 0, 0, 0)\n\n\turequire.Error(t, ledger.SpendAllowance(alice, bob, 2000000))\n\tcheckBalances(1000, 0, 100)\n\tcheckAllowances(300, 0, 0, 0, 0, 0)\n\n\turequire.NoError(t, ledger.SpendAllowance(alice, bob, 100))\n\tcheckBalances(1000, 0, 100)\n\tcheckAllowances(200, 0, 0, 0, 0, 0)\n}\n\nfunc TestCallerTeller(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\tcarl := testutils.TestAddress(\"carl\")\n\n\ttoken, ledger := newTestToken(\"Dummy\", \"DUMMY\", 6, 0, cur)\n\tteller := ledger.CallerTeller()\n\n\tcheckBalances := func(aliceEB, bobEB, carlEB uint256) {\n\t\tt.Helper()\n\t\texp := fmtAmounts(\"alice\", aliceEB, \"bob\", bobEB, \"carl\", carlEB)\n\t\tgot := fmtAmounts(\"alice\", token.BalanceOf(alice), \"bob\", token.BalanceOf(bob), \"carl\", token.BalanceOf(carl))\n\t\tuassert.Equal(t, got, exp, \"invalid balances\")\n\t}\n\tcheckAllowances := func(abEB, acEB, baEB, bcEB, caEB, cbEB uint256) {\n\t\tt.Helper()\n\t\texp := fmtAmounts(\"ab\", abEB, \"ac\", acEB, \"ba\", baEB, \"bc\", bcEB, \"ca\", caEB, \"cb\", cbEB)\n\t\tgot := fmtAmounts(\n\t\t\t\"ab\", token.Allowance(alice, bob), \"ac\", token.Allowance(alice, carl),\n\t\t\t\"ba\", token.Allowance(bob, alice), \"bc\", token.Allowance(bob, carl),\n\t\t\t\"ca\", token.Allowance(carl, alice), \"cb\", token.Allowance(carl, bob),\n\t\t)\n\t\tuassert.Equal(t, got, exp, \"invalid allowances\")\n\t}\n\n\turequire.NoError(t, ledger.Mint(alice, 1000))\n\tcheckBalances(1000, 0, 0)\n\tcheckAllowances(0, 0, 0, 0, 0, 0)\n\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\tfunc(cur realm) { urequire.NoError(t, teller.Approve(0, cur, bob, 600)) }(cross(cur))\n\tcheckBalances(1000, 0, 0)\n\tcheckAllowances(600, 0, 0, 0, 0, 0)\n\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\tfunc(cur realm) { urequire.Error(t, teller.TransferFrom(0, cur, alice, carl, 700)) }(cross(cur))\n\tcheckBalances(1000, 0, 0)\n\tcheckAllowances(600, 0, 0, 0, 0, 0)\n\tfunc(cur realm) { urequire.NoError(t, teller.TransferFrom(0, cur, alice, carl, 400)) }(cross(cur))\n\tcheckBalances(600, 0, 400)\n\tcheckAllowances(200, 0, 0, 0, 0, 0)\n}\n\nfunc TestReadonlyTeller(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\n\ttoken, ledger := newTestToken(\"Dummy\", \"DUMMY\", 6, 0, cur)\n\turequire.NoError(t, ledger.Mint(alice, math.MaxUint256))\n\tro := token.ReadonlyTeller()\n\n\tequal256(t, uint256(math.MaxUint256), ro.TotalSupply(), \"readonly TotalSupply\")\n\tequal256(t, uint256(math.MaxUint256), ro.BalanceOf(alice), \"readonly BalanceOf\")\n\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\tfunc(cur realm) {\n\t\tuassert.ErrorIs(t, ro.Transfer(0, cur, bob, 1), ErrReadonly)\n\t\tuassert.ErrorIs(t, ro.Approve(0, cur, bob, 1), ErrReadonly)\n\t\tuassert.ErrorIs(t, ro.TransferFrom(0, cur, alice, bob, 1), ErrReadonly)\n\t}(cross(cur))\n\tequal256(t, uint256(0), token.BalanceOf(bob), \"readonly teller moved funds\")\n}\n\n// TestCallerTellerAbove2Pow63 drives the frame-relative teller with amounts\n// beyond the int64 range, including an approval of MaxUint256.\nfunc TestCallerTellerAbove2Pow63(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\tcarl := testutils.TestAddress(\"carl\")\n\n\ttoken, ledger := newTestToken(\"Dummy\", \"DUMMY\", 18, 0, cur)\n\tteller := ledger.CallerTeller()\n\n\tbig := uint256(1) \u003c\u003c 200\n\turequire.NoError(t, ledger.Mint(alice, big))\n\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\tfunc(cur realm) {\n\t\turequire.NoError(t, teller.Transfer(0, cur, bob, big/2))\n\t\turequire.NoError(t, teller.Approve(0, cur, bob, math.MaxUint256))\n\t}(cross(cur))\n\trequire256(t, uint256(big/2), token.BalanceOf(alice), \"alice\")\n\trequire256(t, uint256(big/2), token.BalanceOf(bob), \"bob\")\n\trequire256(t, uint256(math.MaxUint256), token.Allowance(alice, bob), \"allowance\")\n\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\tfunc(cur realm) {\n\t\t// One more than alice holds is refused.\n\t\tuassert.ErrorIs(t, teller.TransferFrom(0, cur, alice, carl, big/2+1), ErrInsufficientBalance)\n\t\turequire.NoError(t, teller.TransferFrom(0, cur, alice, carl, big/2))\n\t}(cross(cur))\n\trequire256(t, uint256(0), token.BalanceOf(alice), \"alice drained\")\n\trequire256(t, uint256(big/2), token.BalanceOf(carl), \"carl\")\n\trequire256(t, uint256(math.MaxUint256-big/2), token.Allowance(alice, bob), \"allowance decreased\")\n}\n"},{"name":"token.gno","body":"package grc20\n\nimport (\n\t\"chain\"\n\t\"math\"\n\t\"math/overflow\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/markdown/sanitize/v0\"\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, with any realm.Sub subpath\n// stripped — becomes the Token's origRealm. Token.ID() keeps the raw\n// rlm.PkgPath(), and returns it + \".\" + 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), using gno.land/r/nt/grc20reg/v1\n// (the v0 registry only holds v0 tokens). The registry key is the\n// rlmPath.symbol prefix of Token.ID(), without its trailing 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\t// origRealm drops any realm.Sub subpath: a token created while its realm\n\t// operates under a sub identity still belongs to the host realm. guardHome\n\t// resolves the invoking host the same way, so storing the raw path here\n\t// would pin the token to that sub and lock its own realm out for good.\n\torigRealm, _, _ := chain.SplitPkgSubPath(pkgPath)\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\torigRealm: origRealm,\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() uint256 { return tok.ledger.totalSupply }\n\n// KnownAccounts returns the number of known accounts in the bank, that is the\n// number of addresses holding a non-zero balance.\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 creating realm's raw pkgpath — including any\n// realm.Sub subpath, unlike origRealm — 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) uint256 {\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) uint256 {\n\treturn tok.ledger.allowance(owner, spender)\n}\n\n// FormatAmount renders a raw token amount as a decimal string with the\n// token's decimals applied, e.g. 12345 with 4 decimals is \"1.2345\". Trailing\n// zeros of the fractional part are kept, so the width shows the precision.\nfunc (tok Token) FormatAmount(amount uint256) string {\n\treturn FormatAmount(amount, tok.decimals)\n}\n\n// FormatAmount renders a raw amount as a decimal string with `decimals`\n// fractional digits (\"0.0012\" for 12 with 4 decimals). decimals \u003c= 0 renders\n// the plain integer.\nfunc FormatAmount(amount uint256, decimals int) string {\n\tdigits := strconv.FormatUint256(amount, 10)\n\tif decimals \u003c= 0 {\n\t\treturn digits\n\t}\n\tif len(digits) \u003c= decimals {\n\t\tdigits = strings.Repeat(\"0\", decimals-len(digits)+1) + digits\n\t}\n\tcut := len(digits) - decimals\n\treturn digits[:cut] + \".\" + digits[cut:]\n}\n\nfunc (tok Token) RenderHome() string {\n\tstr := \"\"\n\t// name is display text that validName lets carry markdown (links,\n\t// emphasis, HTML), so it is escaped; symbol is escaped too for uniformity.\n\tstr += ufmt.Sprintf(\"# %s ($%s)\\n\\n\", sanitize.InlineText(tok.name), sanitize.InlineText(tok.symbol))\n\tstr += ufmt.Sprintf(\"* **Decimals**: %d\\n\", tok.decimals)\n\tstr += \"* **Total supply**: \" + strconv.FormatUint256(tok.ledger.totalSupply, 10) + \"\\n\"\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 uint256) error {\n\tif !owner.IsValid() || !spender.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\n\t// do nothing\n\tif amount == 0 {\n\t\treturn nil\n\t}\n\n\tcurrentAllowance := led.allowance(owner, spender)\n\tnewAllowance, ok := overflow.Subu256(currentAllowance, amount)\n\tif !ok {\n\t\treturn ErrInsufficientAllowance\n\t}\n\n\tled.setAllowance(owner, spender, newAllowance)\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 uint256) 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\n\tnewFromBalance, ok := overflow.Subu256(led.balanceOf(from), amount)\n\tif !ok {\n\t\treturn ErrInsufficientBalance\n\t}\n\t// Cannot overflow while the balances sum to totalSupply \u003c= MaxUint256;\n\t// the checked add keeps that invariant from being assumed silently.\n\tnewToBalance := overflow.Addu256p(led.balanceOf(to), amount)\n\n\tled.setBalance(to, newToBalance)\n\tled.setBalance(from, newFromBalance)\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.FormatUint256(amount, 10),\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 uint256) error {\n\tif !owner.IsValid() || !to.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tif owner == to {\n\t\treturn ErrCannotTransferToSelf\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 uint256) error {\n\tif !owner.IsValid() || !spender.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tled.setAllowance(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.FormatUint256(amount, 10),\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.\n// The total supply is capped at math.MaxUint256: an amount that would exceed\n// it is refused with ErrMintOverflow and changes nothing.\nfunc (led *PrivateLedger) Mint(addr address, amount uint256) error {\n\tif !addr.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\n\t// limit amount to MaxUint256 - totalSupply\n\tif amount \u003e overflow.Subu256p(math.MaxUint256, led.totalSupply) {\n\t\treturn ErrMintOverflow\n\t}\n\n\tled.totalSupply = overflow.Addu256p(led.totalSupply, amount)\n\t// A balance is bounded by totalSupply, so this cannot overflow either.\n\tnewBalance := overflow.Addu256p(led.balanceOf(addr), amount)\n\tled.setBalance(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.FormatUint256(amount, 10),\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 uint256) error {\n\tif !addr.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tnewBalance, ok := overflow.Subu256(led.balanceOf(addr), amount)\n\tif !ok {\n\t\treturn ErrInsufficientBalance\n\t}\n\n\tled.totalSupply = overflow.Subu256p(led.totalSupply, amount)\n\tled.setBalance(addr, newBalance)\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.FormatUint256(amount, 10),\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) uint256 {\n\tbalance := led.balances.Get(addr.String())\n\tif balance == nil {\n\t\treturn 0\n\t}\n\treturn balance.(uint256)\n}\n\n// setBalance stores balance for addr, removing the entry when it is zero so\n// that an absent key and a zero balance are the same state.\nfunc (led *PrivateLedger) setBalance(addr address, balance uint256) {\n\tif balance == 0 {\n\t\tled.balances.Remove(addr.String())\n\t} else {\n\t\tled.balances.Set(addr.String(), balance)\n\t}\n}\n\n// allowance returns the allowance of the specified owner and spender.\nfunc (led PrivateLedger) allowance(owner, spender address) uint256 {\n\tallowance := led.allowances.Get(allowanceKey(owner, spender))\n\tif allowance == nil {\n\t\treturn 0\n\t}\n\treturn allowance.(uint256)\n}\n\n// setAllowance stores allowance for owner and spender, removing the entry when\n// it is zero, like setBalance.\nfunc (led *PrivateLedger) setAllowance(owner, spender address, allowance uint256) {\n\tkey := allowanceKey(owner, spender)\n\tif allowance == 0 {\n\t\tled.allowances.Remove(key)\n\t} else {\n\t\tled.allowances.Set(key, allowance)\n\t}\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":"token_identity_filetest.gno","body":"// PKGPATH: gno.land/r/demo/grc20identity\n\npackage grc20identity\n\nimport (\n\t\"chain\"\n\n\t\"gno.land/p/nt/grc20/v1\"\n\t\"gno.land/p/nt/seqid/v0\"\n)\n\nvar nextTokenID seqid.ID\n\nfunc main(cur realm) {\n\tfirst, firstLedger := newToken(cur)\n\tsecond, secondLedger := newToken(cur)\n\n\tif first.ID() != \"gno.land/r/demo/grc20identity.DUP.0000001\" {\n\t\tpanic(\"unexpected first token ID: \" + first.ID())\n\t}\n\tif second.ID() != \"gno.land/r/demo/grc20identity.DUP.0000002\" {\n\t\tpanic(\"unexpected second token ID: \" + second.ID())\n\t}\n\n\tholder := chain.PackageAddress(\"holder\")\n\tspender := chain.PackageAddress(\"spender\")\n\tfirstLedger.Mint(holder, 1)\n\tfirstLedger.Approve(holder, spender, 1)\n\tsecondLedger.Mint(holder, 2)\n\tsecondLedger.Approve(holder, spender, 2)\n}\n\nfunc newToken(cur realm) (*grc20.Token, *grc20.PrivateLedger) {\n\treturn grc20.NewToken(\"Same\", \"DUP\", 6, nextTokenID.Next(), cur)\n}\n\n// Events:\n// [\n//   {\n//     \"type\": \"NewToken\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20identity.DUP.0000001\"\n//       },\n//       {\n//         \"key\": \"name\",\n//         \"value\": \"Same\"\n//       },\n//       {\n//         \"key\": \"symbol\",\n//         \"value\": \"DUP\"\n//       },\n//       {\n//         \"key\": \"decimals\",\n//         \"value\": \"6\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v1\"\n//   },\n//   {\n//     \"type\": \"NewToken\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20identity.DUP.0000002\"\n//       },\n//       {\n//         \"key\": \"name\",\n//         \"value\": \"Same\"\n//       },\n//       {\n//         \"key\": \"symbol\",\n//         \"value\": \"DUP\"\n//       },\n//       {\n//         \"key\": \"decimals\",\n//         \"value\": \"6\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v1\"\n//   },\n//   {\n//     \"type\": \"Transfer\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20identity.DUP.0000001\"\n//       },\n//       {\n//         \"key\": \"from\",\n//         \"value\": \"\"\n//       },\n//       {\n//         \"key\": \"to\",\n//         \"value\": \"g18rwjsdf0kcy673ta0q4ujwvsncz4k2a8gcv94t\"\n//       },\n//       {\n//         \"key\": \"value\",\n//         \"value\": \"1\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v1\"\n//   },\n//   {\n//     \"type\": \"Approval\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20identity.DUP.0000001\"\n//       },\n//       {\n//         \"key\": \"owner\",\n//         \"value\": \"g18rwjsdf0kcy673ta0q4ujwvsncz4k2a8gcv94t\"\n//       },\n//       {\n//         \"key\": \"spender\",\n//         \"value\": \"g148tp8xkvvk3l73lmxywpdlsxgdujjlclx7wfyg\"\n//       },\n//       {\n//         \"key\": \"value\",\n//         \"value\": \"1\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v1\"\n//   },\n//   {\n//     \"type\": \"Transfer\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20identity.DUP.0000002\"\n//       },\n//       {\n//         \"key\": \"from\",\n//         \"value\": \"\"\n//       },\n//       {\n//         \"key\": \"to\",\n//         \"value\": \"g18rwjsdf0kcy673ta0q4ujwvsncz4k2a8gcv94t\"\n//       },\n//       {\n//         \"key\": \"value\",\n//         \"value\": \"2\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v1\"\n//   },\n//   {\n//     \"type\": \"Approval\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20identity.DUP.0000002\"\n//       },\n//       {\n//         \"key\": \"owner\",\n//         \"value\": \"g18rwjsdf0kcy673ta0q4ujwvsncz4k2a8gcv94t\"\n//       },\n//       {\n//         \"key\": \"spender\",\n//         \"value\": \"g148tp8xkvvk3l73lmxywpdlsxgdujjlclx7wfyg\"\n//       },\n//       {\n//         \"key\": \"value\",\n//         \"value\": \"2\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v1\"\n//   }\n// ]\n"},{"name":"token_test.gno","body":"package grc20\n\nimport (\n\t\"errors\"\n\t\"math\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\n// newTestToken constructs a Token. The IIFE same-realm cross\n// promotes the test's EOA-origin cur into a fresh /p/grc20\n// CodeRealm cur so NewToken's IsCurrent + non-empty rlm.PkgPath()\n// checks pass.\nfunc newTestToken(name, symbol string, decimals int, id seqid.ID, rlm realm) (tok *Token, adm *PrivateLedger) {\n\tfunc(cur realm) {\n\t\ttok, adm = NewToken(name, symbol, decimals, id, cur)\n\t}(cross(rlm))\n\treturn\n}\n\nfunc TestTestImpl(cur realm, t *testing.T) {\n\tbank, _ := newTestToken(\"Dummy\", \"DUMMY\", 4, 0, cur)\n\turequire.False(t, bank == nil, \"dummy should not be nil\")\n}\n\nfunc TestNewTokenAllowsDuplicateSymbolInSameRealm(cur realm, t *testing.T) {\n\tholder := testutils.TestAddress(\"holder\")\n\n\tfirst, firstLedger := newTestToken(\"Same\", \"DUP\", 6, 1, cur)\n\tsecond, secondLedger := newTestToken(\"Same\", \"DUP\", 6, 2, cur)\n\n\turequire.True(t, first.ID() != second.ID(), \"duplicate symbols should not force duplicate IDs\")\n\n\turequire.NoError(t, firstLedger.Mint(holder, 11))\n\turequire.NoError(t, secondLedger.Mint(holder, 22))\n\trequire256(t, uint256(11), first.BalanceOf(holder), \"first balance\")\n\trequire256(t, uint256(22), second.BalanceOf(holder), \"second balance\")\n}\n\nfunc TestNewTokenValidation(cur realm, t *testing.T) {\n\t// Wrap NewToken to satisfy IsCurrent and pkgPath requirements,\n\t// then validate name, symbol, and decimals.\n\tmustPanic := func(name, sym string, dec int, want error, label string) {\n\t\tt.Helper()\n\t\t// newTestToken wraps NewToken in cross(...), so the panic from\n\t\t// NewToken's validators crosses a realm boundary — use revive()\n\t\t// (defer-recover doesn't see cross-realm panics).\n\t\tr := revive(func() {\n\t\t\tnewTestToken(name, sym, dec, 0, cur)\n\t\t})\n\t\tif r == nil {\n\t\t\tt.Errorf(\"%s: expected panic, got none\", label)\n\t\t\treturn\n\t\t}\n\t\tif r != want {\n\t\t\tt.Errorf(\"%s: expected %v, got %v\", label, want, r)\n\t\t}\n\t}\n\n\t// Empty name / symbol.\n\tmustPanic(\"\", \"OK\", 4, ErrInvalidName, \"empty name\")\n\tmustPanic(\"Name\", \"\", 4, ErrInvalidSymbol, \"empty symbol\")\n\n\t// Length caps.\n\tmustPanic(strings.Repeat(\"a\", MaxNameLen+1), \"OK\", 4, ErrInvalidName, \"name too long\")\n\tmustPanic(\"Name\", strings.Repeat(\"A\", MaxSymbolLen+1), 4, ErrInvalidSymbol, \"symbol too long\")\n\n\t// Name control characters.\n\tmustPanic(\"bad\\x01name\", \"OK\", 4, ErrInvalidName, \"name with control char\")\n\tmustPanic(\"bad\\nname\", \"OK\", 4, ErrInvalidName, \"name with newline\")\n\n\t// Symbol charset — disallowed delimiters and whitespace.\n\tmustPanic(\"Name\", \"BA.D\", 4, ErrInvalidSymbol, \"symbol with dot\")\n\tmustPanic(\"Name\", \"BA/D\", 4, ErrInvalidSymbol, \"symbol with slash\")\n\tmustPanic(\"Name\", \"BA D\", 4, ErrInvalidSymbol, \"symbol with space\")\n\tmustPanic(\"Name\", \"BA\\\"D\", 4, ErrInvalidSymbol, \"symbol with quote\")\n\n\t// Decimals out of range.\n\tmustPanic(\"Name\", \"OK\", -1, ErrInvalidDecimals, \"negative decimals\")\n\tmustPanic(\"Name\", \"OK\", MaxDecimals+1, ErrInvalidDecimals, \"decimals over cap\")\n\n\t// Boundary positives — should NOT panic.\n\ttok, _ := newTestToken(strings.Repeat(\"a\", MaxNameLen), strings.Repeat(\"A\", MaxSymbolLen), MaxDecimals, 0, cur)\n\turequire.True(t, tok != nil, \"boundary name+symbol+decimals should succeed\")\n\n\t// UTF-8 name with non-ASCII is allowed.\n\ttok2, _ := newTestToken(\"Доллар\", \"RUB\", 2, 0, cur)\n\turequire.True(t, tok2 != nil, \"UTF-8 name should be allowed\")\n}\n\nfunc TestToken(cur realm, t *testing.T) {\n\tvar (\n\t\talice = testutils.TestAddress(\"alice\")\n\t\tbob   = testutils.TestAddress(\"bob\")\n\t\tcarl  = testutils.TestAddress(\"carl\")\n\t)\n\n\tbank, adm := newTestToken(\"Dummy\", \"DUMMY\", 6, 0, cur)\n\n\tcheckBalances := func(aliceEB, bobEB, carlEB uint256) {\n\t\tt.Helper()\n\t\texp := fmtAmounts(\"alice\", aliceEB, \"bob\", bobEB, \"carl\", carlEB)\n\t\tgot := fmtAmounts(\"alice\", bank.BalanceOf(alice), \"bob\", bank.BalanceOf(bob), \"carl\", bank.BalanceOf(carl))\n\t\tuassert.Equal(t, got, exp, \"invalid balances\")\n\t}\n\tcheckAllowances := func(abEB, acEB, baEB, bcEB, caEB, cbEB uint256) {\n\t\tt.Helper()\n\t\texp := fmtAmounts(\"ab\", abEB, \"ac\", acEB, \"ba\", baEB, \"bc\", bcEB, \"ca\", caEB, \"cb\", cbEB)\n\t\tgot := fmtAmounts(\n\t\t\t\"ab\", bank.Allowance(alice, bob), \"ac\", bank.Allowance(alice, carl),\n\t\t\t\"ba\", bank.Allowance(bob, alice), \"bc\", bank.Allowance(bob, carl),\n\t\t\t\"ca\", bank.Allowance(carl, alice), \"cb\", bank.Allowance(carl, bob),\n\t\t)\n\t\tuassert.Equal(t, got, exp, \"invalid allowances\")\n\t}\n\n\tcheckBalances(0, 0, 0)\n\tcheckAllowances(0, 0, 0, 0, 0, 0)\n\n\turequire.NoError(t, adm.Mint(alice, 1000))\n\turequire.NoError(t, adm.Mint(alice, 100))\n\tcheckBalances(1100, 0, 0)\n\tcheckAllowances(0, 0, 0, 0, 0, 0)\n\n\turequire.NoError(t, adm.Approve(alice, bob, 99999999))\n\tcheckBalances(1100, 0, 0)\n\tcheckAllowances(99999999, 0, 0, 0, 0, 0)\n\n\turequire.NoError(t, adm.Approve(alice, bob, 400))\n\tcheckBalances(1100, 0, 0)\n\tcheckAllowances(400, 0, 0, 0, 0, 0)\n\n\turequire.Error(t, adm.TransferFrom(alice, bob, carl, 100000000))\n\tcheckBalances(1100, 0, 0)\n\tcheckAllowances(400, 0, 0, 0, 0, 0)\n\n\turequire.NoError(t, adm.TransferFrom(alice, bob, carl, 100))\n\tcheckBalances(1000, 0, 100)\n\tcheckAllowances(300, 0, 0, 0, 0, 0)\n\n\turequire.Error(t, adm.SpendAllowance(alice, bob, 2000000))\n\tcheckBalances(1000, 0, 100)\n\tcheckAllowances(300, 0, 0, 0, 0, 0)\n\n\turequire.NoError(t, adm.SpendAllowance(alice, bob, 100))\n\tcheckBalances(1000, 0, 100)\n\tcheckAllowances(200, 0, 0, 0, 0, 0)\n}\n\nfunc TestMintOverflow(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\ttok, adm := newTestToken(\"Dummy\", \"DUMMY\", 6, 0, cur)\n\n\tsafeValue := uint256(1 \u003c\u003c 255)\n\turequire.NoError(t, adm.Mint(alice, safeValue))\n\trequire256(t, uint256(safeValue), tok.BalanceOf(alice), \"alice balance\")\n\n\terr := adm.Mint(bob, safeValue)\n\tuassert.True(t, errors.Is(err, ErrMintOverflow), \"expected ErrMintOverflow\")\n\trequire256(t, uint256(safeValue), tok.TotalSupply(), \"supply unchanged by a refused mint\")\n\trequire256(t, uint256(0), tok.BalanceOf(bob), \"bob unchanged by a refused mint\")\n}\n\nfunc TestTransferFromAtomicity(cur realm, t *testing.T) {\n\tvar (\n\t\towner   = testutils.TestAddress(\"owner\")\n\t\tspender = testutils.TestAddress(\"spender\")\n\n\t\tinvalidRecipient = address(\"\")\n\t\trecipient        = testutils.TestAddress(\"to\")\n\t)\n\n\ttoken, admin := newTestToken(\"Test\", \"TEST\", 6, 0, cur)\n\n\t// owner has 100 tokens, spender has 50 allowance\n\tinitialBalance := uint256(100)\n\tinitialAllowance := uint256(50)\n\n\turequire.NoError(t, admin.Mint(owner, initialBalance))\n\turequire.NoError(t, admin.Approve(owner, spender, initialAllowance))\n\n\t// transfer to an invalid address to force a transfer failure\n\ttransferAmount := uint256(30)\n\terr := admin.TransferFrom(owner, spender, invalidRecipient, transferAmount)\n\tuassert.Error(t, err, \"transfer should fail due to invalid address\")\n\n\townerBalance := token.BalanceOf(owner)\n\trequire256(t, uint256(initialBalance), ownerBalance, \"owner balance should remain unchanged\")\n\n\t// check if allowance was incorrectly reduced\n\tremainingAllowance := token.Allowance(owner, spender)\n\trequire256(t, uint256(initialAllowance), remainingAllowance, \"allowance should not be reduced when transfer fails\")\n\n\t// Self-transfers fail in Transfer after SpendAllowance. TransferFrom must\n\t// reject them before it can mutate the allowance.\n\terr = admin.TransferFrom(owner, spender, owner, transferAmount)\n\tuassert.True(t, errors.Is(err, ErrCannotTransferToSelf), \"self-transfer should fail\")\n\trequire256(t, uint256(initialBalance), token.BalanceOf(owner), \"owner balance should not change when self-transfer fails\")\n\trequire256(t, uint256(initialAllowance), token.Allowance(owner, spender), \"allowance should not be reduced when self-transfer fails\")\n\n\t// transfer all tokens\n\tadmin.Transfer(owner, recipient, 100)\n\tremainingBalance := token.BalanceOf(owner)\n\trequire256(t, uint256(0), remainingBalance, \"balance should be zero\")\n\tuassert.False(t, token.HasAddr(owner), \"a zero balance is not stored\")\n\n\terr = admin.TransferFrom(owner, spender, recipient, transferAmount)\n\tuassert.Error(t, err, \"transfer should fail due to insufficient balance\")\n}\n\nfunc TestMintUntilOverflow(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\ttok, adm := newTestToken(\"Dummy\", \"DUMMY\", 6, 0, cur)\n\n\ttests := []struct {\n\t\tname           string\n\t\taddr           address\n\t\tamount         uint256\n\t\texpectedError  error\n\t\texpectedSupply uint256\n\t\tdescription    string\n\t}{\n\t\t{\n\t\t\tname:           \"mint zero\",\n\t\t\taddr:           alice,\n\t\t\tamount:         0,\n\t\t\texpectedError:  nil,\n\t\t\texpectedSupply: 0,\n\t\t\tdescription:    \"minting zero is a no-op\",\n\t\t},\n\t\t{\n\t\t\tname:           \"mint almost MaxUint256\",\n\t\t\taddr:           alice,\n\t\t\tamount:         math.MaxUint256 - 1000,\n\t\t\texpectedError:  nil,\n\t\t\texpectedSupply: math.MaxUint256 - 1000,\n\t\t\tdescription:    \"minting almost MaxUint256 should succeed\",\n\t\t},\n\t\t{\n\t\t\tname:           \"mint value that would exceed MaxUint256\",\n\t\t\taddr:           bob,\n\t\t\tamount:         1001,\n\t\t\texpectedError:  ErrMintOverflow,\n\t\t\texpectedSupply: math.MaxUint256 - 1000,\n\t\t\tdescription:    \"minting past MaxUint256 should fail with ErrMintOverflow\",\n\t\t},\n\t\t{\n\t\t\tname:           \"mint small value\",\n\t\t\taddr:           bob,\n\t\t\tamount:         1000,\n\t\t\texpectedError:  nil,\n\t\t\texpectedSupply: math.MaxUint256,\n\t\t\tdescription:    \"minting up to exactly MaxUint256 should succeed\",\n\t\t},\n\t\t{\n\t\t\tname:           \"mint one past MaxUint256\",\n\t\t\taddr:           bob,\n\t\t\tamount:         1,\n\t\t\texpectedError:  ErrMintOverflow,\n\t\t\texpectedSupply: math.MaxUint256,\n\t\t\tdescription:    \"minting any value when at MaxUint256 should fail with ErrMintOverflow\",\n\t\t},\n\t\t{\n\t\t\tname:           \"mint MaxUint256 at MaxUint256\",\n\t\t\taddr:           bob,\n\t\t\tamount:         math.MaxUint256,\n\t\t\texpectedError:  ErrMintOverflow,\n\t\t\texpectedSupply: math.MaxUint256,\n\t\t\tdescription:    \"an amount that would wrap the supply to a small value must be refused\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := adm.Mint(tt.addr, tt.amount)\n\n\t\t\tif tt.expectedError != nil {\n\t\t\t\tuassert.Error(t, err, tt.description)\n\t\t\t\tif !errors.Is(err, tt.expectedError) {\n\t\t\t\t\tt.Errorf(\"expected error %v, got %v\", tt.expectedError, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tuassert.NoError(t, err, tt.description)\n\t\t\t}\n\n\t\t\trequire256(t, uint256(tt.expectedSupply), tok.TotalSupply(), \"totalSupply should match expected value\")\n\t\t})\n\t}\n\n\trequire256(t, uint256(math.MaxUint256-1000), tok.BalanceOf(alice), \"alice balance\")\n\trequire256(t, uint256(1000), tok.BalanceOf(bob), \"bob balance\")\n}\n\n// TestMaxSupplyMovements moves the whole MaxUint256 supply around: every\n// balance and the total stay exact at the top of the range.\nfunc TestMaxSupplyMovements(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\tcarl := testutils.TestAddress(\"carl\")\n\ttok, adm := newTestToken(\"Dummy\", \"DUMMY\", 18, 0, cur)\n\n\turequire.NoError(t, adm.Mint(alice, math.MaxUint256))\n\trequire256(t, uint256(math.MaxUint256), tok.TotalSupply(), \"supply\")\n\n\t// Transfer everything but one.\n\turequire.NoError(t, adm.Transfer(alice, bob, math.MaxUint256-1))\n\trequire256(t, uint256(1), tok.BalanceOf(alice), \"alice\")\n\trequire256(t, uint256(math.MaxUint256-1), tok.BalanceOf(bob), \"bob\")\n\n\t// Overdraft by one is refused and changes nothing.\n\terr := adm.Transfer(alice, bob, 2)\n\tuassert.True(t, errors.Is(err, ErrInsufficientBalance), \"overdraft must be refused\")\n\trequire256(t, uint256(1), tok.BalanceOf(alice), \"alice after refused overdraft\")\n\n\t// The last unit lands on bob, who now holds MaxUint256.\n\turequire.NoError(t, adm.Transfer(alice, bob, 1))\n\trequire256(t, uint256(math.MaxUint256), tok.BalanceOf(bob), \"bob holds everything\")\n\tuassert.False(t, tok.HasAddr(alice), \"alice's zero balance is removed\")\n\tuassert.Equal(t, 1, tok.KnownAccounts())\n\n\t// A transfer above MaxUint256 - balance is refused rather than wrapped.\n\terr = adm.Transfer(bob, carl, math.MaxUint256)\n\turequire.NoError(t, err)\n\terr = adm.Transfer(bob, carl, 1)\n\tuassert.True(t, errors.Is(err, ErrInsufficientBalance), \"empty account must not underflow\")\n\n\t// Burn everything.\n\turequire.NoError(t, adm.Burn(carl, math.MaxUint256))\n\trequire256(t, uint256(0), tok.TotalSupply(), \"supply after burn\")\n\tuassert.Equal(t, 0, tok.KnownAccounts())\n\n\terr = adm.Burn(carl, 1)\n\tuassert.True(t, errors.Is(err, ErrInsufficientBalance), \"burning from zero must be refused\")\n}\n\n// TestAbove2Pow63 moves amounts no int64 token could hold.\nfunc TestAbove2Pow63(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\tcarl := testutils.TestAddress(\"carl\")\n\ttok, adm := newTestToken(\"Dummy\", \"DUMMY\", 18, 0, cur)\n\n\tbig := uint256(1\u003c\u003c63) + 7 // 9223372036854775815\n\turequire.NoError(t, adm.Mint(alice, big*3))\n\trequire256(t, uint256(big*3), tok.TotalSupply(), \"supply\")\n\n\turequire.NoError(t, adm.Transfer(alice, bob, big))\n\trequire256(t, uint256(big*2), tok.BalanceOf(alice), \"alice\")\n\trequire256(t, uint256(big), tok.BalanceOf(bob), \"bob\")\n\n\turequire.NoError(t, adm.Approve(alice, carl, big))\n\turequire.NoError(t, adm.TransferFrom(alice, carl, carl, big))\n\trequire256(t, uint256(big), tok.BalanceOf(alice), \"alice after TransferFrom\")\n\trequire256(t, uint256(big), tok.BalanceOf(carl), \"carl\")\n\trequire256(t, uint256(0), tok.Allowance(alice, carl), \"allowance used up\")\n\n\tuassert.Equal(t, \"9223372036854775815\", strconv.FormatUint256(tok.BalanceOf(bob), 10))\n}\n\n// TestAllowanceNearMax exercises an \"infinite\" (MaxUint256) approval and\n// spending against it.\nfunc TestAllowanceNearMax(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\tcarl := testutils.TestAddress(\"carl\")\n\ttok, adm := newTestToken(\"Dummy\", \"DUMMY\", 18, 0, cur)\n\n\turequire.NoError(t, adm.Mint(alice, 1000))\n\turequire.NoError(t, adm.Approve(alice, bob, math.MaxUint256))\n\trequire256(t, uint256(math.MaxUint256), tok.Allowance(alice, bob), \"max allowance\")\n\n\t// Spending decreases even a max allowance (no \"infinite\" special case).\n\turequire.NoError(t, adm.TransferFrom(alice, bob, carl, 400))\n\trequire256(t, uint256(math.MaxUint256-400), tok.Allowance(alice, bob), \"allowance after spend\")\n\n\t// Spending more than the allowance is refused without wrapping.\n\turequire.NoError(t, adm.Approve(alice, bob, 10))\n\terr := adm.SpendAllowance(alice, bob, 11)\n\tuassert.True(t, errors.Is(err, ErrInsufficientAllowance), \"over-allowance must be refused\")\n\trequire256(t, uint256(10), tok.Allowance(alice, bob), \"allowance unchanged\")\n\n\t// A max-size spend against a max allowance leaves zero, which is removed.\n\turequire.NoError(t, adm.Approve(alice, bob, math.MaxUint256))\n\turequire.NoError(t, adm.SpendAllowance(alice, bob, math.MaxUint256))\n\trequire256(t, uint256(0), tok.Allowance(alice, bob), \"allowance spent\")\n\n\t// Approving zero clears an allowance.\n\turequire.NoError(t, adm.Approve(alice, bob, 5))\n\turequire.NoError(t, adm.Approve(alice, bob, 0))\n\trequire256(t, uint256(0), tok.Allowance(alice, bob), \"allowance cleared\")\n\n\t// TransferFrom with a max allowance but insufficient balance is refused\n\t// before the allowance is touched.\n\turequire.NoError(t, adm.Approve(alice, bob, math.MaxUint256))\n\terr = adm.TransferFrom(alice, bob, carl, 601)\n\tuassert.True(t, errors.Is(err, ErrInsufficientBalance), \"insufficient balance\")\n\trequire256(t, uint256(math.MaxUint256), tok.Allowance(alice, bob), \"allowance untouched\")\n}\n\nfunc TestFormatAmount(t *testing.T) {\n\tcases := []struct {\n\t\tamount   uint256\n\t\tdecimals int\n\t\twant     string\n\t}{\n\t\t{0, 0, \"0\"},\n\t\t{0, 4, \"0.0000\"},\n\t\t{12, 4, \"0.0012\"},\n\t\t{12345, 4, \"1.2345\"},\n\t\t{10000, 4, \"1.0000\"},\n\t\t{math.MaxUint256, 0, \"115792089237316195423570985008687907853269984665640564039457584007913129639935\"},\n\t\t{math.MaxUint256, 18, \"115792089237316195423570985008687907853269984665640564039457.584007913129639935\"},\n\t}\n\tfor _, c := range cases {\n\t\tuassert.Equal(t, c.want, FormatAmount(c.amount, c.decimals))\n\t}\n}\n\nfunc TestRenderHomeMaxSupply(cur realm, t *testing.T) {\n\ttok, adm := newTestToken(\"Dummy\", \"DUMMY\", 6, 0, cur)\n\turequire.NoError(t, adm.Mint(testutils.TestAddress(\"alice\"), math.MaxUint256))\n\texp := \"# Dummy ($DUMMY)\\n\\n\" +\n\t\t\"* **Decimals**: 6\\n\" +\n\t\t\"* **Total supply**: 115792089237316195423570985008687907853269984665640564039457584007913129639935\\n\" +\n\t\t\"* **Known accounts**: 1\\n\"\n\tuassert.Equal(t, exp, tok.RenderHome())\n}\n\n// A token name is display text and may contain markdown. RenderHome must not\n// turn it into a live link (or any other markup) on the token's page.\nfunc TestRenderHomeEscapesName(cur realm, t *testing.T) {\n\tconst evil = \"[Claim reward](https://evil.example)\"\n\ttok, _ := newTestToken(evil, \"EVIL\", 6, 0, cur)\n\tout := tok.RenderHome()\n\tuassert.False(t, strings.Contains(out, evil), \"name rendered as a live link: \"+out)\n\tuassert.True(t, strings.Contains(out, `\\[Claim reward\\]\\(https://evil\\.example\\)`), \"name not escaped: \"+out)\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() uint256\n\n\t// Returns the amount of tokens owned by `account`.\n\tBalanceOf(account address) uint256\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 uint256) 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) uint256\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 uint256) 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 uint256) 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\t// origRealm is the PkgPath of the realm that created the token, captured\n\t// unforgeably in NewToken. A frame-relative teller only works there.\n\torigRealm string\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. It never exceeds\n\t// math.MaxUint256: Mint refuses any amount that would wrap it.\n\ttotalSupply uint256\n\t// chain.Address -\u003e uint256 (absent means zero; zero balances are removed)\n\tbalances avl.Tree\n\t// owner.(chain.Address)+\":\"+spender.(chain.Address)) -\u003e uint256\n\t// (absent means zero; zero allowances are removed)\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\tErrSpoofedRealm          = errors.New(\"rlm does not match the current crossing frame\")\n\tErrForeignCallerTeller   = errors.New(\"caller teller is confined to the token's own realm\")\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// homeGuard marks a frame-relative teller (PrivateLedger.CallerTeller),\n\t// whose actor is resolved from the invoking frame. Such a teller is only\n\t// meaningful in the token's own realm and is refused anywhere else, so\n\t// exporting the value cannot hand out a spend capability.\n\thomeGuard bool\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"},{"name":"uint256_events_filetest.gno","body":"// PKGPATH: gno.land/r/demo/grc20u256events\n\n// Events carry amounts as canonical decimal strings, including amounts no\n// int64 can hold. A refused mint (past MaxUint256) emits nothing.\npackage grc20u256events\n\nimport (\n\t\"chain\"\n\t\"math\"\n\n\t\"gno.land/p/nt/grc20/v1\"\n)\n\nvar (\n\ttoken  *grc20.Token\n\tledger *grc20.PrivateLedger\n)\n\nfunc init(cur realm) {\n\ttoken, ledger = grc20.NewToken(\"Big\", \"BIG\", 18, 0, cur)\n}\n\nfunc main(cur realm) {\n\talice := chain.PackageAddress(\"alice\")\n\tbob := chain.PackageAddress(\"bob\")\n\n\tif err := ledger.Mint(alice, math.MaxUint256); err != nil {\n\t\tpanic(err)\n\t}\n\tif err := ledger.Mint(bob, 1); err != grc20.ErrMintOverflow {\n\t\tpanic(\"expected ErrMintOverflow\")\n\t}\n\tif err := ledger.Transfer(alice, bob, 1\u003c\u003c64); err != nil {\n\t\tpanic(err)\n\t}\n\tif err := ledger.Approve(bob, alice, math.MaxUint256); err != nil {\n\t\tpanic(err)\n\t}\n\tprintln(token.FormatAmount(token.BalanceOf(bob)))\n\tprintln(token.TotalSupply() == math.MaxUint256)\n}\n\n// Output:\n// 18.446744073709551616\n// true\n\n// Events:\n// [\n//   {\n//     \"type\": \"NewToken\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20u256events.BIG.0000000\"\n//       },\n//       {\n//         \"key\": \"name\",\n//         \"value\": \"Big\"\n//       },\n//       {\n//         \"key\": \"symbol\",\n//         \"value\": \"BIG\"\n//       },\n//       {\n//         \"key\": \"decimals\",\n//         \"value\": \"18\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v1\"\n//   },\n//   {\n//     \"type\": \"Transfer\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20u256events.BIG.0000000\"\n//       },\n//       {\n//         \"key\": \"from\",\n//         \"value\": \"\"\n//       },\n//       {\n//         \"key\": \"to\",\n//         \"value\": \"g1vxf6grxsp469xvdqnrkgjjrqjjydmve3kfs04e\"\n//       },\n//       {\n//         \"key\": \"value\",\n//         \"value\": \"115792089237316195423570985008687907853269984665640564039457584007913129639935\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v1\"\n//   },\n//   {\n//     \"type\": \"Transfer\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20u256events.BIG.0000000\"\n//       },\n//       {\n//         \"key\": \"from\",\n//         \"value\": \"g1vxf6grxsp469xvdqnrkgjjrqjjydmve3kfs04e\"\n//       },\n//       {\n//         \"key\": \"to\",\n//         \"value\": \"g1zqwap9sxm4zcvt9vj5tll4uhtrtwxjhe5789f8\"\n//       },\n//       {\n//         \"key\": \"value\",\n//         \"value\": \"18446744073709551616\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v1\"\n//   },\n//   {\n//     \"type\": \"Approval\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20u256events.BIG.0000000\"\n//       },\n//       {\n//         \"key\": \"owner\",\n//         \"value\": \"g1zqwap9sxm4zcvt9vj5tll4uhtrtwxjhe5789f8\"\n//       },\n//       {\n//         \"key\": \"spender\",\n//         \"value\": \"g1vxf6grxsp469xvdqnrkgjjrqjjydmve3kfs04e\"\n//       },\n//       {\n//         \"key\": \"value\",\n//         \"value\": \"115792089237316195423570985008687907853269984665640564039457584007913129639935\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v1\"\n//   }\n// ]\n"},{"name":"zero_amounts_filetest.gno","body":"// PKGPATH: gno.land/r/demo/grc20zero\n\n// Zero-amount operations succeed and emit their events, like v0, but store\n// nothing: an absent key and a zero balance or allowance are the same state,\n// so KnownAccounts and the render do not count the touched accounts.\npackage grc20zero\n\nimport (\n\t\"chain\"\n\n\t\"gno.land/p/nt/grc20/v1\"\n)\n\nfunc main(cur realm) {\n\ttoken, ledger := grc20.NewToken(\"Zero\", \"ZERO\", 0, 0, cur)\n\talice := chain.PackageAddress(\"alice\")\n\tbob := chain.PackageAddress(\"bob\")\n\n\tcheck := func(err error) {\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\tcheck(ledger.Mint(alice, 0))\n\tcheck(ledger.Transfer(alice, bob, 0))\n\tcheck(ledger.Approve(alice, bob, 0))\n\tcheck(ledger.SpendAllowance(alice, bob, 0))\n\n\tprintln(\"alice known:\", token.HasAddr(alice))\n\tprintln(\"bob known:\", token.HasAddr(bob))\n\tprintln(\"known accounts:\", token.KnownAccounts())\n\tprintln(\"allowance:\", token.Allowance(alice, bob))\n\tprint(token.RenderHome())\n}\n\n// Output:\n// alice known: false\n// bob known: false\n// known accounts: 0\n// allowance: 0\n// # Zero ($ZERO)\n//\n// * **Decimals**: 0\n// * **Total supply**: 0\n// * **Known accounts**: 0\n\n// Events:\n// [\n//   {\n//     \"type\": \"NewToken\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20zero.ZERO.0000000\"\n//       },\n//       {\n//         \"key\": \"name\",\n//         \"value\": \"Zero\"\n//       },\n//       {\n//         \"key\": \"symbol\",\n//         \"value\": \"ZERO\"\n//       },\n//       {\n//         \"key\": \"decimals\",\n//         \"value\": \"0\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v1\"\n//   },\n//   {\n//     \"type\": \"Transfer\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20zero.ZERO.0000000\"\n//       },\n//       {\n//         \"key\": \"from\",\n//         \"value\": \"\"\n//       },\n//       {\n//         \"key\": \"to\",\n//         \"value\": \"g1vxf6grxsp469xvdqnrkgjjrqjjydmve3kfs04e\"\n//       },\n//       {\n//         \"key\": \"value\",\n//         \"value\": \"0\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v1\"\n//   },\n//   {\n//     \"type\": \"Transfer\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20zero.ZERO.0000000\"\n//       },\n//       {\n//         \"key\": \"from\",\n//         \"value\": \"g1vxf6grxsp469xvdqnrkgjjrqjjydmve3kfs04e\"\n//       },\n//       {\n//         \"key\": \"to\",\n//         \"value\": \"g1zqwap9sxm4zcvt9vj5tll4uhtrtwxjhe5789f8\"\n//       },\n//       {\n//         \"key\": \"value\",\n//         \"value\": \"0\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v1\"\n//   },\n//   {\n//     \"type\": \"Approval\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20zero.ZERO.0000000\"\n//       },\n//       {\n//         \"key\": \"owner\",\n//         \"value\": \"g1vxf6grxsp469xvdqnrkgjjrqjjydmve3kfs04e\"\n//       },\n//       {\n//         \"key\": \"spender\",\n//         \"value\": \"g1zqwap9sxm4zcvt9vj5tll4uhtrtwxjhe5789f8\"\n//       },\n//       {\n//         \"key\": \"value\",\n//         \"value\": \"0\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v1\"\n//   }\n// ]\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"Y1NGi71ffeGNAZcRsKZwHNKVV5HgaepEkgh2Fouo//9JhTCrK0e/U5bcmi/AZdQFKngh0dl0fz7Do+z4mO+MZg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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":"request_test.gno","body":"package mux\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nfunc TestRequest_GetVar(t *testing.T) {\n\tcases := []struct {\n\t\thandlerPath    string\n\t\treqPath        string\n\t\tgetVarKey      string\n\t\texpectedOutput string\n\t}{\n\n\t\t{\"users/{id}\", \"users/123\", \"id\", \"123\"},\n\t\t{\"users/123\", \"users/123\", \"id\", \"\"},\n\t\t{\"users/{id}\", \"users/123\", \"nonexistent\", \"\"},\n\t\t{\"users/{userId}/posts/{postId}\", \"users/123/posts/456\", \"userId\", \"123\"},\n\t\t{\"users/{userId}/posts/{postId}\", \"users/123/posts/456\", \"postId\", \"456\"},\n\n\t\t// Wildcards\n\t\t{\"*\", \"users/123\", \"*\", \"users/123\"},\n\t\t{\"*\", \"users/123/posts/456\", \"*\", \"users/123/posts/456\"},\n\t\t{\"*\", \"users/123/posts/456/comments/789\", \"*\", \"users/123/posts/456/comments/789\"},\n\t\t{\"users/*\", \"users/john/posts\", \"*\", \"john/posts\"},\n\t\t{\"users/*/comments\", \"users/jane/comments\", \"*\", \"jane/comments\"},\n\t\t{\"api/*/posts/*\", \"api/v1/posts/123\", \"*\", \"v1/posts/123\"},\n\n\t\t// wildcards and parameters\n\t\t{\"api/{version}/*\", \"api/v1/user/settings\", \"version\", \"v1\"},\n\t}\n\tfor _, tt := range cases {\n\t\tname := ufmt.Sprintf(\"%s-%s\", tt.handlerPath, tt.reqPath)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\treq := \u0026Request{\n\t\t\t\tHandlerPath: tt.handlerPath,\n\t\t\t\tPath:        tt.reqPath,\n\t\t\t}\n\t\t\toutput := req.GetVar(tt.getVarKey)\n\t\t\tuassert.Equal(t, tt.expectedOutput, output,\n\t\t\t\t\"handler: %q, path: %q, key: %q\",\n\t\t\t\ttt.handlerPath, tt.reqPath, tt.getVarKey)\n\t\t})\n\t}\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"},{"name":"router_test.gno","body":"package mux\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nfunc TestRouter_Render(t *testing.T) {\n\tcases := []struct {\n\t\tlabel          string\n\t\tpath           string\n\t\texpectedOutput string\n\t\tsetupHandler   func(t *testing.T, r *Router)\n\t}{\n\t\t{\n\t\t\tlabel:          \"route with named parameter\",\n\t\t\tpath:           \"hello/Alice\",\n\t\t\texpectedOutput: \"Hello, Alice!\",\n\t\t\tsetupHandler: func(t *testing.T, r *Router) {\n\t\t\t\tr.HandleFunc(\"hello/{name}\", func(rw *ResponseWriter, req *Request) {\n\t\t\t\t\tname := req.GetVar(\"name\")\n\t\t\t\t\tuassert.Equal(t, \"Alice\", name)\n\t\t\t\t\trw.Write(\"Hello, \" + name + \"!\")\n\t\t\t\t})\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel:          \"static route\",\n\t\t\tpath:           \"hi\",\n\t\t\texpectedOutput: \"Hi, earth!\",\n\t\t\tsetupHandler: func(t *testing.T, r *Router) {\n\t\t\t\tr.HandleFunc(\"hi\", func(rw *ResponseWriter, req *Request) {\n\t\t\t\t\tuassert.Equal(t, req.Path, \"hi\")\n\t\t\t\t\trw.Write(\"Hi, earth!\")\n\t\t\t\t})\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel:          \"route with named parameter and query string\",\n\t\t\tpath:           \"hello/foo/bar?foo=bar\u0026baz\",\n\t\t\texpectedOutput: \"foo bar\",\n\t\t\tsetupHandler: func(t *testing.T, r *Router) {\n\t\t\t\tr.HandleFunc(\"hello/{key}/{val}\", func(rw *ResponseWriter, req *Request) {\n\t\t\t\t\tkey := req.GetVar(\"key\")\n\t\t\t\t\tval := req.GetVar(\"val\")\n\t\t\t\t\tuassert.Equal(t, \"foo\", key)\n\t\t\t\t\tuassert.Equal(t, \"bar\", val)\n\t\t\t\t\tuassert.Equal(t, \"hello/foo/bar?foo=bar\u0026baz\", req.RawPath)\n\t\t\t\t\tuassert.Equal(t, \"hello/foo/bar\", req.Path)\n\t\t\t\t\tuassert.Equal(t, \"bar\", req.Query.Get(\"foo\"))\n\t\t\t\t\tuassert.Empty(t, req.Query.Get(\"baz\"))\n\t\t\t\t\trw.Write(key + \" \" + val)\n\t\t\t\t})\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// TODO: finalize how router should behave with double slash in path.\n\t\t\tlabel:          \"double slash in nested route\",\n\t\t\tpath:           \"a/foo//\",\n\t\t\texpectedOutput: \"test foo\",\n\t\t\tsetupHandler: func(t *testing.T, r *Router) {\n\t\t\t\tr.HandleFunc(\"a/{key}\", func(rw *ResponseWriter, req *Request) {\n\t\t\t\t\t// Assert not called\n\t\t\t\t\tuassert.False(t, true, \"unexpected handler called\")\n\t\t\t\t})\n\n\t\t\t\tr.HandleFunc(\"a/{key}/{val}/\", func(rw *ResponseWriter, req *Request) {\n\t\t\t\t\tkey := req.GetVar(\"key\")\n\t\t\t\t\tval := req.GetVar(\"val\")\n\t\t\t\t\tuassert.Equal(t, key, \"foo\")\n\t\t\t\t\tuassert.Empty(t, val)\n\t\t\t\t\trw.Write(\"test \" + key)\n\t\t\t\t})\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel:          \"wildcard in route\",\n\t\t\tpath:           \"hello/Alice/Bob\",\n\t\t\texpectedOutput: \"Matched: Alice/Bob\",\n\t\t\tsetupHandler: func(t *testing.T, r *Router) {\n\t\t\t\tr.HandleFunc(\"hello/*\", func(rw *ResponseWriter, req *Request) {\n\t\t\t\t\tpath := req.GetVar(\"*\")\n\t\t\t\t\tuassert.Equal(t, \"Alice/Bob\", path)\n\t\t\t\t\tuassert.Equal(t, \"hello/Alice/Bob\", req.Path)\n\t\t\t\t\trw.Write(\"Matched: \" + path)\n\t\t\t\t})\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel:          \"wildcard in route with query string\",\n\t\t\tpath:           \"hello/Alice/Bob?foo=bar\",\n\t\t\texpectedOutput: \"Matched: Alice/Bob\",\n\t\t\tsetupHandler: func(t *testing.T, r *Router) {\n\t\t\t\tr.HandleFunc(\"hello/*\", func(rw *ResponseWriter, req *Request) {\n\t\t\t\t\tpath := req.GetVar(\"*\")\n\t\t\t\t\tuassert.Equal(t, \"Alice/Bob\", path)\n\t\t\t\t\tuassert.Equal(t, \"hello/Alice/Bob?foo=bar\", req.RawPath)\n\t\t\t\t\tuassert.Equal(t, \"hello/Alice/Bob\", req.Path)\n\t\t\t\t\tuassert.Equal(t, \"bar\", req.Query.Get(\"foo\"))\n\t\t\t\t\trw.Write(\"Matched: \" + path)\n\t\t\t\t})\n\t\t\t},\n\t\t},\n\t\t// TODO: {\"hello\", \"Hello, world!\"},\n\t\t// TODO: hello/, /hello, hello//Alice, hello/Alice/, hello/Alice/Bob, etc\n\t}\n\tfor _, tt := range cases {\n\t\tt.Run(tt.label, func(t *testing.T) {\n\t\t\trouter := NewRouter()\n\t\t\ttt.setupHandler(t, router)\n\t\t\toutput := router.Render(tt.path)\n\t\t\tif output != tt.expectedOutput {\n\t\t\t\tt.Errorf(\"Expected output %q, but got %q\", tt.expectedOutput, output)\n\t\t\t}\n\t\t})\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"MqolZVGP5eCTT+PqllKAyOrY/SvZzwywyZ53L6PYqlh7DmmjUkq14RRKcnY1N9LN7FT80YwKJpKv37C9j9Hc3A=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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"},{"name":"ownable_test.gno","body":"package ownable\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nvar (\n\talice = testutils.TestAddress(\"alice\")\n\tbob   = testutils.TestAddress(\"bob\")\n)\n\nfunc TestNewWithAddress(cur realm, t *testing.T) {\n\to := NewWithAddress(alice)\n\n\tgot := o.Owner()\n\tuassert.Equal(t, got, alice)\n}\n\nfunc TestTransferOwnership(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\to := NewWithAddress(alice)\n\tfunc(cur realm) {\n\t\terr := o.TransferOwnership(0, cur, bob)\n\t\turequire.NoError(t, err)\n\t}(cross(cur))\n\n\tgot := o.Owner()\n\tuassert.Equal(t, got, bob)\n}\n\nfunc TestTransferOwnershipUnauthorized(cur realm, t *testing.T) {\n\to := NewWithAddress(alice)\n\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\tfunc(cur realm) {\n\t\tuassert.ErrorContains(t, o.TransferOwnership(0, cur, bob), ErrUnauthorized.Error())\n\t}(cross(cur))\n}\n\nfunc TestDropOwnership(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\to := NewWithAddress(alice)\n\tfunc(cur realm) {\n\t\terr := o.DropOwnership(0, cur)\n\t\turequire.NoError(t, err, \"DropOwnership failed\")\n\t}(cross(cur))\n\n\towner := o.Owner()\n\tuassert.Empty(t, owner, \"owner should be empty\")\n}\n\nfunc TestDropOwnershipUnauthorized(cur realm, t *testing.T) {\n\to := NewWithAddress(alice)\n\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\tfunc(cur realm) {\n\t\tuassert.ErrorContains(t, o.DropOwnership(0, cur), ErrUnauthorized.Error())\n\t}(cross(cur))\n}\n\nfunc TestOwnedBy(cur realm, t *testing.T) {\n\to := NewWithAddress(alice)\n\tuassert.True(t, o.OwnedBy(alice))\n\tuassert.False(t, o.OwnedBy(bob))\n}\n\nfunc TestAssertOwnedBy(cur realm, t *testing.T) {\n\to := NewWithAddress(alice)\n\n\t// Should not panic.\n\to.AssertOwnedBy(alice)\n\n\tuassert.PanicsWithMessage(t, cur, ErrUnauthorized.Error(), func() {\n\t\to.AssertOwnedBy(bob)\n\t})\n}\n\nfunc TestErrInvalidAddress(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\to := NewWithAddress(alice)\n\tfunc(cur realm) {\n\t\terr := o.TransferOwnership(0, cur, \"\")\n\t\tuassert.ErrorContains(t, err, ErrInvalidAddress.Error())\n\n\t\terr = o.TransferOwnership(0, cur, \"10000000001000000000100000000010000000001000000000\")\n\t\tuassert.ErrorContains(t, err, ErrInvalidAddress.Error())\n\t}(cross(cur))\n}\n\nfunc TestNilReceiver(cur realm, t *testing.T) {\n\tvar o *Ownable\n\n\towner := o.Owner()\n\tif owner != address(\"\") {\n\t\tt.Errorf(\"expected empty address but got %v\", owner)\n\t}\n\n\tisOwner := o.OwnedBy(alice)\n\tuassert.False(t, isOwner)\n\n\tdefer func() {\n\t\tr := recover()\n\t\tif r == nil {\n\t\t\tt.Error(\"expected panic but got none\")\n\t\t}\n\t\tif r != ErrUnauthorized {\n\t\t\tt.Errorf(\"expected ErrUnauthorized but got %v\", r)\n\t\t}\n\t}()\n\to.AssertOwnedBy(alice)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"gk3hzP2qRhbtv31noxulHOJjsjXTCagCXP7h9V1g9EANzldij84trT2HgTz0IvedrzYnI4U5e7ZWXWEwdDDwWQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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":"fqname_test.gno","body":"package fqname\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nfunc TestParse(t *testing.T) {\n\ttests := []struct {\n\t\tinput           string\n\t\texpectedPkgPath string\n\t\texpectedName    string\n\t}{\n\t\t{\"gno.land/p/nt/avl/v0.Tree\", \"gno.land/p/nt/avl/v0\", \"Tree\"},\n\t\t{\"gno.land/p/nt/avl/v0\", \"gno.land/p/nt/avl/v0\", \"\"},\n\t\t{\"gno.land/p/nt/avl/v0.Tree.Node\", \"gno.land/p/nt/avl/v0\", \"Tree.Node\"},\n\t\t{\"gno.land/p/nt/avl/v0/nested.Package.Func\", \"gno.land/p/nt/avl/v0/nested\", \"Package.Func\"},\n\t\t{\"path/filepath.Split\", \"path/filepath\", \"Split\"},\n\t\t{\"path.Split\", \"path\", \"Split\"},\n\t\t{\"path/filepath\", \"path/filepath\", \"\"},\n\t\t{\"path\", \"path\", \"\"},\n\t\t{\"\", \"\", \"\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tpkgpath, name := Parse(tt.input)\n\t\tuassert.Equal(t, tt.expectedPkgPath, pkgpath, \"Package path did not match\")\n\t\tuassert.Equal(t, tt.expectedName, name, \"Name did not match\")\n\t}\n}\n\nfunc TestConstruct(t *testing.T) {\n\ttests := []struct {\n\t\tpkgpath  string\n\t\tname     string\n\t\texpected string\n\t}{\n\t\t{\"gno.land/r/demo/foo20\", \"Token\", \"gno.land/r/demo/foo20.Token\"},\n\t\t{\"gno.land/r/demo/foo20\", \"\", \"gno.land/r/demo/foo20\"},\n\t\t{\"path\", \"\", \"path\"},\n\t\t{\"path\", \"Split\", \"path.Split\"},\n\t\t{\"path/filepath\", \"\", \"path/filepath\"},\n\t\t{\"path/filepath\", \"Split\", \"path/filepath.Split\"},\n\t\t{\"\", \"JustName\", \".JustName\"},\n\t\t{\"\", \"\", \"\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tresult := Construct(tt.pkgpath, tt.name)\n\t\tuassert.Equal(t, tt.expected, result, \"Constructed FQName did not match expected\")\n\t}\n}\n\nfunc TestRenderLink(t *testing.T) {\n\ttests := []struct {\n\t\tpkgPath  string\n\t\tslug     string\n\t\texpected string\n\t}{\n\t\t{\"gno.land/p/nt/avl/v0\", \"Tree\", \"[gno.land/p/nt/avl/v0](/p/nt/avl/v0).Tree\"},\n\t\t{\"gno.land/p/nt/avl/v0\", \"\", \"[gno.land/p/nt/avl/v0](/p/nt/avl/v0)\"},\n\t\t{\"github.com/a/b\", \"C\", \"github.com/a/b.C\"},\n\t\t{\"example.com/pkg\", \"Func\", \"example.com/pkg.Func\"},\n\t\t{\"gno.land/r/demo/foo20\", \"Token\", \"[gno.land/r/demo/foo20](/r/demo/foo20).Token\"},\n\t\t{\"gno.land/r/demo/foo20\", \"\", \"[gno.land/r/demo/foo20](/r/demo/foo20)\"},\n\t\t{\"\", \"\", \"\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tresult := RenderLink(tt.pkgPath, tt.slug)\n\t\tuassert.Equal(t, tt.expected, result, \"Rendered link did not match expected\")\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/fqname/v0\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"t/spIYUHtrkXq/br1rMkFjTkM3zOTgZFAs0eo7MlcnpRdH8EkUaNWEQJ3G3iDbScaabl3nih6PMaFJOI0POnDA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"grc20reg","path":"gno.land/r/nt/grc20reg/v1","files":[{"name":"amount_test.gno","body":"package grc20reg\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n)\n\n// equal256 and require256 compare 256-bit amounts natively; uassert and\n// urequire do not support uint256 (see p/nt/ufmt's README).\nfunc equal256(t *testing.T, want, got uint256, msgs ...string) bool {\n\tt.Helper()\n\tif want == got {\n\t\treturn true\n\t}\n\tt.Errorf(\"%swant %s, got %s\", msgPrefix(msgs), strconv.FormatUint256(want, 10), strconv.FormatUint256(got, 10))\n\treturn false\n}\n\nfunc require256(t *testing.T, want, got uint256, msgs ...string) {\n\tt.Helper()\n\tif want != got {\n\t\tt.Fatalf(\"%swant %s, got %s\", msgPrefix(msgs), strconv.FormatUint256(want, 10), strconv.FormatUint256(got, 10))\n\t}\n}\n\nfunc msgPrefix(msgs []string) string {\n\tif len(msgs) == 0 {\n\t\treturn \"\"\n\t}\n\treturn msgs[0] + \": \"\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/nt/grc20reg/v1\"\ngno = \"0.9\"\n"},{"name":"grc20reg.gno","body":"// Package grc20reg (v1) is the registry for gno.land/p/nt/grc20/v1 tokens,\n// whose amounts are uint256. It works like gno.land/r/nt/grc20reg/v0, which\n// keeps holding the v0 (int64) tokens; the two registries are independent, so\n// a realm may register a v0 and a v1 token under the same symbol.\npackage grc20reg\n\nimport (\n\t\"chain\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/nt/avl/rotree/v0\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/fqname/v0\"\n\t\"gno.land/p/nt/grc20/v1\"\n\t\"gno.land/p/nt/markdown/sanitize/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// Write wrappers: a registered token can be moved through the registry without\n// importing the token's realm, which is the point of a registry. What makes\n// that safe is the calling convention, so it is worth stating once here rather\n// than three times below.\n//\n// These are NOT crossing functions. `_ int, rlm realm` is the only shape that\n// gives a non-crossing realm parameter — a realm parameter in first position\n// must be named `cur`, which makes the function crossing — and the distinction\n// is load-bearing, not stylistic:\n//\n//   - Crossing (`func Transfer(cur realm, …)`) mints a fresh `cur` for THIS\n//     realm. RealmTeller would then bind the actor to the registry's own\n//     address and the registry would spend its own balance. Useless at best.\n//   - Non-crossing (`func Transfer(_ int, rlm realm, …)`) declaring-borrows to\n//     the registry without a realm-context change, so `rlm` is still the\n//     caller's own live token and the actor is the caller.\n//\n// The safety comes from RealmTeller's IsCurrent() assertion. The actor is\n// rlm.Address() on a token that must be the live crossing frame, so it is\n// provably the immediate caller: a stale or foreign token is refused with\n// ErrSpoofedRealm. Debiting anyone else would mean holding their live `cur`,\n// which means executing inside their frame — authority they handed over\n// deliberately, and the same trust model RealmTeller already carries.\n//\n// This is deliberately not grc20.CallerTeller. \"Act as whoever called me\" is\n// the confused deputy: the debited account ends up chosen by whoever the hub\n// can be induced to serve, and an intermediate realm frame silently changes who\n// pays. CallerTeller is confined to the token's own realm for that reason and\n// is not reachable from a *Token. \"Act as the realm that called me, verified\n// current\" has nothing to induce — the caller cannot name a victim, only\n// itself.\n//\n// Realm-only by construction: MsgCall cannot build a realm argument\n// (convertArgToGno rejects non-primitive parameter types), so a signing user\n// cannot reach these at all and there is no in-band case to guard against.\n// Users move their own tokens through the token realm's own entry points\n// (wugnot.Transfer, foo20.Transfer, …).\n\n// Transfer moves `amount` out of the CALLING REALM's own balance.\n//\n// Call it non-crossing, forwarding your own `cur`:\n//\n//\tgrc20reg.Transfer(0, cur, \"gno.land/r/demo/defi/foo20.FOO\", to, 100)\nfunc Transfer(_ int, rlm realm, tokenKey string, to address, amount uint256) {\n\tcheckErr(MustGet(tokenKey).RealmTeller(0, rlm).Transfer(0, rlm, to, amount))\n}\n\n// Approve sets an allowance owned by the CALLING REALM, letting `spender` draw\n// on the calling realm's balance. It does not touch the signing user's\n// allowances.\nfunc Approve(_ int, rlm realm, tokenKey string, spender address, amount uint256) {\n\tcheckErr(MustGet(tokenKey).RealmTeller(0, rlm).Approve(0, rlm, spender, amount))\n}\n\n// TransferFrom spends an allowance with the CALLING REALM as the spender.\n//\n// Note the allowance direction this implies: `from` must have approved the\n// calling realm, not the signing user. That is the supported way for a realm to\n// move a user's funds — the user grants the realm an allowance, and the realm\n// draws on it as itself, so every debit is one the owner authorized against\n// that specific realm.\nfunc TransferFrom(_ int, rlm realm, tokenKey string, from, to address, amount uint256) {\n\tcheckErr(MustGet(tokenKey).RealmTeller(0, rlm).TransferFrom(0, rlm, from, to, amount))\n}\n\nfunc checkErr(err error) {\n\tif err != nil {\n\t\tpanic(err)\n\t}\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/nt/grc20reg/v1:\" + key\n\t\t\ts += \"- \" + md.Bold(sanitize.InlineText(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\", sanitize.InlineText(token.GetName()))\n\t\ts += \"- symbol: \" + md.Bold(sanitize.InlineText(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 += \"- total supply: \" + strconv.FormatUint256(token.TotalSupply(), 10) + \"\\n\"\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\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"},{"name":"grc20reg_test.gno","body":"package grc20reg\n\nimport (\n\t\"chain\"\n\t\"math\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/grc20/v1\"\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestRegistry(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/demo/foo\"))\n\ttoken, ledger := grc20.NewToken(\"TestToken\", \"TST\", 4, 0, cur)\n\tledger.Mint(cur.Address(), 1234567)\n\t// register\n\tkey := Register(cross(cur), token, \"mySlug\")\n\tregToken := Get(key)\n\turequire.True(t, regToken != nil, \"expected to find a token\") // fixme: use urequire.NotNil\n\turequire.Equal(t, regToken.GetSymbol(), \"TST\")\n\n\texpected := `- **TestToken** - [gno.land/r/demo/foo](/r/demo/foo).TST - [info](/r/nt/grc20reg/v1:gno.land/r/demo/foo.TST)\n`\n\tgot := Render(\"\")\n\turequire.True(t, strings.Contains(got, expected))\n\t// 404\n\tinvalidToken := Get(\"0xdeadbeef\")\n\turequire.True(t, invalidToken == nil)\n\n\tgot = Render(\"\")\n\turequire.True(t, strings.Contains(got, expected))\n\n\texpected = `# TestToken\n- symbol: **TST**\n- realm: [gno.land/r/demo/foo](/r/demo/foo).TST\n- decimals: 4\n- total supply: 1234567\n`\n\tgot = Render(key)\n\turequire.Equal(t, expected, got)\n\n\t// The registry keys by rlmPath.symbol, so a second token with the same\n\t// symbol in the same realm is rejected even though its Token.ID() differs\n\t// (distinct trailing sequence id). See TestRegisterRejectsOverwrite.\n\tsecond, _ := grc20.NewToken(\"Second\", \"TST\", 4, 1, cur)\n\turequire.NotEqual(t, token.ID(), second.ID()) // ids are decoupled from symbol\n\turequire.AbortsContains(t, cur, \"token already registered\", func() {\n\t\tRegister(cross(cur), second, \"\")\n\t})\n}\n\n// TestRegistryLookupGrantsNoSpendAuthority pins the property that replaced the\n// former Transfer/Approve/TransferFrom wrappers: a registry lookup yields a\n// *Token, and a *Token alone carries no authority to debit anybody. The\n// frame-relative teller is reachable only from the ledger, which never leaves\n// the token's own realm, so this realm cannot act on behalf of its caller —\n// that shape was the confused deputy.\n//\n// The supported route for a realm that must move a user's funds is the one\n// exercised below: the user grants an allowance, and the realm spends it as\n// itself through a RealmTeller, which is eagerly bound to its own address.\nfunc TestRegistryLookupGrantsNoSpendAuthority(cur realm, t *testing.T) {\n\tconst (\n\t\ttokenPath    = \"gno.land/r/demo/token\"\n\t\tconsumerPath = \"gno.land/r/demo/grc20reg_consumer\"\n\t)\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\tconsumer := chain.PackageAddress(consumerPath)\n\n\ttesting.SetRealm(testing.NewCodeRealm(tokenPath))\n\ttoken, ledger := grc20.NewToken(\"TestToken\", \"TST\", 4, 0, cur)\n\turequire.NoError(t, ledger.Mint(alice, 1_000))\n\ttokenKey := Register(cross(cur), token, \"\")\n\tuassert.Equal(t, \"TestToken\", MustGet(tokenKey).GetName())\n\tequal256(t, uint256(1_000), MustGet(tokenKey).BalanceOf(alice))\n\tequal256(t, uint256(0), MustGet(tokenKey).Allowance(alice, consumer))\n\n\t// alice approves the consumer realm on the token. On chain she does this\n\t// through the token realm's own entry point; the ledger stands in for it.\n\turequire.NoError(t, ledger.ImpersonateTeller(alice).Approve(0, cur, consumer, 300))\n\tequal256(t, uint256(300), MustGet(tokenKey).Allowance(alice, consumer))\n\n\t// The consumer spends that allowance as ITSELF, over a token it found in\n\t// the registry and does not own. The actor is fixed at construction, so it\n\t// cannot be redirected by whoever calls in.\n\ttesting.SetRealm(testing.NewCodeRealm(consumerPath))\n\tspender := MustGet(tokenKey).RealmTeller(0, cur)\n\turequire.NoError(t, spender.TransferFrom(0, cur, alice, bob, 200))\n\tequal256(t, uint256(800), MustGet(tokenKey).BalanceOf(alice))\n\tequal256(t, uint256(200), MustGet(tokenKey).BalanceOf(bob))\n\tequal256(t, uint256(100), MustGet(tokenKey).Allowance(alice, consumer))\n\n\t// Beyond the allowance it stops: the balance is not reachable directly.\n\tuassert.ErrorContains(t,\n\t\tspender.TransferFrom(0, cur, alice, bob, 101),\n\t\t\"insufficient allowance\")\n\tequal256(t, uint256(800), MustGet(tokenKey).BalanceOf(alice))\n\tequal256(t, uint256(200), MustGet(tokenKey).BalanceOf(bob))\n}\n\n// TestRegistryConsumerCannotRedirectItsActor is the negative half of the\n// property above: a realm holding a registered token pays out of its own\n// balance, not the signing user's. The actor is bound at construction, so the\n// transaction's origin caller has no bearing on who is debited — which is what\n// made the frame-relative teller in a foreign realm a phishing primitive.\nfunc TestRegistryConsumerCannotRedirectItsActor(cur realm, t *testing.T) {\n\tconst (\n\t\ttokenPath    = \"gno.land/r/demo/token_actor_binding\"\n\t\tconsumerPath = \"gno.land/r/demo/grc20reg_actor_consumer\"\n\t)\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\tconsumer := chain.PackageAddress(consumerPath)\n\n\ttesting.SetRealm(testing.NewCodeRealm(tokenPath))\n\ttoken, ledger := grc20.NewToken(\"ActorBinding\", \"ACTB\", 4, 0, cur)\n\turequire.NoError(t, ledger.Mint(alice, 1_000))\n\turequire.NoError(t, ledger.Mint(consumer, 500))\n\ttokenKey := Register(cross(cur), token, \"\")\n\n\t// alice signs the transaction, and holds a balance the consumer would love\n\t// to spend. The consumer pays out of its own instead.\n\ttesting.SetOriginCaller(alice)\n\ttesting.SetRealm(testing.NewCodeRealm(consumerPath))\n\turequire.NoError(t, MustGet(tokenKey).RealmTeller(0, cur).Transfer(0, cur, bob, 100))\n\tequal256(t, uint256(1_000), MustGet(tokenKey).BalanceOf(alice))\n\tequal256(t, uint256(400), MustGet(tokenKey).BalanceOf(consumer))\n\tequal256(t, uint256(100), MustGet(tokenKey).BalanceOf(bob))\n}\n\nfunc TestRegisterRejectsOverwrite(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/demo/grc20reg_overwrite\"))\n\ttoken, ledger := grc20.NewToken(\"Bar\", \"BAR\", 4, 0, cur)\n\tledger.Mint(cur.Address(), 11)\n\tkey := Register(cross(cur), token, \"\")\n\n\turequire.Equal(t, \"BAR\", Get(key).GetSymbol())\n\tequal256(t, uint256(11), Get(key).BalanceOf(cur.Address()))\n\n\treplacement, _ := grc20.NewToken(\"Replacement\", \"BAR\", 6, 0, cur)\n\turequire.AbortsContains(t, cur, \"token already registered\", func() {\n\t\tRegister(cross(cur), replacement, \"\")\n\t})\n}\n\nfunc TestRegisterRejectsAliasedTokenPaths(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/demo/grc20reg_alias\"))\n\ttoken, _ := grc20.NewToken(\"Aliased Token\", \"ALIAS\", 4, 0, cur)\n\tRegister(cross(cur), token, \"first\")\n\n\turequire.AbortsContains(t, cur, \"token already registered\", func() {\n\t\tRegister(cross(cur), token, \"second\")\n\t})\n}\n\nfunc TestRegisterRejectsTokenFromDifferentRealm(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/demo/grc20reg_id_source\"))\n\ttoken, _ := grc20.NewToken(\"Mismatch Token\", \"MISMATCH\", 4, 0, cur)\n\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/demo/grc20reg_id_target\"))\n\turequire.AbortsContains(t, cur, \"token must be registered from its own realm\", func() {\n\t\tRegister(cross(cur), token, \"\")\n\t})\n}\n\nfunc TestValidateSlug(cur realm, t *testing.T) {\n\t// Valid slugs — should not panic\n\tvalid := []string{\"mytoken\", \"my-token\", \"my_token\", \"Token123\", \"a\", \"A-B_c\", strings.Repeat(\"a\", maxSlugLen)}\n\tfor _, slug := range valid {\n\t\tvalidateSlug(slug) // no panic = pass\n\t}\n}\n\nfunc TestValidateSlugPanicsOnTooLong(cur realm, t *testing.T) {\n\tdefer func() { recover() }()\n\tvalidateSlug(strings.Repeat(\"a\", maxSlugLen+1))\n\tt.Errorf(\"should have panicked\")\n}\n\nfunc TestValidateSlugPanicsOnSpace(cur realm, t *testing.T) {\n\tdefer func() { recover() }()\n\tvalidateSlug(\"has space\")\n\tt.Errorf(\"should have panicked\")\n}\n\nfunc TestValidateSlugPanicsOnDot(cur realm, t *testing.T) {\n\tdefer func() { recover() }()\n\tvalidateSlug(\"has.dot\")\n\tt.Errorf(\"should have panicked\")\n}\n\nfunc TestValidateSlugPanicsOnSlash(cur realm, t *testing.T) {\n\tdefer func() { recover() }()\n\tvalidateSlug(\"has/slash\")\n\tt.Errorf(\"should have panicked\")\n}\n\nfunc TestValidateSlugPanicsOnBrackets(cur realm, t *testing.T) {\n\tdefer func() { recover() }()\n\tvalidateSlug(\"[brackets]\")\n\tt.Errorf(\"should have panicked\")\n}\n\nfunc TestValidateSlugPanicsOnParens(cur realm, t *testing.T) {\n\tdefer func() { recover() }()\n\tvalidateSlug(\"(parens)\")\n\tt.Errorf(\"should have panicked\")\n}\n\nfunc TestValidateSlugPanicsOnInjection(cur realm, t *testing.T) {\n\tdefer func() { recover() }()\n\tvalidateSlug(`) [Claim](https://evil.com`)\n\tt.Errorf(\"should have panicked\")\n}\n\nfunc TestRegisterRejectsNilToken(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/demo/grc20reg_nil\"))\n\turequire.AbortsContains(t, cur, \"nil token\", func() {\n\t\tRegister(cross(cur), nil, \"\")\n\t})\n}\n\n// TestWrappersBindActorToCallingRealm pins the property that makes the write\n// wrappers safe: they are non-crossing, so `rlm` reaches RealmTeller as the\n// caller's own live token and the debit lands on the caller. A crossing wrapper\n// would mint a fresh `cur` for grc20reg and spend the registry's balance\n// instead, which is why the convention is not a stylistic choice.\n//\n// Both the consumer and the registry hold a balance, so whichever one is\n// debited is observable rather than inferred.\nfunc TestWrappersBindActorToCallingRealm(cur realm, t *testing.T) {\n\tconst (\n\t\ttokenPath    = \"gno.land/r/demo/token_wrapper_actor\"\n\t\tconsumerPath = \"gno.land/r/demo/grc20reg_wrapper_consumer\"\n\t)\n\tbob := testutils.TestAddress(\"bob\")\n\tconsumer := chain.PackageAddress(consumerPath)\n\tregistry := chain.PackageAddress(\"gno.land/r/nt/grc20reg/v1\")\n\n\ttesting.SetRealm(testing.NewCodeRealm(tokenPath))\n\ttoken, ledger := grc20.NewToken(\"WrapperActor\", \"WRPA\", 4, 0, cur)\n\turequire.NoError(t, ledger.Mint(consumer, 1_000))\n\turequire.NoError(t, ledger.Mint(registry, 1_000))\n\ttokenKey := Register(cross(cur), token, \"\")\n\n\ttesting.SetRealm(testing.NewCodeRealm(consumerPath))\n\tTransfer(0, cur, tokenKey, bob, 100)\n\n\tequal256(t, uint256(900), MustGet(tokenKey).BalanceOf(consumer))\n\tequal256(t, uint256(1_000), MustGet(tokenKey).BalanceOf(registry))\n\tequal256(t, uint256(100), MustGet(tokenKey).BalanceOf(bob))\n}\n\n// TestWrapperActorIgnoresSigningUser is the negative half: the signing user has\n// a balance the calling realm would like to spend, and does not lose it. The\n// actor comes from rlm.Address(), so the transaction's origin has no say in who\n// pays — a hub cannot be induced to debit its caller's caller.\nfunc TestWrapperActorIgnoresSigningUser(cur realm, t *testing.T) {\n\tconst (\n\t\ttokenPath    = \"gno.land/r/demo/token_wrapper_origin\"\n\t\tconsumerPath = \"gno.land/r/demo/grc20reg_wrapper_origin_consumer\"\n\t)\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\tconsumer := chain.PackageAddress(consumerPath)\n\n\ttesting.SetRealm(testing.NewCodeRealm(tokenPath))\n\ttoken, ledger := grc20.NewToken(\"WrapperOrigin\", \"WRPO\", 4, 0, cur)\n\turequire.NoError(t, ledger.Mint(alice, 1_000))\n\turequire.NoError(t, ledger.Mint(consumer, 500))\n\ttokenKey := Register(cross(cur), token, \"\")\n\n\ttesting.SetOriginCaller(alice)\n\ttesting.SetRealm(testing.NewCodeRealm(consumerPath))\n\tTransfer(0, cur, tokenKey, bob, 100)\n\n\tequal256(t, uint256(1_000), MustGet(tokenKey).BalanceOf(alice))\n\tequal256(t, uint256(400), MustGet(tokenKey).BalanceOf(consumer))\n}\n\n// TestTransferFromSpendsAllowanceGrantedToCallingRealm records the semantic\n// shift that comes with binding the actor to the caller: the owner must have\n// approved the CALLING REALM, not the signing user. An allowance granted to\n// anyone else does not authorize the wrapper.\nfunc TestTransferFromSpendsAllowanceGrantedToCallingRealm(cur realm, t *testing.T) {\n\tconst (\n\t\ttokenPath    = \"gno.land/r/demo/token_wrapper_allowance\"\n\t\tconsumerPath = \"gno.land/r/demo/grc20reg_wrapper_allowance_consumer\"\n\t)\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\tconsumer := chain.PackageAddress(consumerPath)\n\n\ttesting.SetRealm(testing.NewCodeRealm(tokenPath))\n\ttoken, ledger := grc20.NewToken(\"WrapperAllowance\", \"WRPL\", 4, 0, cur)\n\turequire.NoError(t, ledger.Mint(alice, 1_000))\n\ttokenKey := Register(cross(cur), token, \"\")\n\n\t// No allowance yet: the wrapper cannot touch alice's balance.\n\ttesting.SetRealm(testing.NewCodeRealm(consumerPath))\n\tuassert.PanicsContains(t, cur, \"insufficient allowance\", func() {\n\t\tTransferFrom(0, cur, tokenKey, alice, bob, 100)\n\t})\n\tequal256(t, uint256(1_000), MustGet(tokenKey).BalanceOf(alice))\n\n\t// alice approves the consuming realm itself, and only then does it work.\n\turequire.NoError(t, ledger.Approve(alice, consumer, 100))\n\n\ttesting.SetRealm(testing.NewCodeRealm(consumerPath))\n\tTransferFrom(0, cur, tokenKey, alice, bob, 100)\n\tequal256(t, uint256(900), MustGet(tokenKey).BalanceOf(alice))\n\tequal256(t, uint256(100), MustGet(tokenKey).BalanceOf(bob))\n}\n\n// TestWrappersAbove2Pow63 moves amounts no int64 token can hold through the\n// registry's wrappers, including a MaxUint256 approval.\nfunc TestWrappersAbove2Pow63(cur realm, t *testing.T) {\n\tconst (\n\t\ttokenPath    = \"gno.land/r/demo/token_wrapper_big\"\n\t\tconsumerPath = \"gno.land/r/demo/grc20reg_wrapper_big_consumer\"\n\t\tspenderPath  = \"gno.land/r/demo/grc20reg_wrapper_big_spender\"\n\t)\n\tbob := testutils.TestAddress(\"bob\")\n\tconsumer := chain.PackageAddress(consumerPath)\n\tspender := chain.PackageAddress(spenderPath)\n\tbig := uint256(1) \u003c\u003c 100\n\n\ttesting.SetRealm(testing.NewCodeRealm(tokenPath))\n\ttoken, ledger := grc20.NewToken(\"WrapperBig\", \"WRPB\", 18, 0, cur)\n\turequire.NoError(t, ledger.Mint(consumer, math.MaxUint256))\n\ttokenKey := Register(cross(cur), token, \"\")\n\n\ttesting.SetRealm(testing.NewCodeRealm(consumerPath))\n\tTransfer(0, cur, tokenKey, bob, big)\n\tApprove(0, cur, tokenKey, spender, math.MaxUint256)\n\tequal256(t, uint256(math.MaxUint256-big), MustGet(tokenKey).BalanceOf(consumer))\n\tequal256(t, uint256(big), MustGet(tokenKey).BalanceOf(bob))\n\tequal256(t, uint256(math.MaxUint256), MustGet(tokenKey).Allowance(consumer, spender))\n\n\ttesting.SetRealm(testing.NewCodeRealm(spenderPath))\n\tTransferFrom(0, cur, tokenKey, consumer, bob, big)\n\tequal256(t, uint256(math.MaxUint256-2*big), MustGet(tokenKey).BalanceOf(consumer))\n\tequal256(t, uint256(2*big), MustGet(tokenKey).BalanceOf(bob))\n\tequal256(t, uint256(math.MaxUint256-big), MustGet(tokenKey).Allowance(consumer, spender))\n\n\t// The total supply renders in full.\n\tuassert.True(t, strings.Contains(Render(tokenKey),\n\t\t\"- total supply: 115792089237316195423570985008687907853269984665640564039457584007913129639935\\n\"))\n}\n\n// A registered token's name is chosen by its realm and shown on the registry's\n// pages; it must not render as markup.\nfunc TestRenderEscapesTokenName(cur realm, t *testing.T) {\n\tconst evil = \"[Claim reward](https://evil.example)\"\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/demo/grc20reg_evil_name\"))\n\ttoken, _ := grc20.NewToken(evil, \"EVILN\", 4, 0, cur)\n\tkey := Register(cross(cur), token, \"\")\n\n\tfor _, path := range []string{\"\", key} {\n\t\tout := Render(path)\n\t\tuassert.False(t, strings.Contains(out, evil), \"live link in Render(\"+path+\"): \"+out)\n\t\tuassert.True(t, strings.Contains(out, `\\[Claim reward\\]`), \"name not escaped in Render(\"+path+\")\")\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"PtfidYMTjVgmg75X8zS3nlulWjEr/Jdrv9piuSHStI1NtcEsFfnwdl3vFLDFKzQIN0o8fIIb17MRvDn4HPIVzw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"grc20factory","path":"gno.land/r/demo/defi/grc20factory","files":[{"name":"amount_test.gno","body":"package grc20factory\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n)\n\n// equal256 and require256 compare 256-bit amounts natively; uassert and\n// urequire do not support uint256 (see p/nt/ufmt's README).\nfunc equal256(t *testing.T, want, got uint256, msgs ...string) bool {\n\tt.Helper()\n\tif want == got {\n\t\treturn true\n\t}\n\tt.Errorf(\"%swant %s, got %s\", msgPrefix(msgs), strconv.FormatUint256(want, 10), strconv.FormatUint256(got, 10))\n\treturn false\n}\n\nfunc require256(t *testing.T, want, got uint256, msgs ...string) {\n\tt.Helper()\n\tif want != got {\n\t\tt.Fatalf(\"%swant %s, got %s\", msgPrefix(msgs), strconv.FormatUint256(want, 10), strconv.FormatUint256(got, 10))\n\t}\n}\n\nfunc msgPrefix(msgs []string) string {\n\tif len(msgs) == 0 {\n\t\treturn \"\"\n\t}\n\treturn msgs[0] + \": \"\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/demo/defi/grc20factory\"\ngno = \"0.9\"\n"},{"name":"grc20factory.gno","body":"// Package grc20factory lets anyone create a GRC20 token. Tokens are built on\n// gno.land/p/nt/grc20/v1, so every amount is a uint256, and registered in\n// gno.land/r/nt/grc20reg/v1.\npackage grc20factory\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/p/moul/md/v0\"\n\tp \"gno.land/p/nt/avl/pager/v0\"\n\t\"gno.land/p/nt/avl/rotree/v0\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/grc20/v1\"\n\t\"gno.land/p/nt/markdown/sanitize/v0\"\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/nt/grc20reg/v1\"\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 uint256 // per-request amount. disabled if 0.\n}\n\nfunc New(cur realm, name, symbol string, decimals int, initialMint, faucet uint256) {\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 uint256, 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) uint256 {\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) uint256 {\n\tinst := mustGetInstance(symbol)\n\treturn inst.token.ReadonlyTeller().BalanceOf(owner)\n}\n\nfunc Allowance(symbol string, owner, spender address) uint256 {\n\tinst := mustGetInstance(symbol)\n\treturn inst.token.ReadonlyTeller().Allowance(owner, spender)\n}\n\nfunc Transfer(cur realm, symbol string, to address, amount uint256) {\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 uint256) {\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 uint256) {\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 uint256) {\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 uint256) {\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\t// md.Link escapes the link text itself.\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.BalanceOf(address(addr))\n\n\t// Render the balance information.\n\tout := md.Paragraph(\n\t\tmd.Bold(sanitize.InlineText(addr)) + \" balance: \" + strconv.FormatUint256(balance, 10),\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"},{"name":"grc20factory_test.gno","body":"package grc20factory\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nfunc TestReadOnlyPublicMethods(cur realm, t *testing.T) {\n\tadmin := testutils.TestAddress(\"admin\")\n\tbob := testutils.TestAddress(\"bob\")\n\tcarl := testutils.TestAddress(\"carl\")\n\n\ttype test struct {\n\t\tname    string\n\t\tbalance uint256\n\t\tfn      func() uint256\n\t}\n\n\tcheckBalances := func(step string, totSup, balAdm, balBob, allowAdmBob, balCarl uint256) {\n\t\ttests := []test{\n\t\t\t{\"TotalSupply\", totSup, func() uint256 { return TotalSupply(\"FOO\") }},\n\t\t\t{\"BalanceOf(admin)\", balAdm, func() uint256 { return BalanceOf(\"FOO\", admin) }},\n\t\t\t{\"BalanceOf(bob)\", balBob, func() uint256 { return BalanceOf(\"FOO\", bob) }},\n\t\t\t{\"Allowance(admin, bob)\", allowAdmBob, func() uint256 { return Allowance(\"FOO\", admin, bob) }},\n\t\t\t{\"BalanceOf(carl)\", balCarl, func() uint256 { return BalanceOf(\"FOO\", carl) }},\n\t\t}\n\t\tfor _, tc := range tests {\n\t\t\treason := ufmt.Sprintf(\"%s.%s - %s\", step, tc.name, \"balances do not match\")\n\t\t\tequal256(t, tc.balance, tc.fn(), reason)\n\t\t}\n\t}\n\n\t// admin creates FOO and BAR.\n\ttesting.SetOriginCaller(admin)\n\tNewWithAdmin(cross(cur), \"Foo\", \"FOO\", 3, 1_111_111_000, 5_555, admin)\n\tNewWithAdmin(cross(cur), \"Bar\", \"BAR\", 3, 2_222_000, 6_666, admin)\n\tuassert.Equal(t, Bank(\"FOO\").ID(), \"gno.land/r/demo/defi/grc20factory.FOO.0000001\")\n\tuassert.Equal(t, Bank(\"BAR\").ID(), \"gno.land/r/demo/defi/grc20factory.BAR.0000002\")\n\tcheckBalances(\"step1\", 1_111_111_000, 1_111_111_000, 0, 0, 0)\n\n\t// admin mints to bob.\n\tmustGetInstance(\"FOO\").ledger.Mint(bob, 333_333_000)\n\tcheckBalances(\"step2\", 1_444_444_000, 1_111_111_000, 333_333_000, 0, 0)\n\n\t// carl uses the faucet.\n\ttesting.SetOriginCaller(carl)\n\tFaucet(cross(cur), \"FOO\")\n\tcheckBalances(\"step3\", 1_444_449_555, 1_111_111_000, 333_333_000, 0, 5_555)\n\n\t// admin gives to bob some allowance.\n\ttesting.SetOriginCaller(admin)\n\tApprove(cross(cur), \"FOO\", bob, 1_000_000)\n\tcheckBalances(\"step4\", 1_444_449_555, 1_111_111_000, 333_333_000, 1_000_000, 5_555)\n\n\t// bob uses a part of the allowance.\n\ttesting.SetOriginCaller(bob)\n\tTransferFrom(cross(cur), \"FOO\", admin, carl, 400_000)\n\tcheckBalances(\"step5\", 1_444_449_555, 1_110_711_000, 333_333_000, 600_000, 405_555)\n\n\t// bob uses a part of the allowance.\n\ttesting.SetOriginCaller(bob)\n\tTransferFrom(cross(cur), \"FOO\", admin, carl, 600_000)\n\tcheckBalances(\"step6\", 1_444_449_555, 1_110_111_000, 333_333_000, 0, 1_005_555)\n}\n\nfunc TestAbove2Pow63(cur realm, t *testing.T) {\n\tadmin := testutils.TestAddress(\"admin_big\")\n\tbob := testutils.TestAddress(\"bob_big\")\n\tbig := uint256(1) \u003c\u003c 64\n\n\ttesting.SetOriginCaller(admin)\n\tNewWithAdmin(cross(cur), \"Big\", \"BIG\", 18, big, big, admin)\n\tequal256(t, big, TotalSupply(\"BIG\"))\n\n\ttesting.SetOriginCaller(bob)\n\tFaucet(cross(cur), \"BIG\")\n\tequal256(t, big, BalanceOf(\"BIG\", bob))\n\tequal256(t, 2*big, TotalSupply(\"BIG\"))\n\n\ttesting.SetOriginCaller(bob)\n\tTransfer(cross(cur), \"BIG\", admin, big)\n\tequal256(t, 2*big, BalanceOf(\"BIG\", admin))\n\tuassert.Equal(t, \"**\"+admin.String()+\"** balance: 36893488147419103232\\n\\n\", Render(\"BIG/balance/\"+admin.String()))\n}\n\n// Anyone can create a factory token, so its name must not inject markdown\n// into the factory's pages.\nfunc TestRenderEscapesTokenName(cur realm, t *testing.T) {\n\tconst evil = \"[Claim reward](https://evil.example)\"\n\ttesting.SetOriginCaller(testutils.TestAddress(\"evil_creator\"))\n\tNew(cross(cur), evil, \"EVILNAME\", 0, 0, 0)\n\n\tfor _, path := range []string{\"\", \"EVILNAME\"} {\n\t\tout := Render(path)\n\t\tuassert.False(t, strings.Contains(out, evil), \"live link in Render(\"+path+\"): \"+out)\n\t\tuassert.True(t, strings.Contains(out, `\\[Claim reward\\]`), \"name not escaped in Render(\"+path+\"): \"+out)\n\t}\n\n\t// A path segment reflected into the balance page is escaped too.\n\tout := Render(\"EVILNAME/balance/[x](y)\")\n\tuassert.False(t, strings.Contains(out, \"[x](y)\"), \"path segment rendered raw: \"+out)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"ZDA4IxzkALJdwiPbUzbIOYazOpB+VG+FyUK/ADKSlP4iDOG3J5SQmPlEaDPrALZIj7T59NBCRnQ30mYBeiJboA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"panictoerr","path":"gno.land/p/aeddi/panictoerr/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/aeddi/panictoerr/v0\"\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"},{"name":"panictoerr_test.gno","body":"package panictoerr_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\tpte \"gno.land/p/aeddi/panictoerr/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\tgrc20 \"gno.land/r/demo/defi/grc20factory\"\n)\n\n// cur is a zero-value realm used as a placeholder when forwarding to\n// uassert/urequire dispatch helpers that gained an `rlm realm` param.\n// These tests pass `func()` callbacks (no crossing inside the callback),\n// so rlm is ignored — a nil realm here is safe.\nvar cur realm\n\n// Test PanicToError with different types as panic value.\nfunc TestSimplePanicToError(t *testing.T) {\n\terr := pte.PanicToError(func() {\n\t\tpanic(\"string\")\n\t})\n\tuassert.Equal(t, err.Error(), \"string\")\n\n\terr = pte.PanicToError(func() {\n\t\tpanic(errors.New(\"error\"))\n\t})\n\tuassert.Equal(t, err.Error(), \"error\")\n\n\terr = pte.PanicToError(func() {\n\t\tpanic(42)\n\t})\n\tuassert.Equal(t, err.Error(), \"42\")\n}\n\nfunc TestRealmPanicToError(cur realm, t *testing.T) {\n\t// Set a test realm to be able to call a realm.\n\ttestRealm := testing.NewCodeRealm(\"gno.land/r/aeddi/panictoerr/test\")\n\ttesting.SetRealm(testRealm)\n\n\tconst message = \"token instance does not exist\"\n\tvar err error\n\n\t// Define a panicking function (local panic; does not cross any\n\t// realm boundary). Under the unified declaring-realm borrow,\n\t// calling grc20.Bank() would borrow into /r/demo/defi/grc20factory\n\t// and any panic there would cross a realm boundary, becoming an\n\t// abort catchable only by revive() — see the `aborting` case\n\t// below. To exercise the pure recover() path, panic locally.\n\tpanicking := func() {\n\t\tpanic(message)\n\t}\n\n\t// panicking function should panic.\n\tuassert.PanicsWithMessage(t, cur, message, panicking)\n\n\t// panicking function should panic when wrapped in AbortToError.\n\tuassert.PanicsWithMessage(t, cur, message, func() { pte.AbortToError(panicking) })\n\n\t// panicking function should not panic when wrapped in PanicToError.\n\tuassert.NotPanics(\n\t\tt, cur,\n\t\tfunc() { err = pte.PanicToError(panicking) },\n\t\t\"panicking function should not panic when wrapped in PanicToError\",\n\t)\n\tuassert.Equal(t, err.Error(), message)\n\n\t// panicking function should not panic when wrapped in PanicAbortToError.\n\tuassert.NotPanics(\n\t\tt, cur,\n\t\tfunc() { err = pte.PanicAbortToError(panicking) },\n\t\t\"panicking function should not panic when wrapped in PanicAbortToError\",\n\t)\n\tuassert.Equal(t, err.Error(), message)\n\n\t// Define an aborting function (crossing).\n\taborting := func() {\n\t\tgrc20.Faucet(cross(cur), \"unknown\")\n\t}\n\n\t// aborting function should abort.\n\tuassert.AbortsWithMessage(t, cur, message, aborting)\n\n\t// aborting function should abort when wrapped in PanicToError.\n\tuassert.AbortsWithMessage(t, cur, message, func() { pte.PanicToError(aborting) })\n\n\t// aborting function should not abort when wrapped in AbortToError.\n\tuassert.NotAborts(\n\t\tt, cur,\n\t\tfunc() { err = pte.AbortToError(aborting) },\n\t\t\"aborting function should not abort when wrapped in AbortToError\",\n\t)\n\tuassert.Equal(t, err.Error(), message)\n\n\t// aborting function should not abort when wrapped in PanicAbortToError.\n\tuassert.NotAborts(\n\t\tt, cur,\n\t\tfunc() { err = pte.PanicAbortToError(aborting) },\n\t\t\"aborting function should not abort when wrapped in PanicAbortToError\",\n\t)\n\tuassert.Equal(t, err.Error(), message)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"av9yZdxFZYqzGdWs0NUCCGYpBug/O8/+wW1hjMfbEDJx+Pj+N/yoTc/hLNFkpLULjj5/tQDXQ2D0AV2iYj88zA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"NHCgQsxlxjDIuR/qY3f5C4fJCGUMzv9TwCObVy5SBDodiGWF/DO5V24zCrDEkBvrBu1Sd3BcGgCKx4EfySuWxw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"agent","path":"gno.land/p/demo/gnorkle/agent/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/gnorkle/agent/v0\"\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"},{"name":"whitelist_test.gno","body":"package agent_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/gnorkle/agent/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nfunc TestWhitelist(t *testing.T) {\n\tvar whitelist agent.Whitelist\n\n\tuassert.False(t, whitelist.HasDefinition(), \"whitelist should not be defined initially\")\n\n\twhitelist.AddAddresses([]string{\"a\", \"b\"})\n\tuassert.True(t, whitelist.HasAddress(\"a\"), `whitelist should have address \"a\"`)\n\tuassert.True(t, whitelist.HasAddress(\"b\"), `whitelist should have address \"b\"`)\n\tuassert.True(t, whitelist.HasDefinition(), \"whitelist should be defined after adding addresses\")\n\n\twhitelist.RemoveAddress(\"a\")\n\tuassert.False(t, whitelist.HasAddress(\"a\"), `whitelist should not have address \"a\"`)\n\tuassert.True(t, whitelist.HasAddress(\"b\"), `whitelist should still have address \"b\"`)\n\n\twhitelist.ClearAddresses()\n\tuassert.False(t, whitelist.HasAddress(\"a\"), `whitelist cleared; should not have address \"a\"`)\n\tuassert.False(t, whitelist.HasAddress(\"b\"), `whitelist cleared; should still have address \"b\"`)\n\tuassert.False(t, whitelist.HasDefinition(), \"whitelist cleared; should not be defined\")\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"8jRQ0S97Vr/trxfqTnVqVLPwRRCfQWZeTK0AWC4sclt8cEHK4SvhMYju4dWapkKJQley5La8qz9n6bKDBbaYpQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"feed","path":"gno.land/p/demo/gnorkle/feed/v0","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/v0\"\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"1erO5OcUjmKfhymHEg8CdHM5q5XEYz7NRbQoaz6PQkNaTLb5EFx/GhetQGe0w+/Oh4RxoOnYdpfc9KViuY+yAw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"ingester","path":"gno.land/p/demo/gnorkle/ingester/v0","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/v0\"\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"X69323MudpnPx18uDYapnu74wyLaurVN/E9ODt01k7sb5hVF5XY+zq0n60OdWgKvYpF3RJpUIOZSvgdLr9RwkA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"message","path":"gno.land/p/demo/gnorkle/message/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/gnorkle/message/v0\"\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":"parse_test.gno","body":"package message_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/gnorkle/message/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nfunc TestParseFunc(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tinput        string\n\t\texpFuncType  message.FuncType\n\t\texpRemainder string\n\t}{\n\t\t{\n\t\t\tname: \"empty\",\n\t\t},\n\t\t{\n\t\t\tname:        \"func only\",\n\t\t\tinput:       \"ingest\",\n\t\t\texpFuncType: message.FuncTypeIngest,\n\t\t},\n\t\t{\n\t\t\tname:         \"func with short remainder\",\n\t\t\tinput:        \"commit,asdf\",\n\t\t\texpFuncType:  message.FuncTypeCommit,\n\t\t\texpRemainder: \"asdf\",\n\t\t},\n\t\t{\n\t\t\tname:         \"func with long remainder\",\n\t\t\tinput:        \"request,hello,world,goodbye\",\n\t\t\texpFuncType:  message.FuncTypeRequest,\n\t\t\texpRemainder: \"hello,world,goodbye\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tfuncType, remainder := message.ParseFunc(tt.input)\n\n\t\t\tuassert.Equal(t, string(tt.expFuncType), string(funcType))\n\t\t\tuassert.Equal(t, tt.expRemainder, remainder)\n\t\t})\n\t}\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"7KWmQ5+VydSSGC+plokqvai7coEAqE17TJva4gHV1n4k/LozV8yIaa3gTrmyxf9Ucg2/yVmDtixjYQLGR5Hldw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"gnorkle","path":"gno.land/p/demo/gnorkle/gnorkle/v0","files":[{"name":"feed.gno","body":"package gnorkle\n\nimport (\n\t\"gno.land/p/demo/gnorkle/feed/v0\"\n\t\"gno.land/p/demo/gnorkle/message/v0\"\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/v0\"\ngno = \"0.9\"\n"},{"name":"ingester.gno","body":"package gnorkle\n\nimport \"gno.land/p/demo/gnorkle/ingester/v0\"\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/v0\"\n\t\"gno.land/p/demo/gnorkle/feed/v0\"\n\t\"gno.land/p/demo/gnorkle/message/v0\"\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/v0\"\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"LqiCJFByz9DYhE4wPmAWIZi1KwqKuGYv5oShWmJBL8VYd//HxFroDPTLKKPpW5UT+CLsrr5VAH5Qe895umFS8Q=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"storage","path":"gno.land/p/demo/gnorkle/storage/v0","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/v0\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"EWoJx+B1igyHtxTEMyRceSxVYcaevW74eKQAiOWgFksnz2N8SBqfQf9bcnPwo8W1YmBj/A+hhJHBA0LgW5/dgA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"simple","path":"gno.land/p/demo/gnorkle/storage/simple/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/gnorkle/storage/simple/v0\"\ngno = \"0.9\"\n"},{"name":"storage.gno","body":"package simple\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/demo/gnorkle/feed/v0\"\n\t\"gno.land/p/demo/gnorkle/storage/v0\"\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"},{"name":"storage_test.gno","body":"package simple_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/gnorkle/storage/simple/v0\"\n\t\"gno.land/p/demo/gnorkle/storage/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestStorage(t *testing.T) {\n\tvar undefinedStorage *simple.Storage\n\terr := undefinedStorage.Put(\"\")\n\tuassert.ErrorIs(t, err, storage.ErrUndefined, \"expected storage.ErrUndefined on undefined storage\")\n\n\ttests := []struct {\n\t\tname                      string\n\t\tvaluesToPut               []string\n\t\texpLatestValueString      string\n\t\texpLatestValueTimeIsZero  bool\n\t\texpHistoricalValueStrings []string\n\t}{\n\t\t{\n\t\t\tname:                     \"empty\",\n\t\t\texpLatestValueTimeIsZero: true,\n\t\t},\n\t\t{\n\t\t\tname:                      \"one value\",\n\t\t\tvaluesToPut:               []string{\"one\"},\n\t\t\texpLatestValueString:      \"one\",\n\t\t\texpHistoricalValueStrings: []string{\"one\"},\n\t\t},\n\t\t{\n\t\t\tname:                      \"two values\",\n\t\t\tvaluesToPut:               []string{\"one\", \"two\"},\n\t\t\texpLatestValueString:      \"two\",\n\t\t\texpHistoricalValueStrings: []string{\"one\", \"two\"},\n\t\t},\n\t\t{\n\t\t\tname:                      \"three values\",\n\t\t\tvaluesToPut:               []string{\"one\", \"two\", \"three\"},\n\t\t\texpLatestValueString:      \"three\",\n\t\t\texpHistoricalValueStrings: []string{\"two\", \"three\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tsimpleStorage := simple.NewStorage(2)\n\t\t\tfor _, value := range tt.valuesToPut {\n\t\t\t\terr := simpleStorage.Put(value)\n\t\t\t\turequire.NoError(t, err, \"unexpected error putting value in storage\")\n\t\t\t}\n\n\t\t\tlatestValue := simpleStorage.GetLatest()\n\t\t\tuassert.Equal(t, tt.expLatestValueString, latestValue.String)\n\t\t\tuassert.Equal(t, tt.expLatestValueTimeIsZero, latestValue.Time.IsZero())\n\n\t\t\thistoricalValues := simpleStorage.GetHistory()\n\t\t\turequire.Equal(t, len(tt.expHistoricalValueStrings), len(historicalValues), \"historical values length does not match\")\n\n\t\t\tfor i, expValue := range tt.expHistoricalValueStrings {\n\t\t\t\tuassert.Equal(t, historicalValues[i].String, expValue)\n\t\t\t\turequire.False(t, historicalValues[i].Time.IsZero(), ufmt.Sprintf(\"unexpeced zero time for historical value at index %d\", i))\n\t\t\t}\n\t\t})\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"QYoJyy3XcBz716yDyspYoMpcKlwWcYAHAS1Q/durK8oJovktR6U0A9s9UPODkNGssO4Hqf1KnNAETW+n2eycjw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"single","path":"gno.land/p/demo/gnorkle/ingesters/single/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/gnorkle/ingesters/single/v0\"\ngno = \"0.9\"\n"},{"name":"ingester.gno","body":"package single\n\nimport (\n\t\"gno.land/p/demo/gnorkle/gnorkle/v0\"\n\t\"gno.land/p/demo/gnorkle/ingester/v0\"\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"},{"name":"ingester_test.gno","body":"package single_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/gnorkle/ingester/v0\"\n\t\"gno.land/p/demo/gnorkle/ingesters/single/v0\"\n\t\"gno.land/p/demo/gnorkle/storage/simple/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nfunc TestValueIngester(t *testing.T) {\n\tstorage := simple.NewStorage(1)\n\n\tvar undefinedIngester *single.ValueIngester\n\t_, err := undefinedIngester.Ingest(\"asdf\", \"gno11111\")\n\tuassert.ErrorIs(t, err, ingester.ErrUndefined, \"undefined ingester call to Ingest should return ingester.ErrUndefined\")\n\n\terr = undefinedIngester.CommitValue(storage, \"gno11111\")\n\tuassert.ErrorIs(t, err, ingester.ErrUndefined, \"undefined ingester call to CommitValue should return ingester.ErrUndefined\")\n\n\tvar valueIngester single.ValueIngester\n\ttyp := valueIngester.Type()\n\tuassert.Equal(t, int(ingester.TypeSingle), int(typ), \"single value ingester should return type ingester.TypeSingle\")\n\n\tingestValue := \"value\"\n\tautocommit, err := valueIngester.Ingest(ingestValue, \"gno11111\")\n\tuassert.True(t, autocommit, \"single value ingester should return autocommit true\")\n\tuassert.NoError(t, err)\n\n\terr = valueIngester.CommitValue(storage, \"gno11111\")\n\tuassert.NoError(t, err)\n\n\tlatestValue := storage.GetLatest()\n\tuassert.Equal(t, ingestValue, latestValue.String)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"vPn4RLK6cYQYVry4FOcXxzgYTJmrqIAeU3jk5X0c0IoCZCNtELrtzF76eX8IO4IouXOFP1T2J56p8tXme+8trg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"static","path":"gno.land/p/demo/gnorkle/feeds/static/v0","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/v0\"\n\t\"gno.land/p/demo/gnorkle/gnorkle/v0\"\n\t\"gno.land/p/demo/gnorkle/ingesters/single/v0\"\n\t\"gno.land/p/demo/gnorkle/message/v0\"\n\t\"gno.land/p/demo/gnorkle/storage/simple/v0\"\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":"feed_test.gno","body":"package static_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/gnorkle/feed/v0\"\n\t\"gno.land/p/demo/gnorkle/feeds/static/v0\"\n\t\"gno.land/p/demo/gnorkle/gnorkle/v0\"\n\t\"gno.land/p/demo/gnorkle/ingester/v0\"\n\t\"gno.land/p/demo/gnorkle/message/v0\"\n\t\"gno.land/p/demo/gnorkle/storage/simple/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\ntype mockIngester struct {\n\tcanAutoCommit   bool\n\tingestErr       error\n\tcommitErr       error\n\tvalue           string\n\tproviderAddress string\n}\n\nfunc (i mockIngester) Type() ingester.Type {\n\treturn ingester.Type(0)\n}\n\nfunc (i *mockIngester) Ingest(value, providerAddress string) (bool, error) {\n\tif i.ingestErr != nil {\n\t\treturn false, i.ingestErr\n\t}\n\n\ti.value = value\n\ti.providerAddress = providerAddress\n\treturn i.canAutoCommit, nil\n}\n\nfunc (i *mockIngester) CommitValue(storage gnorkle.Storage, providerAddress string) error {\n\tif i.commitErr != nil {\n\t\treturn i.commitErr\n\t}\n\n\treturn storage.Put(i.value)\n}\n\nfunc TestNewSingleValueFeed(t *testing.T) {\n\tstaticFeed := static.NewSingleValueFeed(\"1\", \"\")\n\n\tuassert.Equal(t, \"1\", staticFeed.ID())\n\tuassert.Equal(t, int(feed.TypeStatic), int(staticFeed.Type()))\n}\n\nfunc TestFeed_Ingest(t *testing.T) {\n\tvar undefinedFeed *static.Feed\n\terr := undefinedFeed.Ingest(\"\", \"\", \"\")\n\tuassert.ErrorIs(t, err, feed.ErrUndefined)\n\n\ttests := []struct {\n\t\tname               string\n\t\tingester           *mockIngester\n\t\tverifyIsLocked     bool\n\t\tdoCommit           bool\n\t\tfuncType           message.FuncType\n\t\tmsg                string\n\t\tproviderAddress    string\n\t\texpFeedValueString string\n\t\texpErrText         string\n\t\texpIsActive        bool\n\t}{\n\t\t{\n\t\t\tname:        \"func invalid error\",\n\t\t\tingester:    \u0026mockIngester{},\n\t\t\tfuncType:    message.FuncType(\"derp\"),\n\t\t\texpErrText:  \"invalid message function derp\",\n\t\t\texpIsActive: true,\n\t\t},\n\t\t{\n\t\t\tname: \"func ingest ingest error\",\n\t\t\tingester: \u0026mockIngester{\n\t\t\t\tingestErr: errors.New(\"ingest error\"),\n\t\t\t},\n\t\t\tfuncType:    message.FuncTypeIngest,\n\t\t\texpErrText:  \"ingest error\",\n\t\t\texpIsActive: true,\n\t\t},\n\t\t{\n\t\t\tname: \"func ingest commit error\",\n\t\t\tingester: \u0026mockIngester{\n\t\t\t\tcommitErr:     errors.New(\"commit error\"),\n\t\t\t\tcanAutoCommit: true,\n\t\t\t},\n\t\t\tfuncType:    message.FuncTypeIngest,\n\t\t\texpErrText:  \"commit error\",\n\t\t\texpIsActive: true,\n\t\t},\n\t\t{\n\t\t\tname: \"func commit commit error\",\n\t\t\tingester: \u0026mockIngester{\n\t\t\t\tcommitErr:     errors.New(\"commit error\"),\n\t\t\t\tcanAutoCommit: true,\n\t\t\t},\n\t\t\tfuncType:    message.FuncTypeCommit,\n\t\t\texpErrText:  \"commit error\",\n\t\t\texpIsActive: true,\n\t\t},\n\t\t{\n\t\t\tname:            \"only ingest\",\n\t\t\tingester:        \u0026mockIngester{},\n\t\t\tfuncType:        message.FuncTypeIngest,\n\t\t\tmsg:             \"still active feed\",\n\t\t\tproviderAddress: \"gno1234\",\n\t\t\texpIsActive:     true,\n\t\t},\n\t\t{\n\t\t\tname:               \"ingest autocommit\",\n\t\t\tingester:           \u0026mockIngester{canAutoCommit: true},\n\t\t\tfuncType:           message.FuncTypeIngest,\n\t\t\tmsg:                \"still active feed\",\n\t\t\tproviderAddress:    \"gno1234\",\n\t\t\texpFeedValueString: \"still active feed\",\n\t\t\tverifyIsLocked:     true,\n\t\t},\n\t\t{\n\t\t\tname:           \"commit no value\",\n\t\t\tingester:       \u0026mockIngester{},\n\t\t\tfuncType:       message.FuncTypeCommit,\n\t\t\tmsg:            \"shouldn't be stored\",\n\t\t\tverifyIsLocked: true,\n\t\t},\n\t\t{\n\t\t\tname:               \"ingest then commmit\",\n\t\t\tingester:           \u0026mockIngester{},\n\t\t\tfuncType:           message.FuncTypeIngest,\n\t\t\tmsg:                \"blahblah\",\n\t\t\tdoCommit:           true,\n\t\t\texpFeedValueString: \"blahblah\",\n\t\t\tverifyIsLocked:     true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tstaticFeed := static.NewFeed(\n\t\t\t\t\"1\",\n\t\t\t\t\"string\",\n\t\t\t\ttt.ingester,\n\t\t\t\tsimple.NewStorage(1),\n\t\t\t\tnil,\n\t\t\t)\n\n\t\t\tvar errText string\n\t\t\tif err := staticFeed.Ingest(tt.funcType, tt.msg, tt.providerAddress); err != nil {\n\t\t\t\terrText = err.Error()\n\t\t\t}\n\n\t\t\turequire.Equal(t, tt.expErrText, errText)\n\n\t\t\tif tt.doCommit {\n\t\t\t\terr := staticFeed.Ingest(message.FuncTypeCommit, \"\", \"\")\n\t\t\t\turequire.NoError(t, err, \"follow up commit failed\")\n\t\t\t}\n\n\t\t\tif tt.verifyIsLocked {\n\t\t\t\terrText = \"\"\n\t\t\t\tif err := staticFeed.Ingest(tt.funcType, tt.msg, tt.providerAddress); err != nil {\n\t\t\t\t\terrText = err.Error()\n\t\t\t\t}\n\n\t\t\t\turequire.Equal(t, \"feed locked\", errText)\n\t\t\t}\n\n\t\t\tuassert.Equal(t, tt.providerAddress, tt.ingester.providerAddress)\n\n\t\t\tfeedValue, dataType, isLocked := staticFeed.Value()\n\t\t\tuassert.Equal(t, tt.expFeedValueString, feedValue.String)\n\t\t\tuassert.Equal(t, \"string\", dataType)\n\t\t\tuassert.Equal(t, tt.verifyIsLocked, isLocked)\n\t\t\tuassert.Equal(t, tt.expIsActive, staticFeed.IsActive())\n\t\t})\n\t}\n}\n\ntype mockTask struct {\n\terr   error\n\tvalue string\n}\n\nfunc (t mockTask) MarshalJSON() ([]byte, error) {\n\tif t.err != nil {\n\t\treturn nil, t.err\n\t}\n\n\treturn []byte(`{\"value\":\"` + t.value + `\"}`), nil\n}\n\nfunc TestFeed_Tasks(t *testing.T) {\n\tid := \"99\"\n\tvalueDataType := \"int\"\n\n\ttests := []struct {\n\t\tname       string\n\t\ttasks      []feed.Task\n\t\texpErrText string\n\t\texpJSON    string\n\t}{\n\t\t{\n\t\t\tname:    \"no tasks\",\n\t\t\texpJSON: `{\"id\":\"99\",\"type\":\"0\",\"value_type\":\"int\",\"tasks\":[]}`,\n\t\t},\n\t\t{\n\t\t\tname: \"marshal error\",\n\t\t\ttasks: []feed.Task{\n\t\t\t\tmockTask{err: errors.New(\"marshal error\")},\n\t\t\t},\n\t\t\texpErrText: \"marshal error\",\n\t\t},\n\t\t{\n\t\t\tname: \"one task\",\n\t\t\ttasks: []feed.Task{\n\t\t\t\tmockTask{value: \"single\"},\n\t\t\t},\n\t\t\texpJSON: `{\"id\":\"99\",\"type\":\"0\",\"value_type\":\"int\",\"tasks\":[{\"value\":\"single\"}]}`,\n\t\t},\n\t\t{\n\t\t\tname: \"two tasks\",\n\t\t\ttasks: []feed.Task{\n\t\t\t\tmockTask{value: \"first\"},\n\t\t\t\tmockTask{value: \"second\"},\n\t\t\t},\n\t\t\texpJSON: `{\"id\":\"99\",\"type\":\"0\",\"value_type\":\"int\",\"tasks\":[{\"value\":\"first\"},{\"value\":\"second\"}]}`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tstaticFeed := static.NewSingleValueFeed(\n\t\t\t\tid,\n\t\t\t\tvalueDataType,\n\t\t\t\ttt.tasks...,\n\t\t\t)\n\n\t\t\turequire.Equal(t, len(tt.tasks), len(staticFeed.Tasks()))\n\n\t\t\tvar errText string\n\t\t\tjson, err := staticFeed.MarshalJSON()\n\t\t\tif err != nil {\n\t\t\t\terrText = err.Error()\n\t\t\t}\n\n\t\t\turequire.Equal(t, tt.expErrText, errText)\n\t\t\turequire.Equal(t, tt.expJSON, string(json))\n\t\t})\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/gnorkle/feeds/static/v0\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"jes7I1RkO9pqcqqspwqLMXo4NSmqwDB8hzjTGjR7MWIiEj9DaB4eIQSYw+LNfr9Wt22HJartrw1/Z0hqdTqn9Q=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"launderpkg","path":"gno.land/p/demo/tests/launderpkg/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/tests/launderpkg/v0\"\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/v0.\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"/yFc7+AjmOBuLDUdSrYDgeqf3/8QjtLs6UnAeCD2jYE32v+8vasTWobJwihJHicTRJYSKaf/UPCKRYDJ6H8/KQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"launderattack","path":"gno.land/p/demo/tests/launderattack/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/tests/launderattack/v0\"\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/v0\"\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"Ol1AM9tWzc28+ZIyemasY6DhwGZvpSExS2AOj9zgH1IOb7A+7DNBwP288+D/k2M1temmMJvx8K9eisN7HLLEUQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"p_closurecap","path":"gno.land/p/demo/tests/p_closurecap/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/tests/p_closurecap/v0\"\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"l7x0+SF41VVA1GGhkRX2M/ThgAazfHDDd5MgozXjk7w7zpf3KJmzGHRAswEOG2JkURySGx5Q/sLwyUPLNixAKA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"p_crossrealm","path":"gno.land/p/demo/tests/p_crossrealm/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/tests/p_crossrealm/v0\"\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"xZVQ26Dp2JeLZtpdtaoAn9DepoEwAcKDzj+LjTwYJ7ls7gd5wuUtwvF34cw1ECqEOA7nkm7FrLWx9Fh7hSbIZw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"subtests","path":"gno.land/p/demo/tests/subtests/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/tests/subtests/v0\"\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"D0Po0moXzuqatW+fYLUaH0BwR1+a2fC/m3MWPZ97r0s+zidTvm5K9vjHdEsrE8i9g+KfjiACmtD80TuD2HX8Hw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"tests","path":"gno.land/p/demo/tests/v0","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/v0\"\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/v0\"\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"},{"name":"tests_test.gno","body":"package tests_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/tests/v0\"\n)\n\nvar World = \"WORLD\"\n\nfunc TestGetHelloWorld(t *testing.T) {\n\t// tests.World is 'world'\n\ts := \"hello \" + tests.World + World\n\tconst want = \"hello worldWORLD\"\n\tif s != want {\n\t\tt.Error(\"not the same\")\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"b48HTFRq5XivULXIF3uEUOb1J4zQDvC/a7iSilm+dk5xP82kBIuVFKaTtD7bXWaRP+GeqV4eOfkdkNV4TyYvlQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"blog","path":"gno.land/p/gnoland/blog/v0","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":"blog_test.gno","body":"package blog\n\n// TODO: add generic tests here.\n//       right now, you can checkout r/gnoland/blog/*_test.gno.\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/gnoland/blog/v0\"\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"oI9B/RBI8hTv+4dC79rzt1CSRqpm4nIBuoHjJmn1C+Q+ikWOZIpwg+RwN2+0jcfeFKZRiicFMMJ6bwbli8OmMQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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/list/v0` - ordered list built on top of `BPTree`.\n- `gno.land/p/nt/bptree/pager/v0` - pagination helper for trees and lists.\n- `gno.land/p/nt/bptree/rotree/v0` - 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":"example_test.gno","body":"package bptree\n\n// ExampleNew shows NewBPTree32() and adding a value\nfunc ExampleNew() {\n\tvar tree *BPTree\n\ttree = NewBPTree32()\n\ttree.Set(\"key0\", \"value0\")\n\n\tvar updated bool\n\tupdated = tree.Set(\"key1\", \"value1\")\n\tprintln(updated, tree.Size())\n\n\t// Output:\n\t// false 2\n}\n\n// ExampleUpdate shows NewBPTree32() and updating a value\nfunc ExampleUpdate() {\n\tvar tree *BPTree\n\ttree = NewBPTree32()\n\ttree.Set(\"key0\", \"value0\")\n\n\tvar updated bool\n\tupdated = tree.Set(\"key0\", \"new_value0\")\n\tprintln(updated, tree.Size())\n\n\t// Output:\n\t// true 1\n}\n\n// ExampleZeroValue shows updating the zero value of a BPTree without NewBPTree32()\nfunc ExampleZeroValue() {\n\tvar tree BPTree\n\ttree.Set(\"key0\", \"value0\")\n\ttree.Set(\"key1\", \"value1\")\n\n\tvar updated bool\n\tupdated = tree.Set(\"key2\", \"value2\")\n\tprintln(updated, tree.Size())\n\n\t// Output:\n\t// false 3\n}\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"},{"name":"tree_test.gno","body":"package bptree\n\nimport (\n\t\"math/rand\"\n\t\"sort\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/avl/v0\"\n)\n\n//----------------------------------------\n// Basic operations\n\nfunc TestNewTree(t *testing.T) {\n\ttree := NewBPTree32()\n\tif tree.Size() != 0 {\n\t\tt.Error(\"Expected empty tree size to be 0\")\n\t}\n}\n\nfunc TestZeroValue(t *testing.T) {\n\tvar tree BPTree\n\tif tree.Size() != 0 {\n\t\tt.Error(\"Expected zero-value tree size to be 0\")\n\t}\n\tif tree.Has(\"x\") {\n\t\tt.Error(\"Expected Has to return false on zero-value tree\")\n\t}\n\tif v := tree.Get(\"x\"); v != nil {\n\t\tt.Errorf(\"Expected Get to return nil on zero-value tree, got %v\", v)\n\t}\n\tif _, ok := tree.Remove(\"x\"); ok {\n\t\tt.Error(\"Expected Remove to return false on zero-value tree\")\n\t}\n\n\t// Set should work on zero-value tree.\n\tif updated := tree.Set(\"a\", 1); updated {\n\t\tt.Error(\"Expected Set to return false for new key\")\n\t}\n\tif tree.Size() != 1 {\n\t\tt.Errorf(\"Expected size 1, got %d\", tree.Size())\n\t}\n\tif v := tree.Get(\"a\"); v != 1 {\n\t\tt.Errorf(\"Expected Get(a) = 1, got %v\", v)\n\t}\n}\n\nfunc TestSetAndGet(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\n\tif tree.Size() != 2 {\n\t\tt.Errorf(\"Expected size 2, got %d\", tree.Size())\n\t}\n\n\tif v := tree.Get(\"key1\"); v != \"value1\" {\n\t\tt.Errorf(\"Expected value1, got %v\", v)\n\t}\n\n\tif v := tree.Get(\"missing\"); v != nil {\n\t\tt.Errorf(\"Expected Get to return nil for missing key, got %v\", v)\n\t}\n}\n\nfunc TestSetUpdate(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\tif updated := tree.Set(\"k\", \"v1\"); updated {\n\t\tt.Error(\"Expected false for new key\")\n\t}\n\tif updated := tree.Set(\"k\", \"v2\"); !updated {\n\t\tt.Error(\"Expected true for existing key\")\n\t}\n\tif tree.Size() != 1 {\n\t\tt.Errorf(\"Expected size 1 after update, got %d\", tree.Size())\n\t}\n\tif v := tree.Get(\"k\"); v != \"v2\" {\n\t\tt.Errorf(\"Expected v2, got %v\", v)\n\t}\n}\n\nfunc TestHas(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\ttree.Set(\"a\", 1)\n\tif !tree.Has(\"a\") {\n\t\tt.Error(\"Expected Has(a) = true\")\n\t}\n\tif tree.Has(\"b\") {\n\t\tt.Error(\"Expected Has(b) = false\")\n\t}\n}\n\nfunc TestRemove(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\ttree.Set(\"a\", 1)\n\ttree.Set(\"b\", 2)\n\n\tv, ok := tree.Remove(\"a\")\n\tif !ok || v != 1 {\n\t\tt.Errorf(\"Expected (1, true), got (%v, %v)\", v, ok)\n\t}\n\tif tree.Size() != 1 {\n\t\tt.Errorf(\"Expected size 1, got %d\", tree.Size())\n\t}\n\tif tree.Has(\"a\") {\n\t\tt.Error(\"Expected Has(a) = false after remove\")\n\t}\n\n\tv, ok = tree.Remove(\"missing\")\n\tif ok || v != nil {\n\t\tt.Errorf(\"Expected (nil, false), got (%v, %v)\", v, ok)\n\t}\n}\n\nfunc TestRemoveLastKey(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\ttree.Set(\"a\", 1)\n\ttree.Remove(\"a\")\n\tif tree.Size() != 0 {\n\t\tt.Errorf(\"Expected size 0, got %d\", tree.Size())\n\t}\n\n\t// Insert after removing everything.\n\ttree.Set(\"b\", 2)\n\tif tree.Size() != 1 {\n\t\tt.Errorf(\"Expected size 1, got %d\", tree.Size())\n\t}\n\tif v := tree.Get(\"b\"); v != 2 {\n\t\tt.Errorf(\"Expected 2, got %v\", v)\n\t}\n}\n\nfunc TestNilValue(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\ttree.Set(\"k\", nil)\n\t// Has distinguishes a nil-valued entry from a missing key (Get returns\n\t// nil for both cases, by design).\n\tif !tree.Has(\"k\") {\n\t\tt.Error(\"Expected Has(k) = true for nil-valued entry\")\n\t}\n\tif v := tree.Get(\"k\"); v != nil {\n\t\tt.Errorf(\"Expected nil value, got %v\", v)\n\t}\n\tv, ok := tree.Remove(\"k\")\n\tif !ok {\n\t\tt.Error(\"Expected remove to succeed\")\n\t}\n\tif v != nil {\n\t\tt.Errorf(\"Expected nil removed value, got %v\", v)\n\t}\n}\n\nfunc TestEmptyStringKey(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\ttree.Set(\"\", \"empty\")\n\ttree.Set(\"a\", \"alpha\")\n\ttree.Set(\"b\", \"beta\")\n\n\tif !tree.Has(\"\") {\n\t\tt.Error(\"Expected Has('') = true for empty string key\")\n\t}\n\tif v := tree.Get(\"\"); v != \"empty\" {\n\t\tt.Errorf(\"Expected empty, got %v\", v)\n\t}\n\tif tree.Size() != 3 {\n\t\tt.Errorf(\"Expected size 3, got %d\", tree.Size())\n\t}\n\n\t// Remove \"\" key.\n\tv, ok := tree.Remove(\"\")\n\tif !ok || v != \"empty\" {\n\t\tt.Errorf(\"Remove('') = (%v, %v), want (empty, true)\", v, ok)\n\t}\n\tif tree.Size() != 2 {\n\t\tt.Errorf(\"Expected size 2, got %d\", tree.Size())\n\t}\n\tif tree.Has(\"\") {\n\t\tt.Error(\"Expected Has('') = false after remove\")\n\t}\n}\n\nfunc TestGetMissingReturnsNil(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\ttree.Set(\"a\", 1)\n\tif v := tree.Get(\"missing\"); v != nil {\n\t\tt.Errorf(\"Expected nil value for missing key, got %v\", v)\n\t}\n}\n\nfunc TestRemoveMissingReturnsNilFalse(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\ttree.Set(\"a\", 1)\n\tv, ok := tree.Remove(\"missing\")\n\tif v != nil {\n\t\tt.Errorf(\"Expected nil value for missing remove, got %v\", v)\n\t}\n\tif ok {\n\t\tt.Error(\"Expected false for missing remove\")\n\t}\n\n\t// Also on empty tree.\n\tvar empty BPTree\n\tv, ok = empty.Remove(\"x\")\n\tif v != nil || ok {\n\t\tt.Errorf(\"Remove on empty tree: got (%v, %v), want (nil, false)\", v, ok)\n\t}\n}\n\nfunc TestFanoutNeverResets(t *testing.T) {\n\tvar tree BPTree\n\ttree.Set(\"a\", 1)\n\ttree.Remove(\"a\")\n\t// Tree is empty again, but fanout should still be 32.\n\ttree.Set(\"b\", 2)\n\tif tree.Size() != 1 {\n\t\tt.Errorf(\"Expected size 1, got %d\", tree.Size())\n\t}\n\tif tree.fanout != 32 {\n\t\tt.Errorf(\"Expected fanout 32 after re-insert, got %d\", tree.fanout)\n\t}\n}\n\nfunc TestSingleEntry(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\ttree.Set(\"x\", 42)\n\n\t// Root should be a leaf for single entry.\n\tif tree.root == nil {\n\t\tt.Fatal(\"root is nil\")\n\t}\n\tif !tree.root.isLeaf() {\n\t\tt.Error(\"root should be a leaf for single entry\")\n\t}\n\tif tree.Size() != 1 {\n\t\tt.Errorf(\"Expected size 1, got %d\", tree.Size())\n\t}\n\tk, v := tree.GetByIndex(0)\n\tif k != \"x\" || v != 42 {\n\t\tt.Errorf(\"GetByIndex(0) = (%v, %v), want (x, 42)\", k, v)\n\t}\n}\n\n//----------------------------------------\n// GetByIndex\n\nfunc TestGetByIndex(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\ttree.Set(\"c\", 3)\n\ttree.Set(\"a\", 1)\n\ttree.Set(\"b\", 2)\n\n\tk, v := tree.GetByIndex(0)\n\tif k != \"a\" || v != 1 {\n\t\tt.Errorf(\"GetByIndex(0) = (%v, %v), want (a, 1)\", k, v)\n\t}\n\tk, v = tree.GetByIndex(1)\n\tif k != \"b\" || v != 2 {\n\t\tt.Errorf(\"GetByIndex(1) = (%v, %v), want (b, 2)\", k, v)\n\t}\n\tk, v = tree.GetByIndex(2)\n\tif k != \"c\" || v != 3 {\n\t\tt.Errorf(\"GetByIndex(2) = (%v, %v), want (c, 3)\", k, v)\n\t}\n}\n\nfunc TestGetByIndexPanics(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\ttree.Set(\"a\", 1)\n\n\tassertPanics(t, \"empty tree\", func() {\n\t\tvar empty BPTree\n\t\tempty.GetByIndex(0)\n\t})\n\tassertPanics(t, \"negative index\", func() {\n\t\ttree.GetByIndex(-1)\n\t})\n\tassertPanics(t, \"index == size\", func() {\n\t\ttree.GetByIndex(1)\n\t})\n}\n\n//----------------------------------------\n// Iterate\n\nfunc TestIterate(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\ttree.Set(\"a\", 1)\n\ttree.Set(\"b\", 2)\n\ttree.Set(\"c\", 3)\n\ttree.Set(\"d\", 4)\n\ttree.Set(\"e\", 5)\n\n\t// Full iteration.\n\tgot := collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.Iterate(\"\", \"\", cb)\n\t})\n\tassertSliceEqual(t, got, []string{\"a\", \"b\", \"c\", \"d\", \"e\"})\n\n\t// Bounded iteration [b, d) → b, c.\n\tgot = collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.Iterate(\"b\", \"d\", cb)\n\t})\n\tassertSliceEqual(t, got, []string{\"b\", \"c\"})\n\n\t// start == end → empty.\n\tgot = collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.Iterate(\"b\", \"b\", cb)\n\t})\n\tassertSliceEqual(t, got, nil)\n\n\t// start \u003e end → empty.\n\tgot = collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.Iterate(\"z\", \"a\", cb)\n\t})\n\tassertSliceEqual(t, got, nil)\n\n\t// No lower bound.\n\tgot = collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.Iterate(\"\", \"c\", cb)\n\t})\n\tassertSliceEqual(t, got, []string{\"a\", \"b\"})\n\n\t// Empty tree.\n\tvar empty BPTree\n\tstopped := empty.Iterate(\"\", \"\", func(k string, v any) bool {\n\t\tt.Error(\"should not be called\")\n\t\treturn false\n\t})\n\tif stopped {\n\t\tt.Error(\"Expected false from Iterate on empty tree\")\n\t}\n}\n\nfunc TestIterateEarlyStop(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\ttree.Set(\"a\", 1)\n\ttree.Set(\"b\", 2)\n\ttree.Set(\"c\", 3)\n\n\tcount := 0\n\tstopped := tree.Iterate(\"\", \"\", func(k string, v any) bool {\n\t\tcount++\n\t\treturn true\n\t})\n\tif !stopped {\n\t\tt.Error(\"Expected true from early-stopped Iterate\")\n\t}\n\tif count != 1 {\n\t\tt.Errorf(\"Expected 1 callback, got %d\", count)\n\t}\n}\n\nfunc TestIterateAllEdgeCases(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\ttree.Set(\"a\", 1)\n\ttree.Set(\"b\", 2)\n\ttree.Set(\"c\", 3)\n\ttree.Set(\"d\", 4)\n\ttree.Set(\"e\", 5)\n\n\t// Iterate(\"a\", \"a\") → empty [a,a).\n\tgot := collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.Iterate(\"a\", \"a\", cb)\n\t})\n\tassertSliceEqual(t, got, nil)\n\n\t// Iterate(\"z\", \"a\") → empty (start \u003e end).\n\tgot = collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.Iterate(\"z\", \"a\", cb)\n\t})\n\tassertSliceEqual(t, got, nil)\n\n\t// Iterate(\"\", \"a\") → nothing \u003c \"a\".\n\tgot = collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.Iterate(\"\", \"a\", cb)\n\t})\n\tassertSliceEqual(t, got, nil)\n\n\t// Iterate(\"\", \"b\") → just \"a\".\n\tgot = collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.Iterate(\"\", \"b\", cb)\n\t})\n\tassertSliceEqual(t, got, []string{\"a\"})\n\n\t// ReverseIterate(\"a\", \"a\") → visits \"a\" (both inclusive).\n\tgot = collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.ReverseIterate(\"a\", \"a\", cb)\n\t})\n\tassertSliceEqual(t, got, []string{\"a\"})\n\n\t// ReverseIterate(\"z\", \"a\") → empty (bounds don't swap).\n\tgot = collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.ReverseIterate(\"z\", \"a\", cb)\n\t})\n\tassertSliceEqual(t, got, nil)\n\n\t// ReverseIterate(\"c\", \"\") → keys \u003e= \"c\" descending.\n\tgot = collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.ReverseIterate(\"c\", \"\", cb)\n\t})\n\tassertSliceEqual(t, got, []string{\"e\", \"d\", \"c\"})\n\n\t// Iterate(\"\", \"a\") on tree with key \"\" stored.\n\ttree2 := NewBPTreeN(4)\n\ttree2.Set(\"\", \"empty\")\n\ttree2.Set(\"a\", \"alpha\")\n\ttree2.Set(\"b\", \"beta\")\n\tgot = collectKeys(tree2, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.Iterate(\"\", \"a\", cb)\n\t})\n\tassertSliceEqual(t, got, []string{\"\"})\n\n\t// Iterate(\"\", \"\") visits all including \"\" key.\n\tgot = collectKeys(tree2, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.Iterate(\"\", \"\", cb)\n\t})\n\tassertSliceEqual(t, got, []string{\"\", \"a\", \"b\"})\n}\n\nfunc TestAllIterationEmptyTree(t *testing.T) {\n\tvar tree BPTree\n\tnoop := func(k string, v any) bool {\n\t\tt.Error(\"should not be called\")\n\t\treturn false\n\t}\n\tif tree.Iterate(\"\", \"\", noop) {\n\t\tt.Error(\"Iterate on empty tree should return false\")\n\t}\n\tif tree.ReverseIterate(\"\", \"\", noop) {\n\t\tt.Error(\"ReverseIterate on empty tree should return false\")\n\t}\n\tif tree.IterateByOffset(0, 1, noop) {\n\t\tt.Error(\"IterateByOffset on empty tree should return false\")\n\t}\n\tif tree.ReverseIterateByOffset(0, 1, noop) {\n\t\tt.Error(\"ReverseIterateByOffset on empty tree should return false\")\n\t}\n}\n\nfunc TestReverseIterate(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\ttree.Set(\"a\", 1)\n\ttree.Set(\"b\", 2)\n\ttree.Set(\"c\", 3)\n\ttree.Set(\"d\", 4)\n\ttree.Set(\"e\", 5)\n\n\t// Full reverse.\n\tgot := collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.ReverseIterate(\"\", \"\", cb)\n\t})\n\tassertSliceEqual(t, got, []string{\"e\", \"d\", \"c\", \"b\", \"a\"})\n\n\t// Bounded [b, d] inclusive → d, c, b.\n\tgot = collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.ReverseIterate(\"b\", \"d\", cb)\n\t})\n\tassertSliceEqual(t, got, []string{\"d\", \"c\", \"b\"})\n\n\t// start == end → visits that one key.\n\tgot = collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.ReverseIterate(\"c\", \"c\", cb)\n\t})\n\tassertSliceEqual(t, got, []string{\"c\"})\n\n\t// start \u003e end → empty.\n\tgot = collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.ReverseIterate(\"z\", \"a\", cb)\n\t})\n\tassertSliceEqual(t, got, nil)\n\n\t// No upper bound.\n\tgot = collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.ReverseIterate(\"c\", \"\", cb)\n\t})\n\tassertSliceEqual(t, got, []string{\"e\", \"d\", \"c\"})\n\n\t// end \u003e max key → e, d, c, b, a.\n\tgot = collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.ReverseIterate(\"a\", \"z\", cb)\n\t})\n\tassertSliceEqual(t, got, []string{\"e\", \"d\", \"c\", \"b\", \"a\"})\n}\n\nfunc TestIterateByOffset(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\ttree.Set(\"a\", 1)\n\ttree.Set(\"b\", 2)\n\ttree.Set(\"c\", 3)\n\ttree.Set(\"d\", 4)\n\ttree.Set(\"e\", 5)\n\n\tgot := collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.IterateByOffset(1, 3, cb)\n\t})\n\tassertSliceEqual(t, got, []string{\"b\", \"c\", \"d\"})\n\n\t// offset=0, count=0 → nothing.\n\tgot = collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.IterateByOffset(0, 0, cb)\n\t})\n\tassertSliceEqual(t, got, nil)\n\n\t// offset=size → nothing.\n\tgot = collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.IterateByOffset(5, 1, cb)\n\t})\n\tassertSliceEqual(t, got, nil)\n\n\t// count exceeds remaining.\n\tgot = collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.IterateByOffset(3, 100, cb)\n\t})\n\tassertSliceEqual(t, got, []string{\"d\", \"e\"})\n}\n\nfunc TestReverseIterateByOffset(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\ttree.Set(\"a\", 1)\n\ttree.Set(\"b\", 2)\n\ttree.Set(\"c\", 3)\n\ttree.Set(\"d\", 4)\n\ttree.Set(\"e\", 5)\n\n\t// Full reverse.\n\tgot := collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.ReverseIterateByOffset(0, 5, cb)\n\t})\n\tassertSliceEqual(t, got, []string{\"e\", \"d\", \"c\", \"b\", \"a\"})\n\n\t// offset=1, count=2 → [d, c].\n\tgot = collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.ReverseIterateByOffset(1, 2, cb)\n\t})\n\tassertSliceEqual(t, got, []string{\"d\", \"c\"})\n\n\t// offset=4, count=1 → [a].\n\tgot = collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.ReverseIterateByOffset(4, 1, cb)\n\t})\n\tassertSliceEqual(t, got, []string{\"a\"})\n\n\t// offset=4, count=10 → [a].\n\tgot = collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.ReverseIterateByOffset(4, 10, cb)\n\t})\n\tassertSliceEqual(t, got, []string{\"a\"})\n\n\t// offset \u003e= size → nothing.\n\tgot = collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.ReverseIterateByOffset(5, 1, cb)\n\t})\n\tassertSliceEqual(t, got, nil)\n\n\t// count=0 → nothing.\n\tgot = collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.ReverseIterateByOffset(0, 0, cb)\n\t})\n\tassertSliceEqual(t, got, nil)\n\n\t// negative offset\n\tgot = collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.ReverseIterateByOffset(-1, 3, cb)\n\t})\n\tassertSliceEqual(t, got, []string{\"e\", \"d\", \"c\"})\n}\n\nfunc TestReverseIterateEarlyStop(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\ttree.Set(\"a\", 1)\n\ttree.Set(\"b\", 2)\n\ttree.Set(\"c\", 3)\n\tcount := 0\n\tstopped := tree.ReverseIterate(\"\", \"\", func(k string, v any) bool {\n\t\tcount++\n\t\treturn true\n\t})\n\tif !stopped {\n\t\tt.Error(\"Expected true from early-stopped ReverseIterate\")\n\t}\n\tif count != 1 {\n\t\tt.Errorf(\"Expected 1 callback, got %d\", count)\n\t}\n}\n\n//----------------------------------------\n// Splits and merges (use small fanout to trigger them)\n\nfunc TestSplitAndMerge(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\n\t// Insert enough keys to cause multiple splits.\n\tkeys := []string{\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\", \"i\", \"j\", \"k\", \"l\"}\n\tfor _, k := range keys {\n\t\ttree.Set(k, k)\n\t}\n\tif tree.Size() != len(keys) {\n\t\tt.Errorf(\"Expected size %d, got %d\", len(keys), tree.Size())\n\t}\n\n\t// Verify all keys present and in order.\n\tgot := collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.Iterate(\"\", \"\", cb)\n\t})\n\tassertSliceEqual(t, got, keys)\n\n\t// Verify GetByIndex works for all positions.\n\tfor i, k := range keys {\n\t\tgk, gv := tree.GetByIndex(i)\n\t\tif gk != k || gv != k {\n\t\t\tt.Errorf(\"GetByIndex(%d) = (%v, %v), want (%v, %v)\", i, gk, gv, k, k)\n\t\t}\n\t}\n\n\t// Remove keys one by one and verify.\n\tfor _, k := range keys {\n\t\tv, ok := tree.Remove(k)\n\t\tif !ok || v != k {\n\t\t\tt.Errorf(\"Remove(%v) = (%v, %v), want (%v, true)\", k, v, ok, k)\n\t\t}\n\t}\n\tif tree.Size() != 0 {\n\t\tt.Errorf(\"Expected size 0 after removing all, got %d\", tree.Size())\n\t}\n}\n\nfunc TestRemoveFromMiddle(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\tfor _, k := range []string{\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\"} {\n\t\ttree.Set(k, k)\n\t}\n\n\t// Remove from the middle to trigger redistributions and merges.\n\ttree.Remove(\"d\")\n\ttree.Remove(\"e\")\n\ttree.Remove(\"b\")\n\ttree.Remove(\"g\")\n\n\tgot := collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.Iterate(\"\", \"\", cb)\n\t})\n\tassertSliceEqual(t, got, []string{\"a\", \"c\", \"f\", \"h\"})\n\tif tree.Size() != 4 {\n\t\tt.Errorf(\"Expected size 4, got %d\", tree.Size())\n\t}\n}\n\nfunc TestSequentialInsertRemove(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\n\t// Sequential insert.\n\tn := 100\n\tfor i := 0; i \u003c n; i++ {\n\t\tk := intToKey(i)\n\t\ttree.Set(k, i)\n\t}\n\tif tree.Size() != n {\n\t\tt.Errorf(\"Expected size %d, got %d\", n, tree.Size())\n\t}\n\n\t// Verify sorted order.\n\tprev := \"\"\n\ttree.Iterate(\"\", \"\", func(k string, v any) bool {\n\t\tif k \u003c= prev \u0026\u0026 prev != \"\" {\n\t\t\tt.Errorf(\"Keys not in order: %q after %q\", k, prev)\n\t\t}\n\t\tprev = k\n\t\treturn false\n\t})\n\n\t// Remove all in reverse order.\n\tfor i := n - 1; i \u003e= 0; i-- {\n\t\tk := intToKey(i)\n\t\t_, ok := tree.Remove(k)\n\t\tif !ok {\n\t\t\tt.Errorf(\"Remove(%v) failed\", k)\n\t\t}\n\t}\n\tif tree.Size() != 0 {\n\t\tt.Errorf(\"Expected size 0, got %d\", tree.Size())\n\t}\n}\n\nfunc TestRandomInsertRemove(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\n\t// Insert in \"random\" order (shuffled via simple hash).\n\tkeys := make([]string, 50)\n\tfor i := range keys {\n\t\tkeys[i] = intToKey((i*37 + 13) % 50)\n\t}\n\tfor _, k := range keys {\n\t\ttree.Set(k, k)\n\t}\n\tif tree.Size() != 50 {\n\t\tt.Errorf(\"Expected size 50, got %d\", tree.Size())\n\t}\n\n\t// Remove half.\n\tfor i := 0; i \u003c 25; i++ {\n\t\ttree.Remove(keys[i])\n\t}\n\tif tree.Size() != 25 {\n\t\tt.Errorf(\"Expected size 25, got %d\", tree.Size())\n\t}\n\n\t// Verify remaining keys are in sorted order.\n\tprev := \"\"\n\ttree.Iterate(\"\", \"\", func(k string, v any) bool {\n\t\tif k \u003c= prev \u0026\u0026 prev != \"\" {\n\t\t\tt.Errorf(\"Keys not in order: %q after %q\", k, prev)\n\t\t}\n\t\tprev = k\n\t\treturn false\n\t})\n}\n\nfunc TestDifferentFanouts(t *testing.T) {\n\tfor _, fanout := range []int{4, 5, 8, 16, 32} {\n\t\ttree := NewBPTreeN(fanout)\n\t\tn := 100\n\t\tfor i := 0; i \u003c n; i++ {\n\t\t\ttree.Set(intToKey(i), i)\n\t\t}\n\t\tif tree.Size() != n {\n\t\t\tt.Errorf(\"fanout=%d: expected size %d, got %d\", fanout, n, tree.Size())\n\t\t}\n\n\t\t// Verify order.\n\t\tgot := collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\t\treturn tr.Iterate(\"\", \"\", cb)\n\t\t})\n\t\tfor i := 1; i \u003c len(got); i++ {\n\t\t\tif got[i] \u003c= got[i-1] {\n\t\t\t\tt.Errorf(\"fanout=%d: keys not in order at %d\", fanout, i)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\t// Remove all.\n\t\tfor i := 0; i \u003c n; i++ {\n\t\t\ttree.Remove(intToKey(i))\n\t\t}\n\t\tif tree.Size() != 0 {\n\t\t\tt.Errorf(\"fanout=%d: expected size 0 after remove all, got %d\", fanout, tree.Size())\n\t\t}\n\t}\n}\n\nfunc TestNegativeCount(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\ttree.Set(\"a\", 1)\n\n\tgot := collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.IterateByOffset(0, -1, cb)\n\t})\n\tassertSliceEqual(t, got, nil)\n\n\tgot = collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.ReverseIterateByOffset(0, -1, cb)\n\t})\n\tassertSliceEqual(t, got, nil)\n}\n\nfunc TestRootCollapse(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\t// Insert enough to create inner nodes.\n\tfor _, k := range []string{\"a\", \"b\", \"c\", \"d\", \"e\"} {\n\t\ttree.Set(k, k)\n\t}\n\tif tree.root.isLeaf() {\n\t\tt.Error(\"Expected inner root after 5 inserts with fanout 4\")\n\t}\n\n\t// Remove enough to trigger merges and root collapse.\n\ttree.Remove(\"a\")\n\ttree.Remove(\"b\")\n\ttree.Remove(\"c\")\n\t// With only \"d\" and \"e\" left, root should collapse to a leaf.\n\tif !tree.root.isLeaf() {\n\t\tt.Error(\"Expected leaf root after removing down to 2 entries\")\n\t}\n\tif tree.Size() != 2 {\n\t\tt.Errorf(\"Expected size 2, got %d\", tree.Size())\n\t}\n\tgot := collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.Iterate(\"\", \"\", cb)\n\t})\n\tassertSliceEqual(t, got, []string{\"d\", \"e\"})\n}\n\nfunc TestSeparatorKeyAfterLeftmostDeletion(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\t// Insert keys to create a split.\n\tfor _, k := range []string{\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\"} {\n\t\ttree.Set(k, k)\n\t}\n\t// Remove leftmost key \"a\" — should trigger separator update.\n\ttree.Remove(\"a\")\n\n\t// Verify all remaining keys are accessible.\n\tfor _, k := range []string{\"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\"} {\n\t\tif !tree.Has(k) {\n\t\t\tt.Errorf(\"Expected Has(%s) = true after removing 'a'\", k)\n\t\t}\n\t}\n\tif tree.Has(\"a\") {\n\t\tt.Error(\"Expected Has(a) = false\")\n\t}\n\n\t// Verify sorted order.\n\tgot := collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.Iterate(\"\", \"\", cb)\n\t})\n\tassertSliceEqual(t, got, []string{\"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\"})\n\n\t// Verify GetByIndex still works.\n\tk, _ := tree.GetByIndex(0)\n\tif k != \"b\" {\n\t\tt.Errorf(\"GetByIndex(0) = %v, want b\", k)\n\t}\n}\n\nfunc TestNinetyTenSplit(t *testing.T) {\n\t// Sequential inserts should trigger 90/10 splits, keeping left leaves ~97% full.\n\ttree := NewBPTreeN(4)\n\tfor _, k := range []string{\"a\", \"b\", \"c\", \"d\", \"e\"} {\n\t\ttree.Set(k, k)\n\t}\n\t// After inserting a,b,c,d (leaf full), then e (append → 90/10 split):\n\t// Left should have fanout-1=3 entries [a,b,c], right should have 2 entries [d,e].\n\tif !tree.root.isLeaf() == true {\n\t\t// Root should be an inner node after split.\n\t}\n\tinner := tree.root.(*innerNode)\n\tleft := inner.children[0].(*leafNode)\n\tright := inner.children[1].(*leafNode)\n\tif len(left.keys) != 3 {\n\t\tt.Errorf(\"90/10 split: left leaf has %d entries, want 3\", len(left.keys))\n\t}\n\tif len(right.keys) != 2 {\n\t\tt.Errorf(\"90/10 split: right leaf has %d entries, want 2\", len(right.keys))\n\t}\n\n\t// Verify all keys accessible.\n\tgot := collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.Iterate(\"\", \"\", cb)\n\t})\n\tassertSliceEqual(t, got, []string{\"a\", \"b\", \"c\", \"d\", \"e\"})\n}\n\nfunc TestNinetyTenSplitLargeFanout(t *testing.T) {\n\t// With fanout=32, sequential inserts should produce high fill factor.\n\ttree := NewBPTree32()\n\tn := 200\n\tfor i := 0; i \u003c n; i++ {\n\t\ttree.Set(intToKey(i), i)\n\t}\n\tif tree.Size() != n {\n\t\tt.Errorf(\"Expected size %d, got %d\", n, tree.Size())\n\t}\n\n\t// Verify all keys in order.\n\tprev := \"\"\n\tcount := 0\n\ttree.Iterate(\"\", \"\", func(k string, v any) bool {\n\t\tif k \u003c= prev \u0026\u0026 prev != \"\" {\n\t\t\tt.Errorf(\"Keys not in order: %q after %q\", k, prev)\n\t\t}\n\t\tprev = k\n\t\tcount++\n\t\treturn false\n\t})\n\tif count != n {\n\t\tt.Errorf(\"Iterate visited %d entries, want %d\", count, n)\n\t}\n\n\t// Remove all and verify.\n\tfor i := 0; i \u003c n; i++ {\n\t\t_, ok := tree.Remove(intToKey(i))\n\t\tif !ok {\n\t\t\tt.Errorf(\"Remove(%s) failed\", intToKey(i))\n\t\t}\n\t}\n\tif tree.Size() != 0 {\n\t\tt.Errorf(\"Expected size 0, got %d\", tree.Size())\n\t}\n}\n\nfunc TestMixedSplitTypes(t *testing.T) {\n\t// Sequential inserts trigger 90/10, then a middle insert triggers 50/50.\n\ttree := NewBPTreeN(4)\n\t// Sequential: triggers 90/10 split.\n\tfor _, k := range []string{\"b\", \"c\", \"d\", \"e\"} {\n\t\ttree.Set(k, k)\n\t}\n\t// Insert \"a\" at the beginning of the left leaf — not an append, so if\n\t// that leaf overflows it will use 50/50.\n\ttree.Set(\"a\", \"a\")\n\n\t// Verify all keys present and sorted.\n\tgot := collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.Iterate(\"\", \"\", cb)\n\t})\n\tassertSliceEqual(t, got, []string{\"a\", \"b\", \"c\", \"d\", \"e\"})\n}\n\nfunc TestSizeCacheConsistency(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\tkeys := []string{\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\", \"i\", \"j\", \"k\", \"l\"}\n\n\t// Check after each insert.\n\tfor _, k := range keys {\n\t\ttree.Set(k, k)\n\t\tverifySizeCache(t, tree.root)\n\t}\n\n\t// Check after each remove.\n\tfor _, k := range keys {\n\t\ttree.Remove(k)\n\t\tverifySizeCache(t, tree.root)\n\t}\n\n\t// Also check with large fanout.\n\ttree2 := NewBPTree32()\n\tfor i := 0; i \u003c 100; i++ {\n\t\ttree2.Set(intToKey(i), i)\n\t}\n\tverifySizeCache(t, tree2.root)\n\tfor i := 0; i \u003c 100; i++ {\n\t\ttree2.Remove(intToKey(i))\n\t\tverifySizeCache(t, tree2.root)\n\t}\n}\n\nfunc verifySizeCache(t *testing.T, n node) {\n\tt.Helper()\n\tif n == nil || n.isLeaf() {\n\t\treturn\n\t}\n\tinner := n.(*innerNode)\n\t// Verify each sizes[i] matches the actual child subtree size.\n\tfor i, child := range inner.children {\n\t\tactual := child.nodeSize()\n\t\tif inner.sizes[i] != actual {\n\t\t\tt.Errorf(\"innerNode.sizes[%d]=%d but child.nodeSize()=%d\", i, inner.sizes[i], actual)\n\t\t}\n\t\tverifySizeCache(t, child)\n\t}\n}\n\nfunc TestValueIndirection(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\n\t// Nil value through *any.\n\ttree.Set(\"nil\", nil)\n\tif !tree.Has(\"nil\") {\n\t\tt.Error(\"Expected key 'nil' to exist\")\n\t}\n\tif v := tree.Get(\"nil\"); v != nil {\n\t\tt.Errorf(\"Expected nil value, got %v\", v)\n\t}\n\n\t// Various types.\n\ttree.Set(\"int\", 42)\n\ttree.Set(\"str\", \"hello\")\n\ttree.Set(\"slice\", []byte{1, 2, 3})\n\n\tif v := tree.Get(\"int\"); v != 42 {\n\t\tt.Errorf(\"Expected 42, got %v\", v)\n\t}\n\tif v := tree.Get(\"str\"); v != \"hello\" {\n\t\tt.Errorf(\"Expected hello, got %v\", v)\n\t}\n\tif v := tree.Get(\"slice\"); len(v.([]byte)) != 3 {\n\t\tt.Errorf(\"Expected 3-byte slice, got %v\", v)\n\t}\n\n\t// Update value in place (should reuse the *any pointer).\n\ttree.Set(\"int\", 99)\n\tif v := tree.Get(\"int\"); v != 99 {\n\t\tt.Errorf(\"Expected 99 after update, got %v\", v)\n\t}\n\n\t// Remove nil value.\n\tv, ok := tree.Remove(\"nil\")\n\tif !ok || v != nil {\n\t\tt.Errorf(\"Remove nil: got (%v, %v), want (nil, true)\", v, ok)\n\t}\n\n\t// Remove typed value.\n\tv, ok = tree.Remove(\"int\")\n\tif !ok || v != 99 {\n\t\tt.Errorf(\"Remove int: got (%v, %v), want (99, true)\", v, ok)\n\t}\n\n\t// Large string values — each stored as separate *any object.\n\tbigVal := make([]byte, 10000)\n\tfor i := range bigVal {\n\t\tbigVal[i] = byte(i % 256)\n\t}\n\tfor i := 0; i \u003c 10; i++ {\n\t\ttree.Set(intToKey(i), string(bigVal))\n\t}\n\tif tree.Size() != 12 { // 10 new + \"str\" + \"slice\" remaining\n\t\tt.Errorf(\"Expected size 12, got %d\", tree.Size())\n\t}\n\n\t// Verify large values round-trip correctly.\n\tfor i := 0; i \u003c 10; i++ {\n\t\tv := tree.Get(intToKey(i))\n\t\tif v == nil {\n\t\t\tt.Errorf(\"Missing key %s\", intToKey(i))\n\t\t\tcontinue\n\t\t}\n\t\ts := v.(string)\n\t\tif len(s) != 10000 {\n\t\t\tt.Errorf(\"Key %s: expected 10000-byte string, got %d\", intToKey(i), len(s))\n\t\t}\n\t}\n\n\t// Iteration with mixed values.\n\tcount := 0\n\ttree.Iterate(\"\", \"\", func(k string, v any) bool {\n\t\tcount++\n\t\treturn false\n\t})\n\tif count != 12 {\n\t\tt.Errorf(\"Iterate counted %d, want 12\", count)\n\t}\n\n\t// Remove all and verify clean.\n\tfor i := 0; i \u003c 10; i++ {\n\t\ttree.Remove(intToKey(i))\n\t}\n\ttree.Remove(\"str\")\n\ttree.Remove(\"slice\")\n\tif tree.Size() != 0 {\n\t\tt.Errorf(\"Expected empty tree, got size %d\", tree.Size())\n\t}\n}\n\nfunc TestFanoutPanics(t *testing.T) {\n\tassertPanics(t, \"fanout 3\", func() {\n\t\tNewBPTreeN(3)\n\t})\n\tassertPanics(t, \"fanout 0\", func() {\n\t\tNewBPTreeN(0)\n\t})\n}\n\n//----------------------------------------\n// Ported from avl/v0 node_test.gno\n\nfunc TestHasTableDriven(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []string\n\t\thasKey   string\n\t\texpected bool\n\t}{\n\t\t{\"has key in non-empty tree\", []string{\"C\", \"A\", \"B\", \"E\", \"D\"}, \"B\", true},\n\t\t{\"does not have key in non-empty tree\", []string{\"C\", \"A\", \"B\", \"E\", \"D\"}, \"F\", false},\n\t\t{\"has key in single-node tree\", []string{\"A\"}, \"A\", true},\n\t\t{\"does not have key in single-node tree\", []string{\"A\"}, \"B\", false},\n\t\t{\"does not have key in empty tree\", []string{}, \"A\", false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttree := NewBPTreeN(4)\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree.Set(key, nil)\n\t\t\t}\n\t\t\tresult := tree.Has(tt.hasKey)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"Expected %v, got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetByIndexTableDriven(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tinput       []string\n\t\tidx         int\n\t\texpectKey   string\n\t\texpectPanic bool\n\t}{\n\t\t{\"get by valid index\", []string{\"C\", \"A\", \"B\", \"E\", \"D\"}, 2, \"C\", false},\n\t\t{\"get by valid index (smallest)\", []string{\"C\", \"A\", \"B\", \"E\", \"D\"}, 0, \"A\", false},\n\t\t{\"get by valid index (largest)\", []string{\"C\", \"A\", \"B\", \"E\", \"D\"}, 4, \"E\", false},\n\t\t{\"get by invalid index (negative)\", []string{\"C\", \"A\", \"B\", \"E\", \"D\"}, -1, \"\", true},\n\t\t{\"get by invalid index (out of range)\", []string{\"C\", \"A\", \"B\", \"E\", \"D\"}, 5, \"\", true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttree := NewBPTreeN(4)\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree.Set(key, nil)\n\t\t\t}\n\t\t\tif tt.expectPanic {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r == nil {\n\t\t\t\t\t\tt.Errorf(\"Expected a panic but didn't get one\")\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t}\n\t\t\tkey, _ := tree.GetByIndex(tt.idx)\n\t\t\tif !tt.expectPanic \u0026\u0026 key != tt.expectKey {\n\t\t\t\tt.Errorf(\"Expected key %s, got %s\", tt.expectKey, key)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRemoveTableDriven(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tinput     []string\n\t\tremoveKey string\n\t\texpected  []string\n\t}{\n\t\t{\"remove from middle\", []string{\"C\", \"A\", \"B\", \"D\"}, \"B\", []string{\"A\", \"C\", \"D\"}},\n\t\t{\"remove first key\", []string{\"C\", \"A\", \"B\", \"D\"}, \"A\", []string{\"B\", \"C\", \"D\"}},\n\t\t{\"remove last key\", []string{\"C\", \"A\", \"B\", \"E\", \"D\"}, \"E\", []string{\"A\", \"B\", \"C\", \"D\"}},\n\t\t{\"remove root-equivalent key\", []string{\"C\", \"A\", \"B\", \"E\", \"D\"}, \"C\", []string{\"A\", \"B\", \"D\", \"E\"}},\n\t\t{\"remove non-existent key\", []string{\"C\", \"A\", \"B\", \"E\", \"D\"}, \"F\", []string{\"A\", \"B\", \"C\", \"D\", \"E\"}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttree := NewBPTreeN(4)\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree.Set(key, nil)\n\t\t\t}\n\t\t\ttree.Remove(tt.removeKey)\n\t\t\tvar result []string\n\t\t\ttree.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\t\t\tresult = append(result, key)\n\t\t\t\treturn false\n\t\t\t})\n\t\t\tif len(result) == 0 {\n\t\t\t\tresult = []string{}\n\t\t\t}\n\t\t\tassertSliceEqual(t, result, tt.expected)\n\t\t})\n\t}\n}\n\nfunc TestTraverse(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []string\n\t\texpected []string\n\t}{\n\t\t{\"empty tree\", []string{}, []string{}},\n\t\t{\"single node tree\", []string{\"A\"}, []string{\"A\"}},\n\t\t{\"small tree\", []string{\"C\", \"A\", \"B\", \"E\", \"D\"}, []string{\"A\", \"B\", \"C\", \"D\", \"E\"}},\n\t\t{\"large tree\", []string{\"H\", \"D\", \"L\", \"B\", \"F\", \"J\", \"N\", \"A\", \"C\", \"E\", \"G\", \"I\", \"K\", \"M\", \"O\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\", \"F\", \"G\", \"H\", \"I\", \"J\", \"K\", \"L\", \"M\", \"N\", \"O\"}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttree := NewBPTreeN(4)\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree.Set(key, nil)\n\t\t\t}\n\n\t\t\tt.Run(\"iterate\", func(t *testing.T) {\n\t\t\t\tvar result []string\n\t\t\t\ttree.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\t\t\t\tresult = append(result, key)\n\t\t\t\t\treturn false\n\t\t\t\t})\n\t\t\t\tif len(result) == 0 {\n\t\t\t\t\tresult = []string{}\n\t\t\t\t}\n\t\t\t\tassertSliceEqual(t, result, tt.expected)\n\t\t\t})\n\n\t\t\tt.Run(\"ReverseIterate\", func(t *testing.T) {\n\t\t\t\tvar result []string\n\t\t\t\ttree.ReverseIterate(\"\", \"\", func(key string, value any) bool {\n\t\t\t\t\tresult = append(result, key)\n\t\t\t\t\treturn false\n\t\t\t\t})\n\t\t\t\texpected := make([]string, len(tt.expected))\n\t\t\t\tcopy(expected, tt.expected)\n\t\t\t\tfor i, j := 0, len(expected)-1; i \u003c j; i, j = i+1, j-1 {\n\t\t\t\t\texpected[i], expected[j] = expected[j], expected[i]\n\t\t\t\t}\n\t\t\t\tif len(result) == 0 {\n\t\t\t\t\tresult = []string{}\n\t\t\t\t}\n\t\t\t\tassertSliceEqual(t, result, expected)\n\t\t\t})\n\n\t\t\tt.Run(\"TraverseInRange\", func(t *testing.T) {\n\t\t\t\tvar result []string\n\t\t\t\tstart, end := \"C\", \"M\"\n\t\t\t\ttree.Iterate(start, end, func(key string, value any) bool {\n\t\t\t\t\tresult = append(result, key)\n\t\t\t\t\treturn false\n\t\t\t\t})\n\t\t\t\texpected := make([]string, 0)\n\t\t\t\tfor _, key := range tt.expected {\n\t\t\t\t\tif key \u003e= start \u0026\u0026 key \u003c end {\n\t\t\t\t\t\texpected = append(expected, key)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif len(result) == 0 {\n\t\t\t\t\tresult = []string{}\n\t\t\t\t}\n\t\t\t\tassertSliceEqual(t, result, expected)\n\t\t\t})\n\n\t\t\tt.Run(\"early termination\", func(t *testing.T) {\n\t\t\t\tif len(tt.input) == 0 {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tvar result []string\n\t\t\t\tvar count int\n\t\t\t\ttree.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\t\t\t\tcount++\n\t\t\t\t\tresult = append(result, key)\n\t\t\t\t\treturn true\n\t\t\t\t})\n\t\t\t\tif count != 1 {\n\t\t\t\t\tt.Errorf(\"Expected callback to be called exactly once, got %d calls\", count)\n\t\t\t\t}\n\t\t\t\tif len(result) != 1 {\n\t\t\t\t\tt.Errorf(\"Expected exactly one result, got %d items\", len(result))\n\t\t\t\t}\n\t\t\t\tif len(result) \u003e 0 \u0026\u0026 result[0] != tt.expected[0] {\n\t\t\t\t\tt.Errorf(\"Expected first item to be %v, got %v\", tt.expected[0], result[0])\n\t\t\t\t}\n\t\t\t})\n\t\t})\n\t}\n}\n\nfunc TestTraverseByOffset(t *testing.T) {\n\tsl := []string{\"Alfa\", \"Alfred\", \"Alpha\", \"Alphabet\", \"Beta\", \"Beth\", \"Book\", \"Browser\"}\n\n\t// Insert in reverse order to ensure ordering is independent of insertion order.\n\treversed := make([]string, len(sl))\n\tcopy(reversed, sl)\n\tfor i, j := 0, len(reversed)-1; i \u003c j; i, j = i+1, j-1 {\n\t\treversed[i], reversed[j] = reversed[j], reversed[i]\n\t}\n\n\tt.Run(\"ascending\", func(t *testing.T) {\n\t\ttree := NewBPTreeN(4)\n\t\tfor _, v := range reversed {\n\t\t\ttree.Set(v, nil)\n\t\t}\n\n\t\t// Single-element offset traversal.\n\t\tvar result []string\n\t\tfor i := 0; i \u003c len(sl); i++ {\n\t\t\ttree.IterateByOffset(i, 1, func(key string, value any) bool {\n\t\t\t\tresult = append(result, key)\n\t\t\t\treturn false\n\t\t\t})\n\t\t}\n\t\tassertSliceEqual(t, result, sl)\n\n\t\t// Sliding window.\n\t\tfor l := 2; l \u003c= len(sl); l++ {\n\t\t\tfor i := 0; i \u003c= len(sl); i++ {\n\t\t\t\tmax := i + l\n\t\t\t\tif max \u003e len(sl) {\n\t\t\t\t\tmax = len(sl)\n\t\t\t\t}\n\t\t\t\texp := sl[i:max]\n\t\t\t\tvar actual []string\n\t\t\t\ttree.IterateByOffset(i, l, func(key string, value any) bool {\n\t\t\t\t\tactual = append(actual, key)\n\t\t\t\t\treturn false\n\t\t\t\t})\n\t\t\t\tif len(actual) == 0 {\n\t\t\t\t\tactual = []string{}\n\t\t\t\t}\n\t\t\t\tassertSliceEqual(t, actual, exp)\n\t\t\t}\n\t\t}\n\t})\n\n\tt.Run(\"descending\", func(t *testing.T) {\n\t\ttree := NewBPTreeN(4)\n\t\tfor _, v := range reversed {\n\t\t\ttree.Set(v, nil)\n\t\t}\n\n\t\t// The descending order.\n\t\tdesc := make([]string, len(sl))\n\t\tcopy(desc, sl)\n\t\tfor i, j := 0, len(desc)-1; i \u003c j; i, j = i+1, j-1 {\n\t\t\tdesc[i], desc[j] = desc[j], desc[i]\n\t\t}\n\n\t\t// Single-element offset traversal in reverse.\n\t\tvar result []string\n\t\tfor i := 0; i \u003c len(desc); i++ {\n\t\t\ttree.ReverseIterateByOffset(i, 1, func(key string, value any) bool {\n\t\t\t\tresult = append(result, key)\n\t\t\t\treturn false\n\t\t\t})\n\t\t}\n\t\tassertSliceEqual(t, result, desc)\n\n\t\t// Sliding window in descending.\n\t\tfor l := 2; l \u003c= len(desc); l++ {\n\t\t\tfor i := 0; i \u003c= len(desc); i++ {\n\t\t\t\tmax := i + l\n\t\t\t\tif max \u003e len(desc) {\n\t\t\t\t\tmax = len(desc)\n\t\t\t\t}\n\t\t\t\texp := desc[i:max]\n\t\t\t\tvar actual []string\n\t\t\t\ttree.ReverseIterateByOffset(i, l, func(key string, value any) bool {\n\t\t\t\t\tactual = append(actual, key)\n\t\t\t\t\treturn false\n\t\t\t\t})\n\t\t\t\tif len(actual) == 0 {\n\t\t\t\t\tactual = []string{}\n\t\t\t\t}\n\t\t\t\tassertSliceEqual(t, actual, exp)\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc TestBSTProperty(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\tkeys := []string{\"D\", \"B\", \"F\", \"A\", \"C\", \"E\", \"G\"}\n\tfor _, key := range keys {\n\t\ttree.Set(key, nil)\n\t}\n\n\tvar result []string\n\ttree.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\tresult = append(result, key)\n\t\treturn false\n\t})\n\n\tfor i := 1; i \u003c len(result); i++ {\n\t\tif result[i] \u003c result[i-1] {\n\t\t\tt.Errorf(\"Sorted property violated: %s \u003c %s (index %d)\",\n\t\t\t\tresult[i], result[i-1], i)\n\t\t}\n\t}\n}\n\nfunc TestRemoveFromEmptyTree(t *testing.T) {\n\tvar tree BPTree\n\tval, removed := tree.Remove(\"NonExistent\")\n\tif val != nil || removed {\n\t\tt.Errorf(\"Expected no value and removed=false when removing from empty tree\")\n\t}\n}\n\n// Ported from avl tree_test.gno\n\nfunc TestPortedTreeSize(t *testing.T) {\n\ttree := NewBPTree32()\n\tif tree.Size() != 0 {\n\t\tt.Error(\"Expected empty tree size to be 0\")\n\t}\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\tif tree.Size() != 2 {\n\t\tt.Error(\"Expected tree size to be 2\")\n\t}\n}\n\nfunc TestPortedTreeHas(t *testing.T) {\n\ttree := NewBPTree32()\n\ttree.Set(\"key1\", \"value1\")\n\tif !tree.Has(\"key1\") {\n\t\tt.Error(\"Expected tree to have key1\")\n\t}\n\tif tree.Has(\"key2\") {\n\t\tt.Error(\"Expected tree to not have key2\")\n\t}\n}\n\nfunc TestPortedTreeGet(t *testing.T) {\n\ttree := NewBPTree32()\n\ttree.Set(\"key1\", \"value1\")\n\tif value := tree.Get(\"key1\"); value != \"value1\" {\n\t\tt.Error(\"Expected Get to return value1\")\n\t}\n\tif value := tree.Get(\"key2\"); value != nil {\n\t\tt.Error(\"Expected Get to return nil for non-existent key\")\n\t}\n}\n\nfunc TestPortedTreeGetByIndex(t *testing.T) {\n\ttree := NewBPTree32()\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\tkey, value := tree.GetByIndex(0)\n\tif key != \"key1\" || value != \"value1\" {\n\t\tt.Error(\"Expected GetByIndex(0) to return key1 and value1\")\n\t}\n\tkey, value = tree.GetByIndex(1)\n\tif key != \"key2\" || value != \"value2\" {\n\t\tt.Error(\"Expected GetByIndex(1) to return key2 and value2\")\n\t}\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Error(\"Expected GetByIndex to panic for out-of-range index\")\n\t\t}\n\t}()\n\ttree.GetByIndex(2)\n}\n\nfunc TestPortedTreeRemove(t *testing.T) {\n\ttree := NewBPTree32()\n\ttree.Set(\"key1\", \"value1\")\n\tvalue, removed := tree.Remove(\"key1\")\n\tif !removed || value != \"value1\" || tree.Size() != 0 {\n\t\tt.Error(\"Expected Remove to remove key-value pair\")\n\t}\n\t_, removed = tree.Remove(\"key2\")\n\tif removed {\n\t\tt.Error(\"Expected Remove to return false for non-existent key\")\n\t}\n}\n\nfunc TestPortedTreeIterate(t *testing.T) {\n\ttree := NewBPTree32()\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\ttree.Set(\"key3\", \"value3\")\n\tvar keys []string\n\ttree.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\tkeys = append(keys, key)\n\t\treturn false\n\t})\n\tassertSliceEqual(t, keys, []string{\"key1\", \"key2\", \"key3\"})\n}\n\nfunc TestPortedTreeReverseIterate(t *testing.T) {\n\ttree := NewBPTree32()\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\ttree.Set(\"key3\", \"value3\")\n\tvar keys []string\n\ttree.ReverseIterate(\"\", \"\", func(key string, value any) bool {\n\t\tkeys = append(keys, key)\n\t\treturn false\n\t})\n\tassertSliceEqual(t, keys, []string{\"key3\", \"key2\", \"key1\"})\n}\n\nfunc TestPortedTreeIterateByOffset(t *testing.T) {\n\ttree := NewBPTree32()\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\ttree.Set(\"key3\", \"value3\")\n\tvar keys []string\n\ttree.IterateByOffset(1, 2, func(key string, value any) bool {\n\t\tkeys = append(keys, key)\n\t\treturn false\n\t})\n\tassertSliceEqual(t, keys, []string{\"key2\", \"key3\"})\n}\n\nfunc TestPortedTreeReverseIterateByOffset(t *testing.T) {\n\ttree := NewBPTree32()\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\ttree.Set(\"key3\", \"value3\")\n\tvar keys []string\n\ttree.ReverseIterateByOffset(1, 2, func(key string, value any) bool {\n\t\tkeys = append(keys, key)\n\t\treturn false\n\t})\n\tassertSliceEqual(t, keys, []string{\"key2\", \"key1\"})\n}\n\nfunc TestPortedReverseIterateByOffsetVaried(t *testing.T) {\n\ttree := NewBPTree32()\n\ttree.Set(\"a\", 1)\n\ttree.Set(\"b\", 2)\n\ttree.Set(\"c\", 3)\n\ttree.Set(\"d\", 4)\n\ttree.Set(\"e\", 5)\n\n\tcases := []struct {\n\t\toffset int\n\t\tlimit  int\n\t\twant   []string\n\t}{\n\t\t{0, 5, []string{\"e\", \"d\", \"c\", \"b\", \"a\"}},\n\t\t{0, 1, []string{\"e\"}},\n\t\t{0, 3, []string{\"e\", \"d\", \"c\"}},\n\t\t{1, 2, []string{\"d\", \"c\"}},\n\t\t{2, 2, []string{\"c\", \"b\"}},\n\t\t{3, 5, []string{\"b\", \"a\"}},\n\t\t{4, 1, []string{\"a\"}},\n\t\t{4, 10, []string{\"a\"}},\n\t\t{5, 1, nil},\n\t\t{0, 0, nil},\n\t\t{10, 1, nil},\n\t}\n\n\tfor _, tc := range cases {\n\t\tvar got []string\n\t\ttree.ReverseIterateByOffset(tc.offset, tc.limit, func(key string, value any) bool {\n\t\t\tgot = append(got, key)\n\t\t\treturn false\n\t\t})\n\t\tif !slicesEqual(got, tc.want) {\n\t\t\tt.Errorf(\"ReverseIterateByOffset(%d, %d): got %v, want %v\",\n\t\t\t\ttc.offset, tc.limit, got, tc.want)\n\t\t}\n\t}\n\n\t// Early termination.\n\tvar got []string\n\ttree.ReverseIterateByOffset(1, 5, func(key string, value any) bool {\n\t\tgot = append(got, key)\n\t\treturn len(got) \u003e= 2\n\t})\n\tassertSliceEqual(t, got, []string{\"d\", \"c\"})\n}\n\nfunc TestPortedBalanceAfterRemoval(t *testing.T) {\n\t// This tests the behavioral equivalence: after various insert/remove\n\t// patterns, the tree maintains correct sorted order and all keys are\n\t// accessible. (AVL tests checked balance factors; we check correctness.)\n\ttests := []struct {\n\t\tname       string\n\t\tinsertKeys []string\n\t\tremoveKey  string\n\t}{\n\t\t{\"remove right node\", []string{\"B\", \"A\", \"D\", \"C\", \"E\"}, \"E\"},\n\t\t{\"remove left node\", []string{\"D\", \"B\", \"E\", \"A\", \"C\"}, \"A\"},\n\t\t{\"remove after complex insert\", []string{\"C\", \"B\", \"E\", \"A\", \"D\", \"F\"}, \"F\"},\n\t\t{\"descending insert, remove middle\", []string{\"E\", \"D\", \"C\", \"B\", \"A\"}, \"C\"},\n\t\t{\"ascending insert, remove middle\", []string{\"A\", \"B\", \"C\", \"D\", \"E\"}, \"C\"},\n\t\t{\"duplicate insert, remove key\", []string{\"C\", \"B\", \"C\", \"A\", \"D\"}, \"C\"},\n\t\t{\"complex case\", []string{\"H\", \"B\", \"A\", \"C\", \"E\", \"D\", \"F\", \"G\"}, \"B\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttree := NewBPTreeN(4)\n\t\t\tfor _, key := range tt.insertKeys {\n\t\t\t\ttree.Set(key, nil)\n\t\t\t}\n\t\t\ttree.Remove(tt.removeKey)\n\n\t\t\t// Verify sorted order.\n\t\t\tvar result []string\n\t\t\ttree.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\t\t\tresult = append(result, key)\n\t\t\t\treturn false\n\t\t\t})\n\t\t\tfor i := 1; i \u003c len(result); i++ {\n\t\t\t\tif result[i] \u003c= result[i-1] {\n\t\t\t\t\tt.Errorf(\"Sorted property violated: %s \u003c= %s\", result[i], result[i-1])\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Verify all expected keys present.\n\t\t\tfor _, key := range tt.insertKeys {\n\t\t\t\tif key == tt.removeKey {\n\t\t\t\t\tif tree.Has(key) {\n\t\t\t\t\t\t// Only check if the key was unique (not duplicated).\n\t\t\t\t\t\t// With duplicates, Set overwrites, so there's only one copy.\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n//----------------------------------------\n// Structural invariant verification\n\n// verifyInvariants checks all B+ tree structural invariants.\nfunc verifyInvariants(t *testing.T, tree *BPTree) {\n\tt.Helper()\n\tif tree.root == nil {\n\t\tif tree.size != 0 {\n\t\t\tt.Errorf(\"nil root but size=%d\", tree.size)\n\t\t}\n\t\treturn\n\t}\n\n\t// Check leaf depth uniformity.\n\tdepth := leafDepth(tree.root, 0)\n\tif depth == -1 {\n\t\tt.Error(\"leaves are at different depths\")\n\t}\n\n\t// Check separator keys, child count bounds, sizes, ref-count.\n\tseen := make(map[node]bool)\n\tverifyNode(t, tree.root, tree.fanout, true, seen)\n\n\t// Check tree.size matches actual leaf count.\n\tactual := tree.root.nodeSize()\n\tif tree.size != actual {\n\t\tt.Errorf(\"tree.size=%d but root.nodeSize()=%d\", tree.size, actual)\n\t}\n}\n\nfunc leafDepth(n node, depth int) int {\n\tif n.isLeaf() {\n\t\treturn depth\n\t}\n\tinner := n.(*innerNode)\n\td := -1\n\tfor _, child := range inner.children {\n\t\tcd := leafDepth(child, depth+1)\n\t\tif d == -1 {\n\t\t\td = cd\n\t\t} else if cd != d {\n\t\t\treturn -1\n\t\t}\n\t}\n\treturn d\n}\n\nfunc verifyNode(t *testing.T, n node, fanout int, isRoot bool, seen map[node]bool) {\n\tt.Helper()\n\tif seen[n] {\n\t\tt.Errorf(\"node reachable by multiple paths (ref-count \u003e= 2)\")\n\t\treturn\n\t}\n\tseen[n] = true\n\n\tif n.isLeaf() {\n\t\tleaf := n.(*leafNode)\n\t\tif len(leaf.keys) == 0 \u0026\u0026 !isRoot {\n\t\t\tt.Errorf(\"non-root leaf has 0 keys\")\n\t\t}\n\t\tif len(leaf.keys) \u003e fanout {\n\t\t\tt.Errorf(\"leaf has %d keys, max=%d\", len(leaf.keys), fanout)\n\t\t}\n\t\treturn\n\t}\n\n\tinner := n.(*innerNode)\n\n\t// Child count bounds.\n\tminC := fanout / 2\n\tif isRoot {\n\t\tminC = 2\n\t}\n\tif len(inner.children) \u003c minC \u0026\u0026 !isRoot {\n\t\tt.Errorf(\"inner node has %d children, min=%d\", len(inner.children), minC)\n\t}\n\tif len(inner.children) \u003e fanout {\n\t\tt.Errorf(\"inner node has %d children, max=%d\", len(inner.children), fanout)\n\t}\n\n\t// keys/children/sizes length consistency.\n\tif len(inner.keys) != len(inner.children)-1 {\n\t\tt.Errorf(\"len(keys)=%d but len(children)=%d\", len(inner.keys), len(inner.children))\n\t}\n\tif len(inner.sizes) != len(inner.children) {\n\t\tt.Errorf(\"len(sizes)=%d but len(children)=%d\", len(inner.sizes), len(inner.children))\n\t}\n\n\t// Separator key correctness: keys[i] == children[i+1].minKey().\n\tfor i, k := range inner.keys {\n\t\texpected := inner.children[i+1].minKey()\n\t\tif k != expected {\n\t\t\tt.Errorf(\"separator keys[%d]=%q but children[%d].minKey()=%q\", i, k, i+1, expected)\n\t\t}\n\t}\n\n\t// sizes[i] matches child.\n\tfor i, child := range inner.children {\n\t\tactual := child.nodeSize()\n\t\tif inner.sizes[i] != actual {\n\t\t\tt.Errorf(\"sizes[%d]=%d but child.nodeSize()=%d\", i, inner.sizes[i], actual)\n\t\t}\n\t\tverifyNode(t, child, fanout, false, seen)\n\t}\n}\n\nfunc TestInvariantsAfterEveryOperation(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\tkeys := []string{\"m\", \"f\", \"t\", \"b\", \"i\", \"p\", \"w\", \"a\", \"d\", \"g\", \"k\", \"n\", \"r\", \"u\", \"y\"}\n\n\tfor _, k := range keys {\n\t\ttree.Set(k, k)\n\t\tverifyInvariants(t, tree)\n\t}\n\tfor _, k := range keys {\n\t\ttree.Remove(k)\n\t\tverifyInvariants(t, tree)\n\t}\n}\n\n//----------------------------------------\n// Behavioral parity: GetByIndex == IterateByOffset\n\nfunc TestGetByIndexMatchesIterateByOffset(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\tfor _, k := range []string{\"h\", \"d\", \"l\", \"b\", \"f\", \"j\", \"n\", \"a\", \"c\", \"e\", \"g\"} {\n\t\ttree.Set(k, k)\n\t}\n\n\tfor i := 0; i \u003c tree.Size(); i++ {\n\t\tk1, v1 := tree.GetByIndex(i)\n\t\tvar k2 string\n\t\tvar v2 any\n\t\ttree.IterateByOffset(i, 1, func(k string, v any) bool {\n\t\t\tk2 = k\n\t\t\tv2 = v\n\t\t\treturn true\n\t\t})\n\t\tif k1 != k2 || v1 != v2 {\n\t\t\tt.Errorf(\"index %d: GetByIndex=(%q,%v) but IterateByOffset=(%q,%v)\", i, k1, v1, k2, v2)\n\t\t}\n\t}\n}\n\n//----------------------------------------\n// Stress: oscillating tree size\n\nfunc TestOscillatingSize(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\n\t// Insert 100.\n\tfor i := 0; i \u003c 100; i++ {\n\t\ttree.Set(intToKey(i), i)\n\t}\n\tverifyInvariants(t, tree)\n\n\t// Remove 50.\n\tfor i := 0; i \u003c 50; i++ {\n\t\ttree.Remove(intToKey(i))\n\t}\n\tverifyInvariants(t, tree)\n\tif tree.Size() != 50 {\n\t\tt.Errorf(\"Expected size 50, got %d\", tree.Size())\n\t}\n\n\t// Insert 50 new.\n\tfor i := 100; i \u003c 150; i++ {\n\t\ttree.Set(intToKey(i), i)\n\t}\n\tverifyInvariants(t, tree)\n\tif tree.Size() != 100 {\n\t\tt.Errorf(\"Expected size 100, got %d\", tree.Size())\n\t}\n\n\t// Remove all.\n\tfor i := 50; i \u003c 150; i++ {\n\t\ttree.Remove(intToKey(i))\n\t}\n\tverifyInvariants(t, tree)\n\tif tree.Size() != 0 {\n\t\tt.Errorf(\"Expected size 0, got %d\", tree.Size())\n\t}\n\n\t// Insert again from empty.\n\tfor i := 0; i \u003c 20; i++ {\n\t\ttree.Set(intToKey(i), i)\n\t}\n\tverifyInvariants(t, tree)\n\tif tree.Size() != 20 {\n\t\tt.Errorf(\"Expected size 20, got %d\", tree.Size())\n\t}\n}\n\n//----------------------------------------\n// All 6 rebalance paths\n\nfunc TestAllRebalancePaths(t *testing.T) {\n\t// We use fanout=4 so min=2 for leaves.\n\t// Each sub-test constructs a specific tree state and triggers one rebalance path.\n\n\tt.Run(\"leaf redistribute from left\", func(t *testing.T) {\n\t\ttree := NewBPTreeN(4)\n\t\tfor _, k := range []string{\"a\", \"b\", \"c\", \"d\", \"e\"} {\n\t\t\ttree.Set(k, k)\n\t\t}\n\t\t// After 90/10 split: left=[a,b,c], right=[d,e].\n\t\t// Remove \"d\" → right=[e] (1 \u003c min=2). Left has 3 \u003e 2 → redistribute from left.\n\t\ttree.Remove(\"d\")\n\t\tverifyInvariants(t, tree)\n\t\tif !tree.Has(\"e\") {\n\t\t\tt.Error(\"Missing key 'e' after redistribute\")\n\t\t}\n\t})\n\n\tt.Run(\"leaf redistribute from right\", func(t *testing.T) {\n\t\ttree := NewBPTreeN(4)\n\t\tfor _, k := range []string{\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\"} {\n\t\t\ttree.Set(k, k)\n\t\t}\n\t\t// Remove from leftmost leaf until it underflows and must redistribute from right.\n\t\ttree.Remove(\"a\")\n\t\ttree.Remove(\"b\")\n\t\tverifyInvariants(t, tree)\n\t})\n\n\tt.Run(\"leaf redistribute from right, no stale root separator keys\", func(t *testing.T) {\n\t\ttree := NewBPTreeN(4)\n\t\tfor _, k := range []string{\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\", \"i\", \"j\", \"k\", \"l\", \"m\", \"n\"} {\n\t\t\ttree.Set(k, k)\n\t\t}\n\t\ttree.Remove(\"g\") // no underflow\n\t\ttree.Remove(\"h\") // leaf [h,i] underflows and must redistribute from right\n\t\tverifyInvariants(t, tree)\n\t})\n\n\tt.Run(\"leaf redistribute from right, no stale inner node separator keys\", func(t *testing.T) {\n\t\ttree := NewBPTreeN(4)\n\t\tfor _, k := range []string{\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\", \"i\", \"j\", \"k\", \"l\", \"m\", \"n\", \"o\", \"p\", \"q\"} {\n\t\t\ttree.Set(k, k)\n\t\t}\n\t\ttree.Remove(\"g\") // no underflow\n\t\ttree.Remove(\"j\") // no underflow\n\t\ttree.Remove(\"k\") // leaf [k,l] underflows and must redistribute from right\n\t\tverifyInvariants(t, tree)\n\t})\n\n\tt.Run(\"stale separator, middle leaf pos0 redistribute from right\", func(t *testing.T) {\n\t\t// Exercises the core bug: remove pos=0 from a middle leaf (childIdx \u003e 0),\n\t\t// where left sibling is at minimum (can't redistribute left) and right\n\t\t// sibling has surplus (redistribute right fires). Without the fix,\n\t\t// parent.keys[childIdx-1] remains stale.\n\t\t//\n\t\t// After inserting a-k with fanout=4, tree is:\n\t\t//   root keys=[\"d\",\"g\",\"j\"]\n\t\t//   children=[[\"a\",\"b\",\"c\"], [\"d\",\"e\",\"f\"], [\"g\",\"h\",\"i\"], [\"j\",\"k\"]]\n\t\t//\n\t\t// Remove \"a\" thins children[0] to [\"b\",\"c\"] (at minimum).\n\t\t// Remove \"f\" thins children[1] to [\"d\",\"e\"] (at minimum).\n\t\t// Remove \"d\" is pos=0 from children[1], causing underflow to [\"e\"].\n\t\t//   Left sibling [\"b\",\"c\"] has 2 = minKeys, can't spare.\n\t\t//   Right sibling [\"g\",\"h\",\"i\"] has 3 \u003e minKeys, redistribute right fires.\n\t\t//   Without fix: keys[0] stays \"d\" (stale). With fix: updated to \"e\".\n\t\ttree := NewBPTreeN(4)\n\t\tfor _, k := range []string{\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\", \"i\", \"j\", \"k\"} {\n\t\t\ttree.Set(k, k)\n\t\t}\n\t\ttree.Remove(\"a\") // thin left sibling to minimum\n\t\ttree.Remove(\"f\") // thin target leaf to minimum\n\t\ttree.Remove(\"d\") // pos=0, underflow, triggers redistribute from right\n\t\tverifyInvariants(t, tree)\n\t})\n\n\tt.Run(\"leaf merge\", func(t *testing.T) {\n\t\ttree := NewBPTreeN(4)\n\t\tfor _, k := range []string{\"a\", \"b\", \"c\", \"d\", \"e\"} {\n\t\t\ttree.Set(k, k)\n\t\t}\n\t\t// Remove enough to make both siblings at minimum, then one more triggers merge.\n\t\ttree.Remove(\"a\")\n\t\ttree.Remove(\"b\") // left=[c], right=[d,e]. left underflows. right has 2=min. merge.\n\t\tverifyInvariants(t, tree)\n\t\tgot := collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\t\treturn tr.Iterate(\"\", \"\", cb)\n\t\t})\n\t\tassertSliceEqual(t, got, []string{\"c\", \"d\", \"e\"})\n\t})\n\n\tt.Run(\"inner redistribute from left\", func(t *testing.T) {\n\t\t// Build a tree with enough inner nodes, then remove to trigger inner rebalance.\n\t\ttree := NewBPTreeN(4)\n\t\tfor i := 0; i \u003c 30; i++ {\n\t\t\ttree.Set(intToKey(i), i)\n\t\t}\n\t\t// Remove from the right side to underflow a right inner node.\n\t\tfor i := 25; i \u003c 30; i++ {\n\t\t\ttree.Remove(intToKey(i))\n\t\t}\n\t\tverifyInvariants(t, tree)\n\t})\n\n\tt.Run(\"inner redistribute from right\", func(t *testing.T) {\n\t\ttree := NewBPTreeN(4)\n\t\tfor i := 0; i \u003c 30; i++ {\n\t\t\ttree.Set(intToKey(i), i)\n\t\t}\n\t\t// Remove from the left side to underflow a left inner node.\n\t\tfor i := 0; i \u003c 5; i++ {\n\t\t\ttree.Remove(intToKey(i))\n\t\t}\n\t\tverifyInvariants(t, tree)\n\t})\n\n\tt.Run(\"inner merge\", func(t *testing.T) {\n\t\ttree := NewBPTreeN(4)\n\t\tfor i := 0; i \u003c 20; i++ {\n\t\t\ttree.Set(intToKey(i), i)\n\t\t}\n\t\t// Remove enough to trigger inner merge.\n\t\tfor i := 0; i \u003c 15; i++ {\n\t\t\ttree.Remove(intToKey(i))\n\t\t}\n\t\tverifyInvariants(t, tree)\n\t\tif tree.Size() != 5 {\n\t\t\tt.Errorf(\"Expected size 5, got %d\", tree.Size())\n\t\t}\n\t})\n}\n\n//----------------------------------------\n// Value mutation after Get should not affect tree\n\nfunc TestValueMutationIndependence(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\n\t// Store a slice value.\n\torig := []int{1, 2, 3}\n\ttree.Set(\"k\", orig)\n\n\t// Get the value and mutate it.\n\tv := tree.Get(\"k\")\n\tgot := v.([]int)\n\tgot[0] = 999\n\n\t// The tree's stored value should also be affected (since slices are reference types\n\t// and *any holds the same interface). This is the expected Go behavior — not a copy.\n\tv2 := tree.Get(\"k\")\n\tgot2 := v2.([]int)\n\tif got2[0] != 999 {\n\t\tt.Errorf(\"Expected slice mutation to be visible (reference semantics), got %v\", got2)\n\t}\n\n\t// But replacing the value via Set should not affect previous Get results.\n\ttree.Set(\"k\", []int{10, 20, 30})\n\tv3 := tree.Get(\"k\")\n\tgot3 := v3.([]int)\n\tif got3[0] != 10 {\n\t\tt.Errorf(\"Expected new value after Set, got %v\", got3)\n\t}\n\t// The old reference should still have 999.\n\tif got[0] != 999 {\n\t\tt.Errorf(\"Old reference should still be 999, got %v\", got)\n\t}\n}\n\n//----------------------------------------\n// Comprehensive invariant check across fanouts and sizes\n\nfunc TestInvariantsAcrossFanouts(t *testing.T) {\n\tfor _, fanout := range []int{4, 5, 7, 8, 16, 32} {\n\t\ttree := NewBPTreeN(fanout)\n\t\tn := 200\n\n\t\t// Insert all.\n\t\tfor i := 0; i \u003c n; i++ {\n\t\t\ttree.Set(intToKey(i), i)\n\t\t}\n\t\tverifyInvariants(t, tree)\n\n\t\t// Remove every 3rd.\n\t\tfor i := 0; i \u003c n; i += 3 {\n\t\t\ttree.Remove(intToKey(i))\n\t\t}\n\t\tverifyInvariants(t, tree)\n\n\t\t// Remove remaining.\n\t\tfor i := 0; i \u003c n; i++ {\n\t\t\ttree.Remove(intToKey(i))\n\t\t}\n\t\tverifyInvariants(t, tree)\n\t\tif tree.Size() != 0 {\n\t\t\tt.Errorf(\"fanout=%d: expected size 0, got %d\", fanout, tree.Size())\n\t\t}\n\t}\n}\n\n//----------------------------------------\n// Deep stack unwinding\n\nfunc TestDeepStackUnwinding(t *testing.T) {\n\t// With fanout=4 and 500 keys, the tree is 4-5 levels deep.\n\t// Iterating across subtree boundaries forces advanceLeaf/retreatLeaf\n\t// to pop multiple stack levels.\n\ttree := NewBPTreeN(4)\n\tn := 500\n\tfor i := 0; i \u003c n; i++ {\n\t\ttree.Set(intToKey(i), i)\n\t}\n\n\t// Full ascending iteration — every leaf boundary is crossed.\n\tcount := 0\n\tprev := \"\"\n\ttree.Iterate(\"\", \"\", func(k string, v any) bool {\n\t\tif k \u003c= prev \u0026\u0026 prev != \"\" {\n\t\t\tt.Errorf(\"ascending order broken: %q after %q\", k, prev)\n\t\t}\n\t\tprev = k\n\t\tcount++\n\t\treturn false\n\t})\n\tif count != n {\n\t\tt.Errorf(\"ascending: visited %d, want %d\", count, n)\n\t}\n\n\t// Full descending iteration.\n\tcount = 0\n\tprev = \"\"\n\ttree.ReverseIterate(\"\", \"\", func(k string, v any) bool {\n\t\tif prev != \"\" \u0026\u0026 k \u003e= prev {\n\t\t\tt.Errorf(\"descending order broken: %q after %q\", k, prev)\n\t\t}\n\t\tprev = k\n\t\tcount++\n\t\treturn false\n\t})\n\tif count != n {\n\t\tt.Errorf(\"descending: visited %d, want %d\", count, n)\n\t}\n\n\t// Offset-based iteration crossing deep boundaries.\n\t// Start from the middle, iterate to the end.\n\tcount = 0\n\ttree.IterateByOffset(250, 250, func(k string, v any) bool {\n\t\tcount++\n\t\treturn false\n\t})\n\tif count != 250 {\n\t\tt.Errorf(\"IterateByOffset(250,250): visited %d, want 250\", count)\n\t}\n\n\t// Reverse offset from middle.\n\tcount = 0\n\ttree.ReverseIterateByOffset(250, 250, func(k string, v any) bool {\n\t\tcount++\n\t\treturn false\n\t})\n\tif count != 250 {\n\t\tt.Errorf(\"ReverseIterateByOffset(250,250): visited %d, want 250\", count)\n\t}\n\n\tverifyInvariants(t, tree)\n}\n\n//----------------------------------------\n// Height invariant\n\nfunc treeHeight(n node) int {\n\tif n == nil {\n\t\treturn 0\n\t}\n\tif n.isLeaf() {\n\t\treturn 1\n\t}\n\treturn 1 + treeHeight(n.(*innerNode).children[0])\n}\n\nfunc TestHeightInvariant(t *testing.T) {\n\t// B+ tree with fanout F should have height \u003c= 1 + log_{ceil(F/2)}(n).\n\tfor _, fanout := range []int{4, 8, 32} {\n\t\ttree := NewBPTreeN(fanout)\n\t\tn := 1000\n\t\tfor i := 0; i \u003c n; i++ {\n\t\t\ttree.Set(intToKey(i), i)\n\t\t}\n\n\t\th := treeHeight(tree.root)\n\t\t// Compute max height: log base ceil(fanout/2) of n, plus 1 for root.\n\t\tminFill := fanout / 2\n\t\tif minFill \u003c 2 {\n\t\t\tminFill = 2\n\t\t}\n\t\tmaxH := 1\n\t\tcapacity := 1\n\t\tfor capacity \u003c n {\n\t\t\tcapacity *= minFill\n\t\t\tmaxH++\n\t\t}\n\n\t\tif h \u003e maxH {\n\t\t\tt.Errorf(\"fanout=%d, n=%d: height=%d exceeds max=%d\", fanout, n, h, maxH)\n\t\t}\n\t}\n}\n\n//----------------------------------------\n// Empty string as separator key\n\nfunc TestEmptyStringSeparator(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\n\t// Insert \"\" first, then other keys. With sequential inserts,\n\t// \"\" will end up in the leftmost leaf and could become a separator.\n\ttree.Set(\"\", \"empty\")\n\ttree.Set(\"a\", \"a\")\n\ttree.Set(\"b\", \"b\")\n\ttree.Set(\"c\", \"c\")\n\ttree.Set(\"d\", \"d\") // triggers split; \"\" should be in left leaf\n\n\tverifyInvariants(t, tree)\n\n\t// Verify all keys accessible.\n\tif v := tree.Get(\"\"); v != \"empty\" {\n\t\tt.Errorf(\"Get('')=%v, want empty\", v)\n\t}\n\tfor _, k := range []string{\"a\", \"b\", \"c\", \"d\"} {\n\t\tif !tree.Has(k) {\n\t\t\tt.Errorf(\"missing key %q\", k)\n\t\t}\n\t}\n\n\t// Iterate should include \"\".\n\tgot := collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.Iterate(\"\", \"\", cb)\n\t})\n\tassertSliceEqual(t, got, []string{\"\", \"a\", \"b\", \"c\", \"d\"})\n\n\t// ReverseIterate should include \"\".\n\tgot = collectKeys(tree, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.ReverseIterate(\"\", \"\", cb)\n\t})\n\tassertSliceEqual(t, got, []string{\"d\", \"c\", \"b\", \"a\", \"\"})\n\n\t// Remove \"\" and verify tree is still valid.\n\tv, ok := tree.Remove(\"\")\n\tif !ok || v != \"empty\" {\n\t\tt.Errorf(\"Remove('')=(%v,%v), want (empty,true)\", v, ok)\n\t}\n\tverifyInvariants(t, tree)\n\tif tree.Has(\"\") {\n\t\tt.Error(\"'' should be gone after remove\")\n\t}\n\n\t// Now insert more keys with \"\" to force it into a separator position.\n\t// Build a tree where \"\" is between two inner node children.\n\ttree2 := NewBPTreeN(4)\n\t// Insert keys that will sort before and after \"\".\n\t// In ASCII, \"\" \u003c everything. So \"\" is always the smallest key.\n\t// To make \"\" a separator, we need it to be the minKey of a right child.\n\t// That happens when the leaf containing \"\" splits and \"\" ends up\n\t// as right.keys[0] (the promoted separator).\n\t// With 50/50 split: left gets lower half, right gets upper half.\n\t// Since \"\" is the smallest, it will always be in the left leaf.\n\t// With 90/10 on append: \"\" would be in the left leaf too.\n\t// So \"\" can become a separator only if it's the minKey of a right child\n\t// after a split where \"\" is in the right half.\n\t// Insert in reverse order so \"\" ends up in the right half of a split:\n\ttree2.Set(\"d\", \"d\")\n\ttree2.Set(\"c\", \"c\")\n\ttree2.Set(\"b\", \"b\")\n\ttree2.Set(\"a\", \"a\")\n\ttree2.Set(\"\", \"empty\") // inserted at position 0 (not append), 50/50 split\n\n\tverifyInvariants(t, tree2)\n\tgot = collectKeys(tree2, func(tr *BPTree, cb IterCbFn) bool {\n\t\treturn tr.Iterate(\"\", \"\", cb)\n\t})\n\tassertSliceEqual(t, got, []string{\"\", \"a\", \"b\", \"c\", \"d\"})\n}\n\n//----------------------------------------\n// Pointer independence after split\n\nfunc TestPointerIndependenceAfterSplit(t *testing.T) {\n\ttree := NewBPTreeN(4)\n\n\t// Insert 5 keys to trigger a split.\n\ttree.Set(\"a\", \"val_a\")\n\ttree.Set(\"b\", \"val_b\")\n\ttree.Set(\"c\", \"val_c\")\n\ttree.Set(\"d\", \"val_d\")\n\ttree.Set(\"e\", \"val_e\") // triggers split\n\n\t// Get values from both halves of the split.\n\tvLeft := tree.Get(\"a\")\n\tvRight := tree.Get(\"d\")\n\n\tif vLeft != \"val_a\" {\n\t\tt.Errorf(\"left value: got %v, want val_a\", vLeft)\n\t}\n\tif vRight != \"val_d\" {\n\t\tt.Errorf(\"right value: got %v, want val_d\", vRight)\n\t}\n\n\t// Update a value in the left half — should not affect right half.\n\ttree.Set(\"a\", \"new_a\")\n\tvLeft2 := tree.Get(\"a\")\n\tvRight2 := tree.Get(\"d\")\n\tif vLeft2 != \"new_a\" {\n\t\tt.Errorf(\"updated left: got %v, want new_a\", vLeft2)\n\t}\n\tif vRight2 != \"val_d\" {\n\t\tt.Errorf(\"right should be unchanged: got %v, want val_d\", vRight2)\n\t}\n\n\t// Update a value in the right half — should not affect left half.\n\ttree.Set(\"d\", \"new_d\")\n\tvLeft3 := tree.Get(\"a\")\n\tvRight3 := tree.Get(\"d\")\n\tif vLeft3 != \"new_a\" {\n\t\tt.Errorf(\"left should be unchanged: got %v, want new_a\", vLeft3)\n\t}\n\tif vRight3 != \"new_d\" {\n\t\tt.Errorf(\"updated right: got %v, want new_d\", vRight3)\n\t}\n\n\t// Verify that the *any pointers in the two leaves are different objects.\n\t// Access internals to check.\n\tinner := tree.root.(*innerNode)\n\tleftLeaf := inner.children[0].(*leafNode)\n\trightLeaf := inner.children[1].(*leafNode)\n\n\t// Each value pointer should be unique.\n\tseen := make(map[*any]bool)\n\tfor _, vp := range leftLeaf.values {\n\t\tif seen[vp] {\n\t\t\tt.Error(\"duplicate *any pointer in left leaf\")\n\t\t}\n\t\tseen[vp] = true\n\t}\n\tfor _, vp := range rightLeaf.values {\n\t\tif seen[vp] {\n\t\t\tt.Error(\"*any pointer shared between left and right leaves after split\")\n\t\t}\n\t\tseen[vp] = true\n\t}\n}\n\n//----------------------------------------\n// Helpers\n\nfunc slicesEqual(a, b []string) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i := range a {\n\t\tif a[i] != b[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc intToKey(i int) string {\n\t// Zero-padded 4-digit string for correct lexicographic ordering.\n\ts := \"0000\"\n\tn := i\n\tb := []byte(s)\n\tfor j := 3; j \u003e= 0; j-- {\n\t\tb[j] = byte('0' + n%10)\n\t\tn /= 10\n\t}\n\treturn string(b)\n}\n\nfunc collectKeys(tree *BPTree, fn func(*BPTree, IterCbFn) bool) []string {\n\tvar keys []string\n\tfn(tree, func(k string, v any) bool {\n\t\tkeys = append(keys, k)\n\t\treturn false\n\t})\n\treturn keys\n}\n\nfunc assertSliceEqual(t *testing.T, got, want []string) {\n\tt.Helper()\n\tif len(got) != len(want) {\n\t\tt.Errorf(\"got %v, want %v\", got, want)\n\t\treturn\n\t}\n\tfor i := range got {\n\t\tif got[i] != want[i] {\n\t\t\tt.Errorf(\"got %v, want %v\", got, want)\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc assertPanics(t *testing.T, name string, fn func()) {\n\tt.Helper()\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Errorf(\"%s: expected panic, got none\", name)\n\t\t}\n\t}()\n\tfn()\n}\n\n//----------------------------------------\n// Stress tests\n\n// TestAVLCrossValidation runs identical random operations on both avl.Tree\n// and BPTree and compares every return value.\nfunc TestAVLCrossValidation(t *testing.T) {\n\trng := rand.New(rand.NewPCG(12345, 0))\n\tat := avl.NewTree()\n\tbt := NewBPTree32()\n\n\tconst nOps = 5000\n\tconst keyRange = 200\n\n\tfor i := 0; i \u003c nOps; i++ {\n\t\tkey := intToKey(rng.IntN(keyRange))\n\t\top := rng.IntN(3)\n\t\tswitch op {\n\t\tcase 0: // Set\n\t\t\taUpd := at.Set(key, i)\n\t\t\tbUpd := bt.Set(key, i)\n\t\t\tif aUpd != bUpd {\n\t\t\t\tt.Fatalf(\"op %d: Set(%q) updated avl=%v bpt=%v\", i, key, aUpd, bUpd)\n\t\t\t}\n\t\tcase 1: // Remove\n\t\t\taVal, aOk := at.Remove(key)\n\t\t\tbVal, bOk := bt.Remove(key)\n\t\t\tif aOk != bOk {\n\t\t\t\tt.Fatalf(\"op %d: Remove(%q) avl=%v bpt=%v\", i, key, aOk, bOk)\n\t\t\t}\n\t\t\tif aOk \u0026\u0026 aVal != bVal {\n\t\t\t\tt.Fatalf(\"op %d: Remove(%q) value avl=%v bpt=%v\", i, key, aVal, bVal)\n\t\t\t}\n\t\tcase 2: // Get\n\t\t\taVal := at.Get(key)\n\t\t\taOk := at.Has(key)\n\t\t\tbVal := bt.Get(key)\n\t\t\tbOk := bt.Has(key)\n\t\t\tif aOk != bOk {\n\t\t\t\tt.Fatalf(\"op %d: exists avl=%v bpt=%v\", i, aOk, bOk)\n\t\t\t}\n\t\t\tif aOk \u0026\u0026 aVal != bVal {\n\t\t\t\tt.Fatalf(\"op %d: Get(%q) value avl=%v bpt=%v\", i, key, aVal, bVal)\n\t\t\t}\n\t\t}\n\t\tif at.Size() != bt.Size() {\n\t\t\tt.Fatalf(\"op %d: size avl=%d bpt=%d\", i, at.Size(), bt.Size())\n\t\t}\n\t}\n\n\t// Compare full iteration.\n\tvar aKeys, bKeys []string\n\tat.Iterate(\"\", \"\", func(k string, v any) bool { aKeys = append(aKeys, k); return false })\n\tbt.Iterate(\"\", \"\", func(k string, v any) bool { bKeys = append(bKeys, k); return false })\n\tassertSliceEqual(t, bKeys, aKeys)\n}\n\n// TestExhaustiveRemovalPermutations inserts N keys then tries all N!\n// permutations of removal order, verifying invariants after each remove.\nfunc TestExhaustiveRemovalPermutations(t *testing.T) {\n\tkeys := []string{\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\"}\n\tn := len(keys)\n\n\tperm := make([]int, n)\n\tfor i := range perm {\n\t\tperm[i] = i\n\t}\n\n\tcount := 0\n\tpermute(perm, 0, func(order []int) {\n\t\ttree := NewBPTreeN(4)\n\t\tfor _, k := range keys {\n\t\t\ttree.Set(k, k)\n\t\t}\n\t\tfor _, idx := range order {\n\t\t\ttree.Remove(keys[idx])\n\t\t\tverifyInvariants(t, tree)\n\t\t}\n\t\tif tree.Size() != 0 {\n\t\t\tt.Fatalf(\"perm %d: tree not empty after removing all keys\", count)\n\t\t}\n\t\tcount++\n\t})\n}\n\n// permute generates all permutations of arr[start:] and calls fn for each.\nfunc permute(arr []int, start int, fn func([]int)) {\n\tif start == len(arr) {\n\t\tfn(arr)\n\t\treturn\n\t}\n\tfor i := start; i \u003c len(arr); i++ {\n\t\tarr[start], arr[i] = arr[i], arr[start]\n\t\tpermute(arr, start+1, fn)\n\t\tarr[start], arr[i] = arr[i], arr[start]\n\t}\n}\n\n// TestMultiFanoutStress runs the same operation sequence across different\n// fanouts and verifies all produce identical results.\nfunc TestMultiFanoutStress(t *testing.T) {\n\tfanouts := []int{4, 5, 6, 7, 8, 16, 32}\n\tconst nOps = 2000\n\tconst keyRange = 100\n\n\t// Record expected results from the first fanout.\n\ttype result struct {\n\t\tsetUpdated bool\n\t\tgetVal     any\n\t\tgetOk      bool\n\t\trmVal      any\n\t\trmOk       bool\n\t}\n\n\trng0 := rand.New(rand.NewPCG(99999, 0))\n\ttype op struct {\n\t\tkind int // 0=set, 1=remove, 2=get\n\t\tkey  string\n\t\tval  int\n\t}\n\tops := make([]op, nOps)\n\tfor i := range ops {\n\t\tops[i] = op{\n\t\t\tkind: rng0.IntN(3),\n\t\t\tkey:  intToKey(rng0.IntN(keyRange)),\n\t\t\tval:  i,\n\t\t}\n\t}\n\n\t// Run on each fanout and collect final keys.\n\tvar referenceKeys []string\n\tfor fi, fanout := range fanouts {\n\t\ttree := NewBPTreeN(fanout)\n\t\tfor _, o := range ops {\n\t\t\tswitch o.kind {\n\t\t\tcase 0:\n\t\t\t\ttree.Set(o.key, o.val)\n\t\t\tcase 1:\n\t\t\t\ttree.Remove(o.key)\n\t\t\tcase 2:\n\t\t\t\ttree.Get(o.key)\n\t\t\t}\n\t\t}\n\t\tverifyInvariants(t, tree)\n\n\t\tvar keys []string\n\t\ttree.Iterate(\"\", \"\", func(k string, v any) bool {\n\t\t\tkeys = append(keys, k)\n\t\t\treturn false\n\t\t})\n\n\t\tif fi == 0 {\n\t\t\treferenceKeys = keys\n\t\t} else {\n\t\t\tassertSliceEqual(t, keys, referenceKeys)\n\t\t}\n\t}\n}\n\n// TestSequentialInsertReverseRemove inserts keys 0..N-1 sequentially\n// (triggering 90/10 splits) then removes them in reverse order\n// (triggering cascading merges from the right).\nfunc TestSequentialInsertReverseRemove(t *testing.T) {\n\tfor _, fanout := range []int{4, 6, 8, 32} {\n\t\ttree := NewBPTreeN(fanout)\n\t\tn := 500\n\t\tfor i := 0; i \u003c n; i++ {\n\t\t\ttree.Set(intToKey(i), i)\n\t\t}\n\t\tverifyInvariants(t, tree)\n\t\tif tree.Size() != n {\n\t\t\tt.Errorf(\"fanout=%d: expected size %d, got %d\", fanout, n, tree.Size())\n\t\t}\n\n\t\tfor i := n - 1; i \u003e= 0; i-- {\n\t\t\tval, ok := tree.Remove(intToKey(i))\n\t\t\tif !ok {\n\t\t\t\tt.Fatalf(\"fanout=%d: Remove(%s) returned false\", fanout, intToKey(i))\n\t\t\t}\n\t\t\tif val != i {\n\t\t\t\tt.Fatalf(\"fanout=%d: Remove(%s) value=%v want %d\", fanout, intToKey(i), val, i)\n\t\t\t}\n\t\t\tverifyInvariants(t, tree)\n\t\t}\n\t\tif tree.Size() != 0 {\n\t\t\tt.Errorf(\"fanout=%d: expected empty tree, got size %d\", fanout, tree.Size())\n\t\t}\n\t}\n}\n\n// TestRandomOpsWithPeriodicVerification does 10K random Set/Remove calls,\n// verifying invariants and sorted iteration every 100 ops.\nfunc TestRandomOpsWithPeriodicVerification(t *testing.T) {\n\trng := rand.New(rand.NewPCG(54321, 0))\n\ttree := NewBPTreeN(4)\n\n\tconst nOps = 10000\n\tconst keyRange = 300\n\tconst checkEvery = 100\n\n\t// Track expected keys in a sorted slice for comparison.\n\tpresent := make(map[string]bool)\n\n\tfor i := 0; i \u003c nOps; i++ {\n\t\tkey := intToKey(rng.IntN(keyRange))\n\t\tif rng.IntN(3) == 0 { // ~33% removes\n\t\t\t_, ok := tree.Remove(key)\n\t\t\tif ok {\n\t\t\t\tdelete(present, key)\n\t\t\t}\n\t\t} else { // ~67% sets\n\t\t\ttree.Set(key, i)\n\t\t\tpresent[key] = true\n\t\t}\n\n\t\tif (i+1)%checkEvery == 0 {\n\t\t\tverifyInvariants(t, tree)\n\n\t\t\t// Check size.\n\t\t\tif tree.Size() != len(present) {\n\t\t\t\tt.Fatalf(\"op %d: size mismatch tree=%d map=%d\", i, tree.Size(), len(present))\n\t\t\t}\n\n\t\t\t// Check iteration matches sorted keys.\n\t\t\tvar expected []string\n\t\t\tfor k := range present {\n\t\t\t\texpected = append(expected, k)\n\t\t\t}\n\t\t\tsort.Strings(expected)\n\n\t\t\tvar got []string\n\t\t\ttree.Iterate(\"\", \"\", func(k string, v any) bool {\n\t\t\t\tgot = append(got, k)\n\t\t\t\treturn false\n\t\t\t})\n\t\t\tassertSliceEqual(t, got, expected)\n\t\t}\n\t}\n}\n\n// TestGetByIndexIterateByOffsetConsistency verifies that GetByIndex(i)\n// matches IterateByOffset(i, 1) for every valid index.\nfunc TestGetByIndexIterateByOffsetConsistency(t *testing.T) {\n\trng := rand.New(rand.NewPCG(77777, 0))\n\ttree := NewBPTreeN(4)\n\n\t// Build a tree with random insertions and removals.\n\tconst nOps = 1000\n\tconst keyRange = 200\n\tfor i := 0; i \u003c nOps; i++ {\n\t\tkey := intToKey(rng.IntN(keyRange))\n\t\tif rng.IntN(4) == 0 {\n\t\t\ttree.Remove(key)\n\t\t} else {\n\t\t\ttree.Set(key, i)\n\t\t}\n\t}\n\n\tn := tree.Size()\n\tfor i := 0; i \u003c n; i++ {\n\t\tgKey, gVal := tree.GetByIndex(i)\n\n\t\tvar iKey string\n\t\tvar iVal any\n\t\ttree.IterateByOffset(i, 1, func(k string, v any) bool {\n\t\t\tiKey = k\n\t\t\tiVal = v\n\t\t\treturn true\n\t\t})\n\n\t\tif gKey != iKey {\n\t\t\tt.Fatalf(\"index %d: GetByIndex key=%q IterateByOffset key=%q\", i, gKey, iKey)\n\t\t}\n\t\tif gVal != iVal {\n\t\t\tt.Fatalf(\"index %d: GetByIndex val=%v IterateByOffset val=%v\", i, gVal, iVal)\n\t\t}\n\t}\n\n\t// Also check ReverseIterateByOffset consistency.\n\tfor i := 0; i \u003c n; i++ {\n\t\tgKey, gVal := tree.GetByIndex(n - 1 - i)\n\n\t\tvar rKey string\n\t\tvar rVal any\n\t\ttree.ReverseIterateByOffset(i, 1, func(k string, v any) bool {\n\t\t\trKey = k\n\t\t\trVal = v\n\t\t\treturn true\n\t\t})\n\n\t\tif gKey != rKey {\n\t\t\tt.Fatalf(\"rev index %d: GetByIndex key=%q ReverseIterateByOffset key=%q\", i, gKey, rKey)\n\t\t}\n\t\tif gVal != rVal {\n\t\t\tt.Fatalf(\"rev index %d: GetByIndex val=%v ReverseIterateByOffset val=%v\", i, gVal, rVal)\n\t\t}\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"mONg8VdRa6vFS19OUfw49Mj5ZxgXWwQlB8rNJjZMGG0z4OFeTqMWsK0mWhJ7qN1Yus1l3wyhhY5C/SQXYO+OFw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"boards","path":"gno.land/p/gnoland/boards/v0","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":"board_test.gno","body":"package boards_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/urequire/v0\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n)\n\nfunc TestNew(t *testing.T) {\n\tboard := boards.New(42)\n\n\turequire.Equal(t, 42, int(board.ID), \"expect board ID to match\")\n\turequire.True(t, board.Threads != nil, \"expect board to support threads\")\n\turequire.True(t, board.ThreadsSequence != nil, \"expect board to initialize a thread ID generator\")\n\turequire.False(t, board.CreatedAt.IsZero(), \"expect board to have a creation date\")\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":"flag_storage_test.gno","body":"package boards_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/urequire/v0\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n)\n\nfunc TestFlagStorageExists(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tsetup  func() boards.FlagStorage\n\t\tuser   address\n\t\texists bool\n\t}{\n\t\t{\n\t\t\tname: \"found\",\n\t\t\tsetup: func() boards.FlagStorage {\n\t\t\t\ts := boards.NewFlagStorage()\n\t\t\t\ts.Add(boards.Flag{User: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"})\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tuser:   \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\texists: true,\n\t\t},\n\t\t{\n\t\t\tname: \"not found\",\n\t\t\tsetup: func() boards.FlagStorage {\n\t\t\t\treturn boards.NewFlagStorage()\n\t\t\t},\n\t\t\tuser:   \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\texists: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := tt.setup()\n\n\t\t\turequire.Equal(t, tt.exists, s.Exists(tt.user))\n\t\t})\n\t}\n}\n\nfunc TestFlagStorageAdd(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tsetup  func() boards.FlagStorage\n\t\tflag   boards.Flag\n\t\terrMsg string\n\t}{\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\tsetup: func() boards.FlagStorage {\n\t\t\t\treturn boards.NewFlagStorage()\n\t\t\t},\n\t\t\tflag: boards.Flag{User: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"},\n\t\t},\n\t\t{\n\t\t\tname: \"flag exists\",\n\t\t\tsetup: func() boards.FlagStorage {\n\t\t\t\ts := boards.NewFlagStorage()\n\t\t\t\ts.Add(boards.Flag{User: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"})\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tflag:   boards.Flag{User: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"},\n\t\t\terrMsg: \"flag from user already exists: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t},\n\t\t{\n\t\t\tname: \"invalid user address\",\n\t\t\tsetup: func() boards.FlagStorage {\n\t\t\t\treturn boards.NewFlagStorage()\n\t\t\t},\n\t\t\tflag:   boards.Flag{User: \"foo\"},\n\t\t\terrMsg: \"post flagging error, invalid user address: foo\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := tt.setup()\n\n\t\t\terr := s.Add(tt.flag)\n\n\t\t\tif tt.errMsg != \"\" {\n\t\t\t\turequire.Error(t, err, \"expect an error\")\n\t\t\t\turequire.ErrorContains(t, err, tt.errMsg, \"expect error to match\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.NoError(t, err, \"expect no error\")\n\t\t\turequire.True(t, s.Exists(tt.flag.User), \"expect flag to be added\")\n\t\t})\n\t}\n}\n\nfunc TestFlagStorageRemove(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tsetup   func() boards.FlagStorage\n\t\taddress address\n\t\tremoved bool\n\t}{\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\tsetup: func() boards.FlagStorage {\n\t\t\t\ts := boards.NewFlagStorage()\n\t\t\t\ts.Add(boards.Flag{User: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"})\n\t\t\t\treturn s\n\t\t\t},\n\t\t\taddress: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\tremoved: true,\n\t\t},\n\t\t{\n\t\t\tname: \"not found\",\n\t\t\tsetup: func() boards.FlagStorage {\n\t\t\t\treturn boards.NewFlagStorage()\n\t\t\t},\n\t\t\taddress: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := tt.setup()\n\n\t\t\turequire.Equal(t, tt.removed, s.Remove(tt.address))\n\t\t})\n\t}\n}\n\nfunc TestFlagStorageSize(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\tsetup func() boards.FlagStorage\n\t\tsize  int\n\t}{\n\t\t{\n\t\t\tname: \"empty\",\n\t\t\tsetup: func() boards.FlagStorage {\n\t\t\t\treturn boards.NewFlagStorage()\n\t\t\t},\n\t\t\tsize: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"one flag\",\n\t\t\tsetup: func() boards.FlagStorage {\n\t\t\t\ts := boards.NewFlagStorage()\n\t\t\t\ts.Add(boards.Flag{User: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"})\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tsize: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple flags\",\n\t\t\tsetup: func() boards.FlagStorage {\n\t\t\t\ts := boards.NewFlagStorage()\n\t\t\t\ts.Add(boards.Flag{User: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"})\n\t\t\t\ts.Add(boards.Flag{User: \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"})\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tsize: 2,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := tt.setup()\n\n\t\t\turequire.Equal(t, tt.size, s.Size())\n\t\t})\n\t}\n}\n\nfunc TestFlagStorageIterate(t *testing.T) {\n\t// Listed in sorted (bech32-string) user order, which is the order\n\t// Iterate yields them in.\n\tflags := []boards.Flag{\n\t\t{\n\t\t\tUser:   \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\tReason: \"a\",\n\t\t},\n\t\t{\n\t\t\tUser:   \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\",\n\t\t\tReason: \"b\",\n\t\t},\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tsetup   func() boards.FlagStorage\n\t\treverse bool\n\t\tflags   []boards.Flag\n\t}{\n\t\t{\n\t\t\tname: \"default\",\n\t\t\tsetup: func() boards.FlagStorage {\n\t\t\t\ts := boards.NewFlagStorage()\n\t\t\t\ts.Add(flags[0])\n\t\t\t\ts.Add(flags[1])\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tflags: flags,\n\t\t},\n\t\t{\n\t\t\tname: \"reverse\",\n\t\t\tsetup: func() boards.FlagStorage {\n\t\t\t\ts := boards.NewFlagStorage()\n\t\t\t\ts.Add(flags[0])\n\t\t\t\ts.Add(flags[1])\n\t\t\t\treturn s\n\t\t\t},\n\t\t\treverse: true,\n\t\t\tflags:   []boards.Flag{flags[1], flags[0]},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := tt.setup()\n\t\t\tcount := s.Size()\n\t\t\tif tt.reverse {\n\t\t\t\tcount = -count\n\t\t\t}\n\n\t\t\tvar i int\n\t\t\ts.Iterate(0, count, func(f boards.Flag) bool {\n\t\t\t\turequire.Equal(t, tt.flags[i].User, f.User, \"expect user to match\")\n\t\t\t\turequire.Equal(t, tt.flags[i].Reason, f.Reason, \"expect reason to match\")\n\n\t\t\t\ti++\n\t\t\t\treturn false\n\t\t\t})\n\t\t})\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/gnoland/boards/v0\"\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":"id_test.gno","body":"package boards_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/urequire/v0\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n)\n\nfunc TestID(t *testing.T) {\n\tid := boards.ID(42)\n\n\turequire.Equal(t, \"42\", id.String(), \"expect string to match\")\n\turequire.Equal(t, \"0000000042\", id.PaddedString(), \"expect padded string to match\")\n\turequire.Equal(t, \"000001a\", id.Key(), \"expect key to match\")\n}\n\nfunc TestIdentifierGenerator(t *testing.T) {\n\tg := boards.NewIdentifierGenerator()\n\n\turequire.Equal(t, uint64(0), uint64(g.Last()), \"expect default to be 0\")\n\turequire.Equal(t, uint64(1), uint64(g.Next()), \"expect next to be 1\")\n\turequire.Equal(t, uint64(1), uint64(g.Last()), \"expect last to be 1\")\n\turequire.Equal(t, uint64(2), uint64(g.Next()), \"expect next to be 2\")\n\turequire.Equal(t, uint64(2), uint64(g.Last()), \"expect last to be 2\")\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":"permission_set_test.gno","body":"package boards\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nfunc TestNewPermissionSet(t *testing.T) {\n\tcases := []struct {\n\t\tname  string\n\t\tperms []Permission\n\t\tcheck []Permission\n\t\twant  bool\n\t}{\n\t\t{\n\t\t\tname:  \"empty\",\n\t\t\tcheck: []Permission{0},\n\t\t\twant:  false,\n\t\t},\n\t\t{\n\t\t\tname:  \"single permission\",\n\t\t\tperms: []Permission{0},\n\t\t\tcheck: []Permission{0},\n\t\t\twant:  true,\n\t\t},\n\t\t{\n\t\t\tname:  \"multiple permissions\",\n\t\t\tperms: []Permission{1, 3, 5},\n\t\t\tcheck: []Permission{1, 3, 5},\n\t\t\twant:  true,\n\t\t},\n\t\t{\n\t\t\tname:  \"high permission value\",\n\t\t\tperms: []Permission{100},\n\t\t\tcheck: []Permission{100},\n\t\t\twant:  true,\n\t\t},\n\t\t{\n\t\t\tname:  \"missing permission\",\n\t\t\tperms: []Permission{100},\n\t\t\tcheck: []Permission{0},\n\t\t\twant:  false,\n\t\t},\n\t\t{\n\t\t\tname:  \"multiple missing permissions\",\n\t\t\tperms: []Permission{1, 3, 5},\n\t\t\tcheck: []Permission{0, 2, 4},\n\t\t\twant:  false,\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\ts := NewPermissionSet(tc.perms...)\n\n\t\t\tfor _, p := range tc.check {\n\t\t\t\tuassert.Equal(t, tc.want, s.Has(p))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestPermissionSetHas(t *testing.T) {\n\tcases := []struct {\n\t\tname  string\n\t\tset   PermissionSet\n\t\tcheck Permission\n\t\twant  bool\n\t}{\n\t\t{\n\t\t\tname:  \"out of range\",\n\t\t\tset:   NewPermissionSet(0),\n\t\t\tcheck: 100,\n\t\t\twant:  false,\n\t\t},\n\t\t{\n\t\t\tname:  \"nil set\",\n\t\t\tcheck: 0,\n\t\t\twant:  false,\n\t\t},\n\t\t{\n\t\t\tname:  \"permission present\",\n\t\t\tset:   NewPermissionSet(5),\n\t\t\tcheck: 5,\n\t\t\twant:  true,\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tuassert.Equal(t, tc.want, tc.set.Has(tc.check))\n\t\t})\n\t}\n}\n\nfunc TestPermissionSetIsEmpty(t *testing.T) {\n\tcases := []struct {\n\t\tname string\n\t\tset  PermissionSet\n\t\twant bool\n\t}{\n\t\t{\n\t\t\tname: \"nil set\",\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"non-empty set\",\n\t\t\tset:  NewPermissionSet(0),\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"empty allocated set\",\n\t\t\tset:  make(PermissionSet, 1),\n\t\t\twant: true,\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tuassert.Equal(t, tc.want, tc.set.IsEmpty())\n\t\t})\n\t}\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":"post_storage_test.gno","body":"package boards_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/urequire/v0\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n)\n\nfunc TestPostStorageGet(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tsetup  func() boards.PostStorage\n\t\tpostID boards.ID\n\t\tfound  bool\n\t}{\n\t\t{\n\t\t\tname: \"single post\",\n\t\t\tsetup: func() boards.PostStorage {\n\t\t\t\ts := boards.NewPostStorage()\n\t\t\t\ts.Add(\u0026boards.Post{ID: 1})\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tpostID: 1,\n\t\t\tfound:  true,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple posts\",\n\t\t\tsetup: func() boards.PostStorage {\n\t\t\t\ts := boards.NewPostStorage()\n\t\t\t\ts.Add(\u0026boards.Post{ID: 1})\n\t\t\t\ts.Add(\u0026boards.Post{ID: 2})\n\t\t\t\ts.Add(\u0026boards.Post{ID: 3})\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tpostID: 2,\n\t\t\tfound:  true,\n\t\t},\n\t\t{\n\t\t\tname: \"not found\",\n\t\t\tsetup: func() boards.PostStorage {\n\t\t\t\treturn boards.NewPostStorage()\n\t\t\t},\n\t\t\tpostID: 404,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := tt.setup()\n\n\t\t\tpost, found := s.Get(tt.postID)\n\n\t\t\tif !tt.found {\n\t\t\t\turequire.False(t, found, \"expect post not to be found\")\n\t\t\t\turequire.True(t, post == nil, \"expect post to be nil\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.True(t, found, \"expect post to be found\")\n\t\t\turequire.False(t, post == nil, \"expect post not to be nil\")\n\t\t\turequire.Equal(t, tt.postID.String(), post.ID.String(), \"expect post ID to match\")\n\t\t})\n\t}\n}\n\nfunc TestPostStorageRemove(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tsetup   func() boards.PostStorage\n\t\tpostID  boards.ID\n\t\tremoved bool\n\t}{\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\tsetup: func() boards.PostStorage {\n\t\t\t\ts := boards.NewPostStorage()\n\t\t\t\ts.Add(\u0026boards.Post{ID: 1})\n\t\t\t\ts.Add(\u0026boards.Post{ID: 2})\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tpostID:  2,\n\t\t\tremoved: true,\n\t\t},\n\t\t{\n\t\t\tname: \"not found\",\n\t\t\tsetup: func() boards.PostStorage {\n\t\t\t\treturn boards.NewPostStorage()\n\t\t\t},\n\t\t\tpostID: 404,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := tt.setup()\n\n\t\t\tpost, removed := s.Remove(tt.postID)\n\n\t\t\tif !tt.removed {\n\t\t\t\turequire.False(t, removed, \"expect post not to be removed\")\n\t\t\t\turequire.True(t, post == nil, \"expect post to be nil\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.True(t, removed, \"expect post to be removed\")\n\t\t\turequire.False(t, post == nil, \"expect post not to be nil\")\n\t\t\turequire.Equal(t, tt.postID.String(), post.ID.String(), \"expect post ID to match\")\n\n\t\t\t_, found := s.Get(tt.postID)\n\t\t\turequire.False(t, found, \"expect post not to be found\")\n\t\t})\n\t}\n}\n\nfunc TestPostStorageAdd(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tpost   *boards.Post\n\t\terrMsg string\n\t}{\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\tpost: \u0026boards.Post{ID: 1},\n\t\t},\n\t\t{\n\t\t\tname:   \"nil post\",\n\t\t\tpost:   nil,\n\t\t\terrMsg: \"saving nil posts is not allowed\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := boards.NewPostStorage()\n\n\t\t\terr := s.Add(tt.post)\n\n\t\t\tif tt.errMsg != \"\" {\n\t\t\t\turequire.Error(t, err, \"expect an error\")\n\t\t\t\turequire.ErrorContains(t, err, tt.errMsg, \"expect error to match\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.NoError(t, err, \"expect no error\")\n\n\t\t\t_, found := s.Get(tt.post.ID)\n\t\t\turequire.True(t, found, \"expect post to be found\")\n\t\t})\n\t}\n}\n\nfunc TestPostStorageSize(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\tsetup func() boards.PostStorage\n\t\tsize  int\n\t}{\n\t\t{\n\t\t\tname: \"empty\",\n\t\t\tsetup: func() boards.PostStorage {\n\t\t\t\treturn boards.NewPostStorage()\n\t\t\t},\n\t\t\tsize: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"one post\",\n\t\t\tsetup: func() boards.PostStorage {\n\t\t\t\ts := boards.NewPostStorage()\n\t\t\t\ts.Add(\u0026boards.Post{ID: 1})\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tsize: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple posts\",\n\t\t\tsetup: func() boards.PostStorage {\n\t\t\t\ts := boards.NewPostStorage()\n\t\t\t\ts.Add(\u0026boards.Post{ID: 1})\n\t\t\t\ts.Add(\u0026boards.Post{ID: 2})\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tsize: 2,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := tt.setup()\n\n\t\t\turequire.Equal(t, tt.size, s.Size())\n\t\t})\n\t}\n}\n\nfunc TestPostStorageIterate(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tsetup   func() boards.PostStorage\n\t\treverse bool\n\t\tids     []boards.ID\n\t}{\n\t\t{\n\t\t\tname: \"default\",\n\t\t\tsetup: func() boards.PostStorage {\n\t\t\t\ts := boards.NewPostStorage()\n\t\t\t\ts.Add(\u0026boards.Post{ID: 1})\n\t\t\t\ts.Add(\u0026boards.Post{ID: 2})\n\t\t\t\ts.Add(\u0026boards.Post{ID: 3})\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tids: []boards.ID{1, 2, 3},\n\t\t},\n\t\t{\n\t\t\tname: \"reverse\",\n\t\t\tsetup: func() boards.PostStorage {\n\t\t\t\ts := boards.NewPostStorage()\n\t\t\t\ts.Add(\u0026boards.Post{ID: 1})\n\t\t\t\ts.Add(\u0026boards.Post{ID: 2})\n\t\t\t\ts.Add(\u0026boards.Post{ID: 3})\n\t\t\t\treturn s\n\t\t\t},\n\t\t\treverse: true,\n\t\t\tids:     []boards.ID{3, 2, 1},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := tt.setup()\n\t\t\tcount := s.Size()\n\t\t\tif tt.reverse {\n\t\t\t\tcount = -count\n\t\t\t}\n\n\t\t\tvar i int\n\t\t\ts.Iterate(0, count, func(p *boards.Post) bool {\n\t\t\t\turequire.True(t, tt.ids[i] == p.ID, \"expect post ID to match\")\n\n\t\t\t\ti++\n\t\t\t\treturn false\n\t\t\t})\n\t\t})\n\t}\n}\n"},{"name":"post_test.gno","body":"package boards_test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/urequire/v0\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n)\n\nfunc TestPostSummary(t *testing.T) {\n\tpost := \u0026boards.Post{ID: 1, Body: strings.Repeat(\"X\", 900)}\n\tsummary := post.Summary()\n\turequire.True(t, strings.HasSuffix(summary, \"...\"), \"expect dotted suffix\")\n\turequire.True(t, len(summary) == 80, \"expect summary length to match\")\n}\n\nfunc TestIsThread(t *testing.T) {\n\tpost := \u0026boards.Post{ID: 1, ThreadID: 1} // IDs match\n\turequire.True(t, boards.IsThread(post), \"expect post to be a thread\")\n\turequire.False(t, boards.IsThread(nil), \"expect nil not to be a thread\")\n\n\tpost = \u0026boards.Post{ID: 2, ThreadID: 1} // IDs doesn't match\n\turequire.False(t, boards.IsThread(post), \"expect post not to be a thread\")\n}\n\nfunc TestIsRepost(t *testing.T) {\n\tpost := \u0026boards.Post{ID: 1, OriginalBoardID: 1} // Original board ID available\n\turequire.True(t, boards.IsRepost(post), \"expect post to be a repost\")\n\turequire.False(t, boards.IsRepost(nil), \"expect nil not to be a repost\")\n\n\tpost = \u0026boards.Post{ID: 1} // Original board ID not available\n\turequire.False(t, boards.IsRepost(post), \"expect post not to be a repost\")\n}\n\nfunc TestSummaryOf(t *testing.T) {\n\tsummary := boards.SummaryOf(strings.Repeat(\"X\", 90), 80)\n\turequire.True(t, strings.HasSuffix(summary, \"...\"), \"expect dotted suffix\")\n\turequire.True(t, len(summary) == 80, \"expect summary length to match\")\n\n\tsummary = boards.SummaryOf(strings.Repeat(\" \", 90), 80)\n\turequire.Empty(t, summary, \"expect summary to be empty\")\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":"reply_test.gno","body":"package boards_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/urequire/v0\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n)\n\nfunc TestNewReply(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tparent  func() *boards.Post\n\t\tcreator address\n\t\tbody    string\n\t\terrMsg  string\n\t}{\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\tparent: func() *boards.Post {\n\t\t\t\tboard := boards.New(1)\n\t\t\t\tid := board.ThreadsSequence.Next()\n\t\t\t\treturn \u0026boards.Post{\n\t\t\t\t\tID:       id,\n\t\t\t\t\tThreadID: id,\n\t\t\t\t\tBoard:    board,\n\t\t\t\t}\n\t\t\t},\n\t\t\tcreator: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\tbody:    \"Foo\",\n\t\t},\n\t\t{\n\t\t\tname:   \"nil parent\",\n\t\t\tparent: func() *boards.Post { return nil },\n\t\t\terrMsg: \"reply requires a parent thread or reply\",\n\t\t},\n\t\t{\n\t\t\tname: \"parent without thread ID\",\n\t\t\tparent: func() *boards.Post {\n\t\t\t\treturn \u0026boards.Post{ID: 1}\n\t\t\t},\n\t\t\terrMsg: \"parent has no thread ID assigned\",\n\t\t},\n\t\t{\n\t\t\tname: \"parent without board\",\n\t\t\tparent: func() *boards.Post {\n\t\t\t\treturn \u0026boards.Post{ID: 1, ThreadID: 1}\n\t\t\t},\n\t\t\terrMsg: \"parent has no board assigned\",\n\t\t},\n\t\t{\n\t\t\tname: \"invalid creator\",\n\t\t\tparent: func() *boards.Post {\n\t\t\t\treturn \u0026boards.Post{ID: 1, ThreadID: 1, Board: boards.New(1)}\n\t\t\t},\n\t\t\tcreator: \"foo\",\n\t\t\terrMsg:  \"invalid reply creator address: foo\",\n\t\t},\n\t\t{\n\t\t\tname: \"empty body\",\n\t\t\tparent: func() *boards.Post {\n\t\t\t\treturn \u0026boards.Post{ID: 1, ThreadID: 1, Board: boards.New(1)}\n\t\t\t},\n\t\t\tcreator: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\tbody:    \"\",\n\t\t\terrMsg:  \"reply body is required\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tparent := tt.parent()\n\n\t\t\treply, err := boards.NewReply(parent, tt.creator, tt.body)\n\n\t\t\tif tt.errMsg != \"\" {\n\t\t\t\turequire.Error(t, err, \"expect an error\")\n\t\t\t\turequire.ErrorContains(t, err, tt.errMsg, \"expect error to match\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.NoError(t, err, \"expect no error\")\n\t\t\turequire.True(t, parent.Board.ThreadsSequence.Last() == reply.ID, \"expect ID to match\")\n\t\t\turequire.True(t, parent.ID == reply.ParentID, \"expect parent ID to match\")\n\t\t\turequire.True(t, parent.ThreadID == reply.ThreadID, \"expect thread ID to match\")\n\t\t\turequire.False(t, reply.Board == nil, \"expect board to be assigned\")\n\t\t\turequire.True(t, parent.Board.ID == reply.Board.ID, \"expect board ID to match\")\n\t\t\turequire.Equal(t, tt.body, reply.Body, \"expect body to match\")\n\t\t\turequire.True(t, reply.Replies != nil, \"expect reply to support sub-replies\")\n\t\t\turequire.True(t, reply.Flags != nil, \"expect reply to support flagging\")\n\t\t\turequire.Equal(t, tt.creator, reply.Creator, \"expect creator to match\")\n\t\t\turequire.False(t, reply.CreatedAt.IsZero(), \"expect creation date to be assigned\")\n\t\t})\n\t}\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":"repost_storage_test.gno","body":"package boards_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/urequire/v0\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n)\n\nfunc TestRepostStorageGet(t *testing.T) {\n\ttests := []struct {\n\t\tname              string\n\t\tsetup             func() boards.RepostStorage\n\t\tboardID, repostID boards.ID\n\t\tfound             bool\n\t}{\n\t\t{\n\t\t\tname: \"single repost\",\n\t\t\tsetup: func() boards.RepostStorage {\n\t\t\t\ts := boards.NewRepostStorage()\n\t\t\t\ts.Add(\u0026boards.Post{\n\t\t\t\t\tID:    1,\n\t\t\t\t\tBoard: \u0026boards.Board{ID: 1},\n\t\t\t\t})\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tboardID:  1,\n\t\t\trepostID: 1,\n\t\t\tfound:    true,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple reposts\",\n\t\t\tsetup: func() boards.RepostStorage {\n\t\t\t\ts := boards.NewRepostStorage()\n\t\t\t\ts.Add(\u0026boards.Post{\n\t\t\t\t\tID:    2,\n\t\t\t\t\tBoard: \u0026boards.Board{ID: 1},\n\t\t\t\t})\n\t\t\t\ts.Add(\u0026boards.Post{\n\t\t\t\t\tID:    5,\n\t\t\t\t\tBoard: \u0026boards.Board{ID: 2},\n\t\t\t\t})\n\t\t\t\ts.Add(\u0026boards.Post{\n\t\t\t\t\tID:    10,\n\t\t\t\t\tBoard: \u0026boards.Board{ID: 3},\n\t\t\t\t})\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tboardID:  1,\n\t\t\trepostID: 2,\n\t\t\tfound:    true,\n\t\t},\n\t\t{\n\t\t\tname: \"not found\",\n\t\t\tsetup: func() boards.RepostStorage {\n\t\t\t\treturn boards.NewRepostStorage()\n\t\t\t},\n\t\t\tboardID: 404,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := tt.setup()\n\n\t\t\trepostID, found := s.Get(tt.boardID)\n\n\t\t\tif !tt.found {\n\t\t\t\turequire.False(t, found, \"expect repost not to be found\")\n\t\t\t\turequire.True(t, int(repostID) == 0, \"expect repost ID to be 0\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.True(t, found, \"expect post to be found\")\n\t\t\turequire.Equal(t, tt.repostID.String(), repostID.String(), \"expect repost ID to match\")\n\t\t})\n\t}\n}\n\nfunc TestRepostStorageAdd(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\trepost *boards.Post\n\t\terrMsg string\n\t}{\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\trepost: \u0026boards.Post{\n\t\t\t\tID:    1,\n\t\t\t\tBoard: \u0026boards.Board{ID: 1},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"nil repost\",\n\t\t\trepost: nil,\n\t\t\terrMsg: \"saving nil reposts is not allowed\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := boards.NewRepostStorage()\n\n\t\t\terr := s.Add(tt.repost)\n\n\t\t\tif tt.errMsg != \"\" {\n\t\t\t\turequire.Error(t, err, \"expect an error\")\n\t\t\t\turequire.ErrorContains(t, err, tt.errMsg, \"expect error to match\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.NoError(t, err, \"expect no error\")\n\n\t\t\t_, found := s.Get(tt.repost.ID)\n\t\t\turequire.True(t, found, \"expect repost to be found\")\n\t\t})\n\t}\n}\n\nfunc TestRepostStorageRemove(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tsetup   func() boards.RepostStorage\n\t\tboardID boards.ID\n\t\tremoved bool\n\t}{\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\tsetup: func() boards.RepostStorage {\n\t\t\t\ts := boards.NewRepostStorage()\n\t\t\t\ts.Add(\u0026boards.Post{\n\t\t\t\t\tID:    100,\n\t\t\t\t\tBoard: \u0026boards.Board{ID: 1},\n\t\t\t\t})\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tboardID: 1,\n\t\t\tremoved: true,\n\t\t},\n\t\t{\n\t\t\tname: \"not found\",\n\t\t\tsetup: func() boards.RepostStorage {\n\t\t\t\treturn boards.NewRepostStorage()\n\t\t\t},\n\t\t\tboardID: 404,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := tt.setup()\n\n\t\t\tremoved := s.Remove(tt.boardID)\n\n\t\t\tif !tt.removed {\n\t\t\t\turequire.False(t, removed, \"expect repost not to be removed\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.True(t, removed, \"expect repost to be removed\")\n\n\t\t\t_, found := s.Get(tt.boardID)\n\t\t\turequire.False(t, found, \"expect repost not to be found\")\n\t\t})\n\t}\n}\n\nfunc TestRepostStorageSize(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\tsetup func() boards.RepostStorage\n\t\tsize  int\n\t}{\n\t\t{\n\t\t\tname: \"empty\",\n\t\t\tsetup: func() boards.RepostStorage {\n\t\t\t\treturn boards.NewRepostStorage()\n\t\t\t},\n\t\t\tsize: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"one repost\",\n\t\t\tsetup: func() boards.RepostStorage {\n\t\t\t\ts := boards.NewRepostStorage()\n\t\t\t\ts.Add(\u0026boards.Post{\n\t\t\t\t\tID:    1,\n\t\t\t\t\tBoard: \u0026boards.Board{ID: 1},\n\t\t\t\t})\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tsize: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple reposts\",\n\t\t\tsetup: func() boards.RepostStorage {\n\t\t\t\ts := boards.NewRepostStorage()\n\t\t\t\ts.Add(\u0026boards.Post{\n\t\t\t\t\tID:    1,\n\t\t\t\t\tBoard: \u0026boards.Board{ID: 1},\n\t\t\t\t})\n\t\t\t\ts.Add(\u0026boards.Post{\n\t\t\t\t\tID:    1,\n\t\t\t\t\tBoard: \u0026boards.Board{ID: 2},\n\t\t\t\t})\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tsize: 2,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := tt.setup()\n\n\t\t\turequire.Equal(t, tt.size, s.Size())\n\t\t})\n\t}\n}\n\nfunc TestRepostStorageIterate(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tsetup   func() boards.RepostStorage\n\t\treverse bool\n\t\tids     [][2]boards.ID\n\t}{\n\t\t{\n\t\t\tname: \"default\",\n\t\t\tsetup: func() boards.RepostStorage {\n\t\t\t\ts := boards.NewRepostStorage()\n\t\t\t\ts.Add(\u0026boards.Post{\n\t\t\t\t\tID:    10,\n\t\t\t\t\tBoard: \u0026boards.Board{ID: 1},\n\t\t\t\t})\n\t\t\t\ts.Add(\u0026boards.Post{\n\t\t\t\t\tID:    20,\n\t\t\t\t\tBoard: \u0026boards.Board{ID: 2},\n\t\t\t\t})\n\t\t\t\ts.Add(\u0026boards.Post{\n\t\t\t\t\tID:    30,\n\t\t\t\t\tBoard: \u0026boards.Board{ID: 3},\n\t\t\t\t})\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tids: [][2]boards.ID{\n\t\t\t\t{1, 10},\n\t\t\t\t{2, 20},\n\t\t\t\t{3, 30},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"reverse\",\n\t\t\tsetup: func() boards.RepostStorage {\n\t\t\t\ts := boards.NewRepostStorage()\n\t\t\t\ts.Add(\u0026boards.Post{\n\t\t\t\t\tID:    10,\n\t\t\t\t\tBoard: \u0026boards.Board{ID: 1},\n\t\t\t\t})\n\t\t\t\ts.Add(\u0026boards.Post{\n\t\t\t\t\tID:    20,\n\t\t\t\t\tBoard: \u0026boards.Board{ID: 2},\n\t\t\t\t})\n\t\t\t\ts.Add(\u0026boards.Post{\n\t\t\t\t\tID:    30,\n\t\t\t\t\tBoard: \u0026boards.Board{ID: 3},\n\t\t\t\t})\n\t\t\t\treturn s\n\t\t\t},\n\t\t\treverse: true,\n\t\t\tids: [][2]boards.ID{\n\t\t\t\t{3, 30},\n\t\t\t\t{2, 20},\n\t\t\t\t{1, 10},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := tt.setup()\n\t\t\tcount := s.Size()\n\t\t\tif tt.reverse {\n\t\t\t\tcount = -count\n\t\t\t}\n\n\t\t\tvar i int\n\t\t\ts.Iterate(0, count, func(boardID, repostID boards.ID) bool {\n\t\t\t\turequire.True(t, tt.ids[i][0] == boardID, \"expect board ID to match\")\n\t\t\t\turequire.True(t, tt.ids[i][1] == repostID, \"expect repost ID to match\")\n\n\t\t\t\ti++\n\t\t\t\treturn false\n\t\t\t})\n\t\t})\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":"storage_test.gno","body":"package boards_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/urequire/v0\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n)\n\nfunc TestStorageGet(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tsetup   func() boards.Storage\n\t\tboardID boards.ID\n\t\tfound   bool\n\t}{\n\t\t{\n\t\t\tname: \"single board\",\n\t\t\tsetup: func() boards.Storage {\n\t\t\t\ts := boards.NewStorage()\n\t\t\t\ts.Add(\u0026boards.Board{ID: 1})\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tboardID: 1,\n\t\t\tfound:   true,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple boards\",\n\t\t\tsetup: func() boards.Storage {\n\t\t\t\ts := boards.NewStorage()\n\t\t\t\ts.Add(\u0026boards.Board{ID: 1})\n\t\t\t\ts.Add(\u0026boards.Board{ID: 2})\n\t\t\t\ts.Add(\u0026boards.Board{ID: 3})\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tboardID: 2,\n\t\t\tfound:   true,\n\t\t},\n\t\t{\n\t\t\tname: \"not found\",\n\t\t\tsetup: func() boards.Storage {\n\t\t\t\treturn boards.NewStorage()\n\t\t\t},\n\t\t\tboardID: 404,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := tt.setup()\n\n\t\t\tboard, found := s.Get(tt.boardID)\n\n\t\t\tif !tt.found {\n\t\t\t\turequire.False(t, found, \"expect board not to be found\")\n\t\t\t\turequire.True(t, board == nil, \"expect board to be nil\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.True(t, found, \"expect board to be found\")\n\t\t\turequire.False(t, board == nil, \"expect board not to be nil\")\n\t\t\turequire.Equal(t, tt.boardID.String(), board.ID.String(), \"expect board ID to match\")\n\t\t})\n\t}\n}\n\nfunc TestStorageGetByName(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tsetup     func() boards.Storage\n\t\tboardName string\n\t\tfound     bool\n\t}{\n\t\t{\n\t\t\tname: \"single board\",\n\t\t\tsetup: func() boards.Storage {\n\t\t\t\ts := boards.NewStorage()\n\t\t\t\ts.Add(\u0026boards.Board{ID: 1, Name: \"A\"})\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tboardName: \"A\",\n\t\t\tfound:     true,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple boards\",\n\t\t\tsetup: func() boards.Storage {\n\t\t\t\ts := boards.NewStorage()\n\t\t\t\ts.Add(\u0026boards.Board{ID: 1, Name: \"A\"})\n\t\t\t\ts.Add(\u0026boards.Board{ID: 2, Name: \"B\"})\n\t\t\t\ts.Add(\u0026boards.Board{ID: 3, Name: \"C\"})\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tboardName: \"B\",\n\t\t\tfound:     true,\n\t\t},\n\t\t{\n\t\t\tname: \"not found\",\n\t\t\tsetup: func() boards.Storage {\n\t\t\t\treturn boards.NewStorage()\n\t\t\t},\n\t\t\tboardName: \"foo\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := tt.setup()\n\n\t\t\tboard, found := s.GetByName(tt.boardName)\n\n\t\t\tif !tt.found {\n\t\t\t\turequire.False(t, found, \"expect board not to be found\")\n\t\t\t\turequire.True(t, board == nil, \"expect board to be nil\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.True(t, found, \"expect board to be found\")\n\t\t\turequire.False(t, board == nil, \"expect board not to be nil\")\n\t\t\turequire.Equal(t, tt.boardName, board.Name, \"expect board name to match\")\n\t\t})\n\t}\n}\n\nfunc TestStorageRemove(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tsetup      func() boards.Storage\n\t\tboardID    boards.ID\n\t\tboardNames []string\n\t\tremoved    bool\n\t}{\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\tsetup: func() boards.Storage {\n\t\t\t\ts := boards.NewStorage()\n\t\t\t\ts.Add(\u0026boards.Board{ID: 1, Name: \"A\"})\n\t\t\t\ts.Add(\u0026boards.Board{ID: 2, Name: \"B\"})\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tboardID:    2,\n\t\t\tboardNames: []string{\"B\"},\n\t\t\tremoved:    true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok with aliases\",\n\t\t\tsetup: func() boards.Storage {\n\t\t\t\ts := boards.NewStorage()\n\t\t\t\ts.Add(\u0026boards.Board{ID: 1, Name: \"A\"})\n\n\t\t\t\tb := \u0026boards.Board{ID: 2, Name: \"B\"}\n\t\t\t\ts.Add(b)\n\n\t\t\t\tb.Aliases = []string{\"A\"}\n\t\t\t\tb.Name = \"C\"\n\t\t\t\ts.Add(b)\n\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tboardID:    2,\n\t\t\tboardNames: []string{\"B\", \"C\"},\n\t\t\tremoved:    true,\n\t\t},\n\t\t{\n\t\t\tname: \"not found\",\n\t\t\tsetup: func() boards.Storage {\n\t\t\t\treturn boards.NewStorage()\n\t\t\t},\n\t\t\tboardID: 404,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := tt.setup()\n\n\t\t\tboard, removed := s.Remove(tt.boardID)\n\n\t\t\tif !tt.removed {\n\t\t\t\turequire.False(t, removed, \"expect board not to be removed\")\n\t\t\t\turequire.True(t, board == nil, \"expect board to be nil\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.True(t, removed, \"expect board to be removed\")\n\t\t\turequire.False(t, board == nil, \"expect board not to be nil\")\n\t\t\turequire.Equal(t, tt.boardID.String(), board.ID.String(), \"expect board ID to match\")\n\n\t\t\t_, found := s.Get(tt.boardID)\n\t\t\turequire.False(t, found, \"expect board not to be found by ID\")\n\n\t\t\tfor _, name := range tt.boardNames {\n\t\t\t\t_, found = s.GetByName(name)\n\t\t\t\turequire.False(t, found, \"expect board not to be found by name: \"+name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestStorageAdd(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tsetup  func() boards.Storage\n\t\tboard  *boards.Board\n\t\terrMsg string\n\t}{\n\t\t{\n\t\t\tname:  \"ok\",\n\t\t\tboard: \u0026boards.Board{ID: 1, Name: \"A\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"nil board\",\n\t\t\tboard:  nil,\n\t\t\terrMsg: \"adding nil boards to the storage is not allowed\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := boards.NewStorage()\n\n\t\t\terr := s.Add(tt.board)\n\n\t\t\tif tt.errMsg != \"\" {\n\t\t\t\turequire.Error(t, err, \"expect an error\")\n\t\t\t\turequire.ErrorContains(t, err, tt.errMsg, \"expect error to match\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.NoError(t, err, \"expect no error\")\n\n\t\t\t_, found := s.Get(tt.board.ID)\n\t\t\turequire.True(t, found, \"expect board to be found by ID\")\n\n\t\t\t_, found = s.GetByName(tt.board.Name)\n\t\t\turequire.True(t, found, \"expect board to be found by name\")\n\t\t})\n\t}\n}\n\nfunc TestStorageSize(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\tsetup func() boards.Storage\n\t\tsize  int\n\t}{\n\t\t{\n\t\t\tname: \"empty\",\n\t\t\tsetup: func() boards.Storage {\n\t\t\t\treturn boards.NewStorage()\n\t\t\t},\n\t\t\tsize: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"one board\",\n\t\t\tsetup: func() boards.Storage {\n\t\t\t\ts := boards.NewStorage()\n\t\t\t\ts.Add(\u0026boards.Board{ID: 1})\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tsize: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple boards\",\n\t\t\tsetup: func() boards.Storage {\n\t\t\t\ts := boards.NewStorage()\n\t\t\t\ts.Add(\u0026boards.Board{ID: 1})\n\t\t\t\ts.Add(\u0026boards.Board{ID: 2})\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tsize: 2,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := tt.setup()\n\n\t\t\turequire.Equal(t, tt.size, s.Size())\n\t\t})\n\t}\n}\n\nfunc TestStorageIterate(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tsetup   func() boards.Storage\n\t\treverse bool\n\t\tids     []boards.ID\n\t}{\n\t\t{\n\t\t\tname: \"default\",\n\t\t\tsetup: func() boards.Storage {\n\t\t\t\ts := boards.NewStorage()\n\t\t\t\ts.Add(\u0026boards.Board{ID: 1})\n\t\t\t\ts.Add(\u0026boards.Board{ID: 2})\n\t\t\t\ts.Add(\u0026boards.Board{ID: 3})\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tids: []boards.ID{1, 2, 3},\n\t\t},\n\t\t{\n\t\t\tname: \"reverse\",\n\t\t\tsetup: func() boards.Storage {\n\t\t\t\ts := boards.NewStorage()\n\t\t\t\ts.Add(\u0026boards.Board{ID: 1})\n\t\t\t\ts.Add(\u0026boards.Board{ID: 2})\n\t\t\t\ts.Add(\u0026boards.Board{ID: 3})\n\t\t\t\treturn s\n\t\t\t},\n\t\t\treverse: true,\n\t\t\tids:     []boards.ID{3, 2, 1},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := tt.setup()\n\t\t\tcount := s.Size()\n\t\t\tif tt.reverse {\n\t\t\t\tcount = -count\n\t\t\t}\n\n\t\t\tvar i int\n\t\t\ts.Iterate(0, count, func(p *boards.Board) bool {\n\t\t\t\turequire.True(t, tt.ids[i] == p.ID, \"expect board ID to match\")\n\n\t\t\t\ti++\n\t\t\t\treturn false\n\t\t\t})\n\t\t})\n\t}\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"},{"name":"thread_test.gno","body":"package boards_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/urequire/v0\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n)\n\nfunc TestNewThread(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tboard       *boards.Board\n\t\tcreator     address\n\t\ttitle, body string\n\t\terrMsg      string\n\t}{\n\t\t{\n\t\t\tname:    \"ok\",\n\t\t\tboard:   boards.New(1),\n\t\t\tcreator: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\ttitle:   \"Test\",\n\t\t\tbody:    \"Foo\",\n\t\t},\n\t\t{\n\t\t\tname:   \"nil board\",\n\t\t\tboard:  nil,\n\t\t\terrMsg: \"thread requires a parent board\",\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid creator\",\n\t\t\tboard:   boards.New(1),\n\t\t\tcreator: \"foo\",\n\t\t\terrMsg:  \"invalid thread creator address: foo\",\n\t\t},\n\t\t{\n\t\t\tname:    \"empty title\",\n\t\t\tboard:   boards.New(1),\n\t\t\tcreator: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\ttitle:   \"\",\n\t\t\terrMsg:  \"thread title is required\",\n\t\t},\n\t\t{\n\t\t\tname:    \"empty body\",\n\t\t\tboard:   boards.New(1),\n\t\t\tcreator: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\ttitle:   \"Test\",\n\t\t\tbody:    \"\",\n\t\t\terrMsg:  \"thread body is required\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tthread, err := boards.NewThread(tt.board, tt.creator, tt.title, tt.body)\n\n\t\t\tif tt.errMsg != \"\" {\n\t\t\t\turequire.Error(t, err, \"expect an error\")\n\t\t\t\turequire.ErrorContains(t, err, tt.errMsg, \"expect error to match\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.NoError(t, err, \"expect no error\")\n\t\t\turequire.True(t, tt.board.ThreadsSequence.Last() == thread.ID, \"expect ID to match\")\n\t\t\turequire.True(t, thread.ThreadID == thread.ID, \"expect thread ID to match\")\n\t\t\turequire.False(t, thread.Board == nil, \"expect board to be assigned\")\n\t\t\turequire.True(t, tt.board.ID == thread.Board.ID, \"expect board ID to match\")\n\t\t\turequire.Equal(t, tt.title, thread.Title, \"expect title to match\")\n\t\t\turequire.Equal(t, tt.body, thread.Body, \"expect body to match\")\n\t\t\turequire.True(t, thread.Replies != nil, \"expect thread to support sub-replies\")\n\t\t\turequire.True(t, thread.Reposts != nil, \"expect thread to support reposts\")\n\t\t\turequire.True(t, thread.Flags != nil, \"expect thread to support flagging\")\n\t\t\turequire.Equal(t, tt.creator, thread.Creator, \"expect creator to match\")\n\t\t\turequire.False(t, thread.CreatedAt.IsZero(), \"expect creation date to be assigned\")\n\t\t})\n\t}\n}\n\nfunc TestNewRepost(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\torigThread *boards.Post\n\t\tdstBoard   *boards.Board\n\t\tcreator    address\n\t\terrMsg     string\n\t}{\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\torigThread: boards.MustNewThread(\n\t\t\t\tboards.New(1),\n\t\t\t\t\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\"Title\",\n\t\t\t\t\"Body\",\n\t\t\t),\n\t\t\tdstBoard: boards.New(2),\n\t\t\tcreator:  \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t},\n\t\t{\n\t\t\tname:       \"nil original thread\",\n\t\t\torigThread: nil,\n\t\t\terrMsg:     \"thread to repost is required\",\n\t\t},\n\t\t{\n\t\t\tname:       \"original thread without board\",\n\t\t\torigThread: \u0026boards.Post{ID: 1},\n\t\t\terrMsg:     \"original thread has no board assigned\",\n\t\t},\n\t\t{\n\t\t\tname: \"nil destination board\",\n\t\t\torigThread: boards.MustNewThread(\n\t\t\t\tboards.New(1),\n\t\t\t\t\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\"Title\",\n\t\t\t\t\"Body\",\n\t\t\t),\n\t\t\tdstBoard: nil,\n\t\t\terrMsg:   \"thread repost requires a destination board\",\n\t\t},\n\t\t{\n\t\t\tname: \"original thread is not a thread\",\n\t\t\torigThread: \u0026boards.Post{\n\t\t\t\tID:       1,\n\t\t\t\tThreadID: 2,\n\t\t\t\tBoard:    boards.New(1),\n\t\t\t},\n\t\t\tdstBoard: boards.New(2),\n\t\t\terrMsg:   \"post must be a thread to be reposted to another board\",\n\t\t},\n\t\t{\n\t\t\tname: \"original thread is a repost\",\n\t\t\torigThread: \u0026boards.Post{\n\t\t\t\tID:              1,\n\t\t\t\tThreadID:        1,\n\t\t\t\tOriginalBoardID: 1,\n\t\t\t\tBoard:           boards.New(1),\n\t\t\t},\n\t\t\tdstBoard: boards.New(2),\n\t\t\terrMsg:   \"reposting a thread that is a repost is not allowed\",\n\t\t},\n\t\t{\n\t\t\tname: \"invalid creator\",\n\t\t\torigThread: boards.MustNewThread(\n\t\t\t\tboards.New(1),\n\t\t\t\t\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\"Title\",\n\t\t\t\t\"Body\",\n\t\t\t),\n\t\t\tdstBoard: boards.New(2),\n\t\t\tcreator:  \"foo\",\n\t\t\terrMsg:   \"invalid thread repost creator address: foo\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tthread, err := boards.NewRepost(tt.origThread, tt.dstBoard, tt.creator)\n\n\t\t\tif tt.errMsg != \"\" {\n\t\t\t\turequire.Error(t, err, \"expect an error\")\n\t\t\t\turequire.ErrorContains(t, err, tt.errMsg, \"expect error to match\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.NoError(t, err, \"expect no error\")\n\t\t\turequire.True(t, thread.ID == tt.dstBoard.ThreadsSequence.Last(), \"expect ID to match\")\n\t\t\turequire.True(t, thread.ThreadID == thread.ID, \"expect thread ID to match\")\n\t\t\turequire.True(t, thread.ParentID == tt.origThread.ID, \"expect parent ID to match\")\n\t\t\turequire.True(t, thread.OriginalBoardID == tt.origThread.Board.ID, \"expect original board ID to match\")\n\t\t\turequire.False(t, thread.Board == nil, \"expect board to be assigned\")\n\t\t\turequire.True(t, thread.Board.ID == tt.dstBoard.ID, \"expect board ID to match\")\n\t\t\turequire.Empty(t, thread.Title, \"expect title to be empty\")\n\t\t\turequire.Empty(t, thread.Body, \"expect body to be empty\")\n\t\t\turequire.True(t, thread.Replies != nil, \"expect thread to support sub-replies\")\n\t\t\turequire.True(t, thread.Reposts != nil, \"expect thread to support reposts\")\n\t\t\turequire.True(t, thread.Flags != nil, \"expect thread to support flagging\")\n\t\t\turequire.Equal(t, tt.creator, thread.Creator, \"expect creator to match\")\n\t\t\turequire.False(t, thread.CreatedAt.IsZero(), \"expect creation date to be assigned\")\n\t\t})\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"pnA8SaB6bIt9otrXuyntXswSEgqn4qmo34PitsGJ0rgxDKZF303NGInwSkpcPGAT2iJAcgT0X4Z22L51Dsil5g=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","package":{"name":"addrset","path":"gno.land/p/moul/addrset/v0","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/v0\"\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":"addrset_test.gno","body":"package addrset\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nfunc TestSet(t *testing.T) {\n\taddr1 := address(\"addr1\")\n\taddr2 := address(\"addr2\")\n\taddr3 := address(\"addr3\")\n\n\ttests := []struct {\n\t\tname    string\n\t\tactions func(s *Set)\n\t\tsize    int\n\t\thas     map[address]bool\n\t\taddrs   []address // for iteration checks\n\t}{\n\t\t{\n\t\t\tname:    \"empty set\",\n\t\t\tactions: func(s *Set) {},\n\t\t\tsize:    0,\n\t\t\thas:     map[address]bool{addr1: false},\n\t\t},\n\t\t{\n\t\t\tname: \"single address\",\n\t\t\tactions: func(s *Set) {\n\t\t\t\ts.Add(addr1)\n\t\t\t},\n\t\t\tsize: 1,\n\t\t\thas: map[address]bool{\n\t\t\t\taddr1: true,\n\t\t\t\taddr2: false,\n\t\t\t},\n\t\t\taddrs: []address{addr1},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple addresses\",\n\t\t\tactions: func(s *Set) {\n\t\t\t\ts.Add(addr1)\n\t\t\t\ts.Add(addr2)\n\t\t\t\ts.Add(addr3)\n\t\t\t},\n\t\t\tsize: 3,\n\t\t\thas: map[address]bool{\n\t\t\t\taddr1: true,\n\t\t\t\taddr2: true,\n\t\t\t\taddr3: true,\n\t\t\t},\n\t\t\taddrs: []address{addr1, addr2, addr3},\n\t\t},\n\t\t{\n\t\t\tname: \"remove address\",\n\t\t\tactions: func(s *Set) {\n\t\t\t\ts.Add(addr1)\n\t\t\t\ts.Add(addr2)\n\t\t\t\ts.Remove(addr1)\n\t\t\t},\n\t\t\tsize: 1,\n\t\t\thas: map[address]bool{\n\t\t\t\taddr1: false,\n\t\t\t\taddr2: true,\n\t\t\t},\n\t\t\taddrs: []address{addr2},\n\t\t},\n\t\t{\n\t\t\tname: \"duplicate adds\",\n\t\t\tactions: func(s *Set) {\n\t\t\t\tuassert.True(t, s.Add(addr1))     // first add returns true\n\t\t\t\tuassert.False(t, s.Add(addr1))    // second add returns false\n\t\t\t\tuassert.True(t, s.Remove(addr1))  // remove existing returns true\n\t\t\t\tuassert.False(t, s.Remove(addr1)) // remove non-existing returns false\n\t\t\t},\n\t\t\tsize: 0,\n\t\t\thas: map[address]bool{\n\t\t\t\taddr1: false,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar set Set\n\n\t\t\t// Execute test actions\n\t\t\ttt.actions(\u0026set)\n\n\t\t\t// Check size\n\t\t\tuassert.Equal(t, tt.size, set.Size())\n\n\t\t\t// Check existence\n\t\t\tfor addr, expected := range tt.has {\n\t\t\t\tuassert.Equal(t, expected, set.Has(addr))\n\t\t\t}\n\n\t\t\t// Check iteration if addresses are specified\n\t\t\tif tt.addrs != nil {\n\t\t\t\tcollected := []address{}\n\t\t\t\tset.IterateByOffset(0, 10, func(addr address) bool {\n\t\t\t\t\tcollected = append(collected, addr)\n\t\t\t\t\treturn false\n\t\t\t\t})\n\n\t\t\t\t// Check length\n\t\t\t\tuassert.Equal(t, len(tt.addrs), len(collected))\n\n\t\t\t\t// Check each address\n\t\t\t\tfor i, addr := range tt.addrs {\n\t\t\t\t\tuassert.Equal(t, addr, collected[i])\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetIterationLimits(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\taddrs    []address\n\t\toffset   int\n\t\tlimit    int\n\t\texpected int\n\t}{\n\t\t{\n\t\t\tname:     \"zero offset full list\",\n\t\t\taddrs:    []address{\"a1\", \"a2\", \"a3\"},\n\t\t\toffset:   0,\n\t\t\tlimit:    10,\n\t\t\texpected: 3,\n\t\t},\n\t\t{\n\t\t\tname:     \"offset with limit\",\n\t\t\taddrs:    []address{\"a1\", \"a2\", \"a3\", \"a4\"},\n\t\t\toffset:   1,\n\t\t\tlimit:    2,\n\t\t\texpected: 2,\n\t\t},\n\t\t{\n\t\t\tname:     \"offset beyond size\",\n\t\t\taddrs:    []address{\"a1\", \"a2\"},\n\t\t\toffset:   3,\n\t\t\tlimit:    1,\n\t\t\texpected: 0,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar set Set\n\t\t\tfor _, addr := range tt.addrs {\n\t\t\t\tset.Add(addr)\n\t\t\t}\n\n\t\t\t// Test forward iteration\n\t\t\tcount := 0\n\t\t\tset.IterateByOffset(tt.offset, tt.limit, func(addr address) bool {\n\t\t\t\tcount++\n\t\t\t\treturn false\n\t\t\t})\n\t\t\tuassert.Equal(t, tt.expected, count)\n\n\t\t\t// Test reverse iteration\n\t\t\tcount = 0\n\t\t\tset.ReverseIterateByOffset(tt.offset, tt.limit, func(addr address) bool {\n\t\t\t\tcount++\n\t\t\t\treturn false\n\t\t\t})\n\t\t\tuassert.Equal(t, tt.expected, count)\n\t\t})\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/addrset/v0\"\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"},{"name":"readonly_test.gno","body":"package addrset\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nfunc TestReadonlySet(t *testing.T) {\n\taddr1 := address(\"addr1\")\n\taddr2 := address(\"addr2\")\n\taddr3 := address(\"addr3\")\n\n\tvar set Set\n\tset.Add(addr1)\n\tset.Add(addr2)\n\n\tro := set.Readonly()\n\tuassert.Equal(t, 2, ro.Size())\n\tuassert.True(t, ro.Has(addr1))\n\tuassert.True(t, ro.Has(addr2))\n\tuassert.False(t, ro.Has(addr3))\n\n\t// NewReadonlySet is equivalent to Set.Readonly.\n\tro2 := NewReadonlySet(\u0026set)\n\tuassert.Equal(t, 2, ro2.Size())\n\tuassert.True(t, ro2.Has(addr1))\n\n\t// The view is a live handle, not a snapshot.\n\tset.Add(addr3)\n\tuassert.Equal(t, 3, ro.Size())\n\tuassert.True(t, ro.Has(addr3))\n\tset.Remove(addr1)\n\tuassert.False(t, ro.Has(addr1))\n}\n\nfunc TestReadonlySetIterateByOffset(t *testing.T) {\n\tvar set Set\n\tset.Add(address(\"addr2\"))\n\tset.Add(address(\"addr1\"))\n\tset.Add(address(\"addr3\"))\n\tro := set.Readonly()\n\n\tvar got []address\n\tstopped := ro.IterateByOffset(0, 10, func(a address) bool {\n\t\tgot = append(got, a)\n\t\treturn false\n\t})\n\tuassert.False(t, stopped)\n\tuassert.Equal(t, 3, len(got))\n\tuassert.Equal(t, address(\"addr1\"), got[0]) // sorted order\n\tuassert.Equal(t, address(\"addr2\"), got[1])\n\tuassert.Equal(t, address(\"addr3\"), got[2])\n\n\t// offset/count window.\n\tgot = nil\n\tro.IterateByOffset(1, 1, func(a address) bool {\n\t\tgot = append(got, a)\n\t\treturn false\n\t})\n\tuassert.Equal(t, 1, len(got))\n\tuassert.Equal(t, address(\"addr2\"), got[0])\n\n\t// Early stop is reported, and iteration actually halts.\n\tcount := 0\n\tstopped = ro.IterateByOffset(0, 10, func(a address) bool {\n\t\tcount++\n\t\treturn true\n\t})\n\tuassert.True(t, stopped)\n\tuassert.Equal(t, 1, count)\n\n\t// Empty set: callback never runs, not stopped.\n\tvar empty Set\n\tstopped = empty.Readonly().IterateByOffset(0, 10, func(a address) bool { return true })\n\tuassert.False(t, stopped)\n}\n\nfunc TestReadonlySetReverseIterateByOffset(t *testing.T) {\n\tvar set Set\n\tset.Add(address(\"addr1\"))\n\tset.Add(address(\"addr2\"))\n\tset.Add(address(\"addr3\"))\n\tro := set.Readonly()\n\n\tvar got []address\n\tstopped := ro.ReverseIterateByOffset(0, 10, func(a address) bool {\n\t\tgot = append(got, a)\n\t\treturn false\n\t})\n\tuassert.False(t, stopped)\n\tuassert.Equal(t, 3, len(got))\n\tuassert.Equal(t, address(\"addr3\"), got[0]) // descending order\n\tuassert.Equal(t, address(\"addr2\"), got[1])\n\tuassert.Equal(t, address(\"addr1\"), got[2])\n\n\t// offset/count window (offset from the end).\n\tgot = nil\n\tro.ReverseIterateByOffset(1, 1, func(a address) bool {\n\t\tgot = append(got, a)\n\t\treturn false\n\t})\n\tuassert.Equal(t, 1, len(got))\n\tuassert.Equal(t, address(\"addr2\"), got[0])\n\n\t// Early stop is reported, and iteration actually halts.\n\tcount := 0\n\tstopped = ro.ReverseIterateByOffset(0, 10, func(a address) bool {\n\t\tcount++\n\t\treturn true\n\t})\n\tuassert.True(t, stopped)\n\tuassert.Equal(t, 1, count)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"ux/VWkEXBup9N76EF4Bi3feWnx3G4akyf0tNH7yXeA8rmCs5Wn5DyfJugvAVmwydpr8sUWMfwEiBQGA+uNWtGw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g15ge0ae9077eh40erwrn2eq0xw6wupwqthpv34l","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/v0\"\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":"groups_test.gno","body":"package groups\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nvar (\n\taddr0 = address(\"addr0\")\n\taddr1 = address(\"addr1\")\n\taddr2 = address(\"addr2\")\n\taddr3 = address(\"addr3\")\n\taddr4 = address(\"addr4\")\n\taddr5 = address(\"addr5\")\n\taddr6 = address(\"addr6\")\n)\n\n// newTestGroup builds the fixture used across tests:\n//\n//\tbase:   addr0, addr1, addr3, addr5\n//\tadmins: addr2, addr3, addr5\n//\tmods:   addr1, addr4, addr5\n//\n// Overlap topology: addr0 base-only, addr2 admins-only, addr4 mods-only,\n// addr1 base+mods, addr3 base+admins, addr5 base+both roles.\n// Distinct addresses: addr0..addr5 (addr6 is never a member).\nfunc newTestGroup(t *testing.T) *Group {\n\tt.Helper()\n\tg := NewGroup()\n\tg.Add(addr0)\n\tg.Add(addr1)\n\tg.Add(addr3)\n\tg.Add(addr5)\n\n\tadmins, err := g.AddRole(\"admins\")\n\turequire.NoError(t, err)\n\tadmins.Members().Add(addr2)\n\tadmins.Members().Add(addr3)\n\tadmins.Members().Add(addr5)\n\n\tmods, err := g.AddRole(\"mods\")\n\turequire.NoError(t, err)\n\tmods.Members().Add(addr1)\n\tmods.Members().Add(addr4)\n\tmods.Members().Add(addr5)\n\treturn g\n}\n\nfunc joinAddrs(addrs []address) string {\n\tstrs := make([]string, 0, len(addrs))\n\tfor _, a := range addrs {\n\t\tstrs = append(strs, string(a))\n\t}\n\treturn strings.Join(strs, \",\")\n}\n\nfunc TestBaseSet(t *testing.T) {\n\tg := NewGroup()\n\tuassert.Equal(t, 0, g.Size())\n\tuassert.False(t, g.Has(addr1))\n\n\tuassert.True(t, g.Add(addr1))\n\tuassert.False(t, g.Add(addr1)) // duplicate\n\tuassert.True(t, g.Add(addr2))\n\tuassert.Equal(t, 2, g.Size())\n\tuassert.True(t, g.Has(addr1))\n\tuassert.False(t, g.Has(addr3))\n\n\tuassert.True(t, g.Remove(addr1))\n\tuassert.False(t, g.Remove(addr1)) // already gone\n\tuassert.Equal(t, 1, g.Size())\n\tuassert.False(t, g.Has(addr1))\n}\n\nfunc TestIterate(t *testing.T) {\n\tg := NewGroup()\n\tg.Add(addr2)\n\tg.Add(addr1)\n\tg.Add(addr3)\n\n\tvar got []address\n\tstopped := g.Iterate(0, 10, func(a address) bool {\n\t\tgot = append(got, a)\n\t\treturn false\n\t})\n\tuassert.False(t, stopped)\n\tuassert.Equal(t, \"addr1,addr2,addr3\", joinAddrs(got)) // sorted order\n\n\tgot = nil\n\tg.Iterate(1, 1, func(a address) bool {\n\t\tgot = append(got, a)\n\t\treturn false\n\t})\n\tuassert.Equal(t, \"addr2\", joinAddrs(got))\n\n\t// Early stop halts iteration, not just the return value.\n\tvisited := 0\n\tstopped = g.Iterate(0, 10, func(a address) bool {\n\t\tvisited++\n\t\treturn true\n\t})\n\tuassert.True(t, stopped)\n\tuassert.Equal(t, 1, visited)\n\n\t// Roles are never consulted.\n\tgg := newTestGroup(t)\n\tgot = nil\n\tgg.Iterate(0, 10, func(a address) bool {\n\t\tgot = append(got, a)\n\t\treturn false\n\t})\n\tuassert.Equal(t, \"addr0,addr1,addr3,addr5\", joinAddrs(got))\n}\n\nfunc TestAddRole(t *testing.T) {\n\tg := NewGroup()\n\n\tr, err := g.AddRole(\"admins\")\n\turequire.NoError(t, err)\n\tuassert.Equal(t, \"admins\", r.Name())\n\tuassert.Equal(t, 0, r.Members().Size())\n\tuassert.Equal(t, 1, g.RoleCount())\n\n\t_, err = g.AddRole(\"admins\")\n\tuassert.ErrorIs(t, err, ErrRoleExists)\n\n\t_, err = g.AddRole(\"\")\n\tuassert.ErrorIs(t, err, ErrEmptyName)\n\n\tuassert.Equal(t, 1, g.RoleCount())\n}\n\nfunc TestGetRoleHasRole(t *testing.T) {\n\tg := newTestGroup(t)\n\n\tr, found := g.GetRole(\"admins\")\n\tuassert.True(t, found)\n\tuassert.Equal(t, \"admins\", r.Name())\n\tuassert.True(t, r.Members().Has(addr2))\n\n\t_, found = g.GetRole(\"nope\")\n\tuassert.False(t, found)\n\n\tuassert.True(t, g.HasRole(\"mods\"))\n\tuassert.False(t, g.HasRole(\"nope\"))\n}\n\nfunc TestRemoveRole(t *testing.T) {\n\tg := newTestGroup(t)\n\n\tuassert.True(t, g.RemoveRole(\"admins\"))\n\tuassert.False(t, g.RemoveRole(\"admins\")) // already gone\n\tuassert.Equal(t, 1, g.RoleCount())\n\tuassert.False(t, g.HasRole(\"admins\"))\n\n\t// Members of the removed role stay in the base set and other roles.\n\tuassert.True(t, g.Has(addr3))     // base\n\tuassert.True(t, g.HasAny(addr5))  // still in base + mods\n\tuassert.False(t, g.HasAny(addr2)) // was only in admins\n}\n\nfunc TestIterateRoles(t *testing.T) {\n\tg := newTestGroup(t)\n\n\tvar names []string\n\tstopped := g.IterateRoles(0, 10, func(rr *ReadonlyRole) bool {\n\t\tnames = append(names, rr.Name())\n\t\treturn false\n\t})\n\tuassert.False(t, stopped)\n\tuassert.Equal(t, \"admins,mods\", strings.Join(names, \",\"))\n\n\t// offset/count window.\n\tnames = nil\n\tg.IterateRoles(1, 1, func(rr *ReadonlyRole) bool {\n\t\tnames = append(names, rr.Name())\n\t\treturn false\n\t})\n\tuassert.Equal(t, \"mods\", strings.Join(names, \",\"))\n\n\t// Early stop halts iteration, not just the return value.\n\tnames = nil\n\tstopped = g.IterateRoles(0, 10, func(rr *ReadonlyRole) bool {\n\t\tnames = append(names, rr.Name())\n\t\treturn true\n\t})\n\tuassert.True(t, stopped)\n\tuassert.Equal(t, \"admins\", strings.Join(names, \",\"))\n\n\t// Mutation path: capture names during iteration, revisit via GetRole.\n\tnames = nil\n\tg.IterateRoles(0, 10, func(rr *ReadonlyRole) bool {\n\t\tnames = append(names, rr.Name())\n\t\treturn false\n\t})\n\tfor _, name := range names {\n\t\tr, found := g.GetRole(name)\n\t\turequire.True(t, found)\n\t\tr.Members().Add(addr6)\n\t}\n\tuassert.Equal(t, \"admins,mods\", strings.Join(g.RolesContaining(addr6), \",\"))\n}\n\nfunc TestRoleMeta(t *testing.T) {\n\tg := NewGroup()\n\tr, err := g.AddRole(\"admins\")\n\turequire.NoError(t, err)\n\n\tuassert.Nil(t, r.Meta())\n\tr.SetMeta(\"quorum=2/3\")\n\tuassert.Equal(t, \"quorum=2/3\", r.Meta())\n\tuassert.Equal(t, \"quorum=2/3\", r.Readonly().Meta())\n\tr.SetMeta(nil)\n\tuassert.Nil(t, r.Meta())\n}\n\nfunc TestHasAny(t *testing.T) {\n\tg := newTestGroup(t)\n\n\tuassert.True(t, g.HasAny(addr0))  // base only\n\tuassert.True(t, g.HasAny(addr1))  // base + mods\n\tuassert.True(t, g.HasAny(addr2))  // admins only\n\tuassert.True(t, g.HasAny(addr4))  // mods only\n\tuassert.True(t, g.HasAny(addr5))  // base + both roles\n\tuassert.False(t, g.HasAny(addr6)) // nowhere\n\n\t// Base-only member, no roles registered at all.\n\tg2 := NewGroup()\n\tg2.Add(addr1)\n\tuassert.True(t, g2.HasAny(addr1))\n\tuassert.False(t, g2.HasAny(addr2))\n}\n\nfunc TestTotalSize(t *testing.T) {\n\tuassert.Equal(t, 0, NewGroup().TotalSize())\n\n\tg := newTestGroup(t)\n\tuassert.Equal(t, 4, g.Size())      // base only\n\tuassert.Equal(t, 6, g.TotalSize()) // addr0..addr5, deduplicated\n}\n\nfunc TestIterateAll(t *testing.T) {\n\tg := newTestGroup(t)\n\n\tcollect := func(offset, count int) (addrs []address, stopped bool) {\n\t\tstopped = g.IterateAll(offset, count, func(a address) bool {\n\t\t\taddrs = append(addrs, a)\n\t\t\treturn false\n\t\t})\n\t\treturn addrs, stopped\n\t}\n\n\t// Order: base first (sorted), then roles in name order, deduplicated.\n\tgot, stopped := collect(0, 10)\n\tuassert.False(t, stopped)\n\tuassert.Equal(t, \"addr0,addr1,addr3,addr5,addr2,addr4\", joinAddrs(got))\n\n\t// offset/count window over the deduplicated output.\n\tgot, _ = collect(1, 2)\n\tuassert.Equal(t, \"addr1,addr3\", joinAddrs(got))\n\tgot, _ = collect(3, 10)\n\tuassert.Equal(t, \"addr5,addr2,addr4\", joinAddrs(got))\n\t// Window straddling skipped duplicates: count applies to deduplicated\n\t// output, not to scanned items.\n\tgot, _ = collect(4, 2)\n\tuassert.Equal(t, \"addr2,addr4\", joinAddrs(got))\n\n\t// Degenerate parameters.\n\tgot, stopped = collect(6, 10) // offset == total\n\tuassert.False(t, stopped)\n\tuassert.Equal(t, 0, len(got))\n\tgot, _ = collect(0, 0)\n\tuassert.Equal(t, 0, len(got))\n\tgot, _ = collect(-1, 2) // negative offset counts as zero\n\tuassert.Equal(t, \"addr0,addr1\", joinAddrs(got))\n\n\t// Early stop halts iteration, not just the return value.\n\tvisited := 0\n\tstopped = g.IterateAll(0, 10, func(a address) bool {\n\t\tvisited++\n\t\treturn true\n\t})\n\tuassert.True(t, stopped)\n\tuassert.Equal(t, 1, visited)\n}\n\n// TestIterateAllWindows pins the pagination contract exhaustively: every\n// (offset, count) window over the deduplicated walk must equal the\n// corresponding slice of the full walk.\nfunc TestIterateAllWindows(t *testing.T) {\n\tg := newTestGroup(t)\n\n\tvar full []address\n\tg.IterateAll(0, 100, func(a address) bool {\n\t\tfull = append(full, a)\n\t\treturn false\n\t})\n\turequire.Equal(t, 6, len(full))\n\n\tfor offset := 0; offset \u003c= len(full); offset++ {\n\t\tfor count := 1; count \u003c= len(full)+1; count++ {\n\t\t\tvar page []address\n\t\t\tg.IterateAll(offset, count, func(a address) bool {\n\t\t\t\tpage = append(page, a)\n\t\t\t\treturn false\n\t\t\t})\n\t\t\tend := offset + count\n\t\t\tif end \u003e len(full) {\n\t\t\t\tend = len(full)\n\t\t\t}\n\t\t\tlabel := \"offset=\" + strconv.Itoa(offset) + \" count=\" + strconv.Itoa(count)\n\t\t\tuassert.Equal(t, joinAddrs(full[offset:end]), joinAddrs(page), label)\n\t\t}\n\t}\n}\n\nfunc TestRolesContaining(t *testing.T) {\n\tg := newTestGroup(t)\n\n\tuassert.Equal(t, \"admins,mods\", strings.Join(g.RolesContaining(addr5), \",\"))\n\tuassert.Equal(t, \"admins\", strings.Join(g.RolesContaining(addr2), \",\"))\n\tuassert.Equal(t, \"mods\", strings.Join(g.RolesContaining(addr1), \",\")) // base not consulted\n\tuassert.Equal(t, 0, len(g.RolesContaining(addr0)))                    // base-only member\n\tuassert.Equal(t, 0, len(g.RolesContaining(addr6)))\n}\n\nfunc TestRemoveFromAll(t *testing.T) {\n\tg := newTestGroup(t)\n\n\tuassert.True(t, g.RemoveFromAll(addr5)) // base + both roles\n\tuassert.False(t, g.HasAny(addr5))\n\n\tuassert.True(t, g.RemoveFromAll(addr0)) // base-only: removed reflects the base hit alone\n\tuassert.False(t, g.HasAny(addr0))\n\n\tuassert.True(t, g.RemoveFromAll(addr2)) // role-only\n\tuassert.False(t, g.HasAny(addr2))\n\n\tuassert.False(t, g.RemoveFromAll(addr6)) // nowhere\n\n\tuassert.Equal(t, 3, g.TotalSize()) // addr1, addr3, addr4 remain\n\tuassert.Equal(t, 2, g.RoleCount()) // roles themselves are kept\n}\n\n// TestRemoveRoleStaleHandle pins the aliasing semantics: a *Role handle\n// held across RemoveRole keeps working as a detached object, but its\n// mutations no longer affect the group, and re-adding the name creates a\n// fresh, independent role.\nfunc TestRemoveRoleStaleHandle(t *testing.T) {\n\tg := newTestGroup(t)\n\tstale, found := g.GetRole(\"admins\")\n\turequire.True(t, found)\n\n\tuassert.True(t, g.RemoveRole(\"admins\"))\n\n\t// The detached role still works as an orphan object...\n\tuassert.Equal(t, \"admins\", stale.Name())\n\tstale.Members().Add(addr6)\n\tuassert.True(t, stale.Members().Has(addr6))\n\n\t// ...but no longer affects the group.\n\tuassert.False(t, g.HasAny(addr6))\n\tuassert.Equal(t, 0, len(g.RolesContaining(addr6)))\n\tuassert.False(t, g.HasAny(addr2))  // addr2 was admins-only\n\tuassert.Equal(t, 5, g.TotalSize()) // fixture minus admins-only addr2\n\n\t// Re-adding the name yields a fresh empty role, not the stale one.\n\tfresh, err := g.AddRole(\"admins\")\n\turequire.NoError(t, err)\n\tuassert.Equal(t, 0, fresh.Members().Size())\n\tuassert.False(t, g.HasAny(addr6))\n}\n\nfunc TestReadonlyGroup(t *testing.T) {\n\tg := newTestGroup(t)\n\trg := g.Readonly()\n\n\t// Base reads.\n\tuassert.True(t, rg.Has(addr0))\n\tuassert.False(t, rg.Has(addr2))\n\tuassert.Equal(t, 4, rg.Size())\n\tvar got []address\n\tstopped := rg.Iterate(0, 10, func(a address) bool {\n\t\tgot = append(got, a)\n\t\treturn false\n\t})\n\tuassert.False(t, stopped)\n\tuassert.Equal(t, \"addr0,addr1,addr3,addr5\", joinAddrs(got))\n\n\t// Role registry reads.\n\tuassert.True(t, rg.HasRole(\"admins\"))\n\tuassert.False(t, rg.HasRole(\"nope\"))\n\tuassert.Equal(t, 2, rg.RoleCount())\n\tvar names []string\n\tstopped = rg.IterateRoles(0, 10, func(rr *ReadonlyRole) bool {\n\t\tnames = append(names, rr.Name())\n\t\treturn false\n\t})\n\tuassert.False(t, stopped)\n\tuassert.Equal(t, \"admins,mods\", strings.Join(names, \",\"))\n\n\trr, found := rg.GetRole(\"admins\")\n\tuassert.True(t, found)\n\tuassert.Equal(t, \"admins\", rr.Name())\n\tuassert.True(t, rr.Members().Has(addr2))\n\tuassert.Equal(t, 3, rr.Members().Size())\n\t_, found = rg.GetRole(\"nope\")\n\tuassert.False(t, found)\n\n\t// Aggregated reads.\n\tuassert.True(t, rg.HasAny(addr4))\n\tuassert.False(t, rg.HasAny(addr6))\n\tuassert.Equal(t, 6, rg.TotalSize())\n\tgot = nil\n\tstopped = rg.IterateAll(0, 10, func(a address) bool {\n\t\tgot = append(got, a)\n\t\treturn false\n\t})\n\tuassert.False(t, stopped)\n\tuassert.Equal(t, \"addr0,addr1,addr3,addr5,addr2,addr4\", joinAddrs(got))\n\tuassert.True(t, rg.IterateAll(0, 10, func(a address) bool { return true }))\n\tuassert.Equal(t, \"admins,mods\", strings.Join(rg.RolesContaining(addr5), \",\"))\n}\n\nfunc TestReadonlyViewsAreLive(t *testing.T) {\n\tg := newTestGroup(t)\n\trg := g.Readonly()\n\tadmins, found := g.GetRole(\"admins\")\n\turequire.True(t, found)\n\trr := admins.Readonly()\n\n\t// Views are thin handles, not snapshots: mutations through the owning\n\t// side are immediately visible through previously created views.\n\tg.Add(addr6)\n\tuassert.Equal(t, 5, rg.Size())\n\tuassert.True(t, rg.Has(addr6))\n\n\tadmins.Members().Add(addr6)\n\tuassert.Equal(t, 4, rr.Members().Size())\n\tuassert.True(t, rr.Members().Has(addr6))\n}\n"},{"name":"readonly.gno","body":"package groups\n\nimport \"gno.land/p/moul/addrset/v0\"\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/v0\"\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"},{"name":"z_readme_filetest.gno","body":"// End-to-end scenario from the package README: a group with a base set and\n// three roles, exercising base vs aggregated semantics, dedup, role\n// removal, and readonly views.\npackage main\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/nt/groups/v0\"\n)\n\nvar (\n\talice = address(\"alice\") // base only\n\tbob   = address(\"bob\")   // base + council\n\tcarol = address(\"carol\") // base + admin\n\tdave  = address(\"dave\")  // council only\n\teve   = address(\"eve\")   // admin + moderator\n\tfrank = address(\"frank\") // moderator only\n\tgrace = address(\"grace\") // admin only\n\tzed   = address(\"zed\")   // not a member\n)\n\nfunc main() {\n\t// 1. A group with a base set and three named roles.\n\tg := groups.NewGroup()\n\tg.Add(alice)\n\tg.Add(bob)\n\tg.Add(carol)\n\n\tadmin, _ := g.AddRole(\"admin\")\n\tadmin.Members().Add(carol)\n\tadmin.Members().Add(eve)\n\tadmin.Members().Add(grace)\n\n\tcouncil, _ := g.AddRole(\"council\")\n\tcouncil.Members().Add(bob)\n\tcouncil.Members().Add(dave)\n\n\tmoderator, _ := g.AddRole(\"moderator\")\n\tmoderator.Members().Add(eve)\n\tmoderator.Members().Add(frank)\n\n\t// 2. Has (base only) vs HasAny (base + roles).\n\tprintln(\"Has(alice):\", g.Has(alice))     // base member\n\tprintln(\"Has(dave):\", g.Has(dave))       // council only, not base\n\tprintln(\"HasAny(dave):\", g.HasAny(dave)) // somewhere in the group\n\tprintln(\"HasAny(zed):\", g.HasAny(zed))   // nowhere\n\n\t// 3. Size (base only) vs TotalSize (deduplicated across everything).\n\tprintln(\"Size:\", g.Size())\n\tprintln(\"TotalSize:\", g.TotalSize())\n\n\t// 4. Iterate walks the base set; IterateAll walks base then roles in\n\t// name order, yielding each distinct address once.\n\tprint(\"Iterate:\")\n\tg.Iterate(0, 10, func(a address) bool {\n\t\tprint(\" \" + string(a))\n\t\treturn false\n\t})\n\tprintln()\n\tprint(\"IterateAll:\")\n\tg.IterateAll(0, 10, func(a address) bool {\n\t\tprint(\" \" + string(a))\n\t\treturn false\n\t})\n\tprintln()\n\n\t// 5. RolesContaining lists role names in lexicographic order; the\n\t// base set is not a role.\n\tprintln(\"RolesContaining(eve):\", strings.Join(g.RolesContaining(eve), \",\"))\n\tprintln(\"RolesContaining(alice):\", strings.Join(g.RolesContaining(alice), \",\"))\n\n\t// 6. Removing a role discards only the role: its members survive in\n\t// the base set and in other roles.\n\tg.RemoveRole(\"admin\")\n\tprintln(\"after RemoveRole(admin):\")\n\tprintln(\"Has(carol):\", g.Has(carol))       // still a base member\n\tprintln(\"HasAny(eve):\", g.HasAny(eve))     // still a moderator\n\tprintln(\"HasAny(grace):\", g.HasAny(grace)) // was admin only, now gone\n\tprintln(\"TotalSize:\", g.TotalSize())\n\n\t// 7. A readonly view mirrors every read and exposes no mutators; it\n\t// is the only handle meant to cross a realm boundary.\n\trg := g.Readonly()\n\tprintln(\"readonly Size:\", rg.Size())\n\tprintln(\"readonly HasAny(dave):\", rg.HasAny(dave))\n\trr, _ := rg.GetRole(\"moderator\")\n\tprintln(\"readonly moderator members:\", rr.Members().Size())\n}\n\n// Output:\n// Has(alice): true\n// Has(dave): false\n// HasAny(dave): true\n// HasAny(zed): false\n// Size: 3\n// TotalSize: 7\n// Iterate: alice bob carol\n// IterateAll: alice bob carol eve grace dave frank\n// RolesContaining(eve): admin,moderator\n// RolesContaining(alice):\n// after RemoveRole(admin):\n// Has(carol): true\n// HasAny(eve): true\n// HasAny(grace): false\n// TotalSize: 6\n// readonly Size: 3\n// readonly HasAny(dave): true\n// readonly moderator members: 2\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"Ru1rJUR7puF+XnzOQ3ZwlUdObHsKudzz4As1v7U7LS9hvqLDbxgpW70Prp6pQhWwsINuqjz5dV8TJaYeXonU6g=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"permissions","path":"gno.land/p/gnoland/boards/exts/permissions/v0","files":[{"name":"README.md","body":"# Boards Permissions Extension\n\nThis is a `gno.land/p/gnoland/boards/v0` 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/v0\"\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":"example_test.gno","body":"package permissions\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\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"},{"name":"gnomod.toml","body":"module = \"gno.land/p/gnoland/boards/exts/permissions/v0\"\ngno = \"0.9\"\n"},{"name":"options.gno","body":"package permissions\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\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/v0\"\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/v0` 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"},{"name":"permissions_test.gno","body":"package permissions\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\n// Test permission constants\nconst (\n\ttestPermA boards.Permission = iota\n\ttestPermB\n\ttestPermC\n)\n\nvar _ boards.Permissions = (*Permissions)(nil)\n\nfunc TestBasicPermissionsWithPermission(cur realm, t *testing.T) {\n\tcases := []struct {\n\t\tname       string\n\t\tuser       address\n\t\tpermission boards.Permission\n\t\targs       boards.Args\n\t\tsetup      func() *Permissions\n\t\terr        string\n\t\tcalled     bool\n\t}{\n\t\t{\n\t\t\tname:       \"ok\",\n\t\t\tuser:       \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\tpermission: testPermA,\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New()\n\t\t\t\tperms.AddRole(\"foo\", testPermA)\n\t\t\t\tperms.SetUserRoles(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\", \"foo\")\n\t\t\t\treturn perms\n\t\t\t},\n\t\t\tcalled: true,\n\t\t},\n\t\t{\n\t\t\tname:       \"ok with arguments\",\n\t\t\tuser:       \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\tpermission: testPermA,\n\t\t\targs:       boards.Args{\"a\", \"b\"},\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New()\n\t\t\t\tperms.AddRole(\"foo\", testPermA)\n\t\t\t\tperms.SetUserRoles(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\", \"foo\")\n\t\t\t\treturn perms\n\t\t\t},\n\t\t\tcalled: true,\n\t\t},\n\t\t{\n\t\t\tname:       \"no permission\",\n\t\t\tuser:       \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\tpermission: testPermA,\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New()\n\t\t\t\tperms.AddRole(\"foo\", testPermA)\n\t\t\t\tperms.SetUserRoles(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\t\t\t\treturn perms\n\t\t\t},\n\t\t\terr: \"unauthorized, user g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 doesn't have the required permission\",\n\t\t},\n\t\t{\n\t\t\tname:       \"is not a member\",\n\t\t\tuser:       \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\tpermission: testPermA,\n\t\t\tsetup: func() *Permissions {\n\t\t\t\treturn New()\n\t\t\t},\n\t\t\terr: \"unauthorized, user g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 doesn't have the required permission\",\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar called bool\n\n\t\t\tperms := tc.setup()\n\t\t\ttestCaseFn := func() {\n\t\t\t\tperms.WithPermission(tc.user, tc.permission, tc.args, func() {\n\t\t\t\t\tcalled = true\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif tc.err != \"\" {\n\t\t\t\turequire.PanicsWithMessage(t, cur, tc.err, testCaseFn, \"expect panic with message\")\n\t\t\t\treturn\n\t\t\t} else {\n\t\t\t\turequire.NotPanics(t, cur, testCaseFn, \"expect no panic\")\n\t\t\t}\n\n\t\t\turequire.Equal(t, tc.called, called, \"expect callback to be called\")\n\t\t})\n\t}\n}\n\nfunc TestBasicPermissionsSetPublicPermissions(t *testing.T) {\n\tuser := address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\tperms := New()\n\n\t// Add a new role with permissions\n\tperms.AddRole(\"adminRole\", testPermA, testPermB, testPermC)\n\turequire.False(t, perms.HasPermission(user, testPermA))\n\turequire.False(t, perms.HasPermission(user, testPermB))\n\turequire.False(t, perms.HasPermission(user, testPermC))\n\n\t// Assign a couple of public permissions\n\tperms.SetPublicPermissions(testPermA, testPermC)\n\turequire.True(t, perms.HasPermission(user, testPermA))\n\turequire.False(t, perms.HasPermission(user, testPermB))\n\turequire.True(t, perms.HasPermission(user, testPermC))\n\n\t// Clear all public permissions\n\tperms.SetPublicPermissions()\n\turequire.False(t, perms.HasPermission(user, testPermA))\n\turequire.False(t, perms.HasPermission(user, testPermB))\n\turequire.False(t, perms.HasPermission(user, testPermC))\n}\n\nfunc TestBasicPermissionsGetUserRoles(t *testing.T) {\n\tcases := []struct {\n\t\tname  string\n\t\tuser  address\n\t\troles []string\n\t\tsetup func() *Permissions\n\t}{\n\t\t{\n\t\t\tname:  \"single role\",\n\t\t\tuser:  \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\troles: []string{\"admin\"},\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New()\n\t\t\t\tperms.AddRole(\"admin\", testPermA)\n\t\t\t\tperms.SetUserRoles(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\", \"admin\")\n\t\t\t\treturn perms\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"multiple roles\",\n\t\t\tuser:  \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\troles: []string{\"admin\", \"bar\", \"foo\"},\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New()\n\t\t\t\tperms.AddRole(\"admin\", testPermA)\n\t\t\t\tperms.AddRole(\"foo\", testPermA)\n\t\t\t\tperms.AddRole(\"bar\", testPermA)\n\t\t\t\tperms.SetUserRoles(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\", \"admin\", \"foo\", \"bar\")\n\t\t\t\treturn perms\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"without roles\",\n\t\t\tuser: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New()\n\t\t\t\tperms.SetUserRoles(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\t\t\t\treturn perms\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"not a user\",\n\t\t\tuser: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\tsetup: func() *Permissions {\n\t\t\t\treturn New()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"multiple users\",\n\t\t\tuser:  \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\troles: []string{\"admin\"},\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New()\n\t\t\t\tperms.AddRole(\"admin\", testPermA)\n\t\t\t\tperms.AddRole(\"bar\", testPermA)\n\t\t\t\tperms.SetUserRoles(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\", \"admin\")\n\t\t\t\tperms.SetUserRoles(\"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\", \"admin\")\n\t\t\t\tperms.SetUserRoles(\"g1w4ek2u3jta047h6lta047h6lta047h6l9huexc\", \"admin\", \"bar\")\n\t\t\t\treturn perms\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tperms := tc.setup()\n\t\t\troles := perms.GetUserRoles(tc.user)\n\n\t\t\turequire.Equal(t, len(tc.roles), len(roles), \"user role count\")\n\t\t\tfor i, r := range roles {\n\t\t\t\tuassert.Equal(t, tc.roles[i], string(r))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBasicPermissionsHasRole(t *testing.T) {\n\tcases := []struct {\n\t\tname  string\n\t\tuser  address\n\t\trole  boards.Role\n\t\tsetup func() *Permissions\n\t\twant  bool\n\t}{\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\tuser: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\trole: \"admin\",\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New()\n\t\t\t\tperms.AddRole(\"admin\", testPermA)\n\t\t\t\tperms.SetUserRoles(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\", \"admin\")\n\t\t\t\treturn perms\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok with multiple roles\",\n\t\t\tuser: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\trole: \"foo\",\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New()\n\t\t\t\tperms.AddRole(\"admin\", testPermA)\n\t\t\t\tperms.AddRole(\"foo\", testPermA)\n\t\t\t\tperms.SetUserRoles(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\", \"admin\", \"foo\")\n\t\t\t\treturn perms\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"user without roles\",\n\t\t\tuser: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New()\n\t\t\t\tperms.SetUserRoles(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\t\t\t\treturn perms\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"has no role\",\n\t\t\tuser: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\trole: \"bar\",\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New()\n\t\t\t\tperms.AddRole(\"foo\", testPermA)\n\t\t\t\tperms.SetUserRoles(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\", \"foo\")\n\t\t\t\treturn perms\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tperms := tc.setup()\n\t\t\tgot := perms.HasRole(tc.user, tc.role)\n\t\t\tuassert.Equal(t, tc.want, got)\n\t\t})\n\t}\n}\n\nfunc TestBasicPermissionsHasPermission(t *testing.T) {\n\tcases := []struct {\n\t\tname       string\n\t\tuser       address\n\t\tpermission boards.Permission\n\t\tsetup      func() *Permissions\n\t\twant       bool\n\t}{\n\t\t{\n\t\t\tname:       \"ok\",\n\t\t\tuser:       \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\tpermission: testPermA,\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New()\n\t\t\t\tperms.AddRole(\"foo\", testPermA)\n\t\t\t\tperms.SetUserRoles(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\", \"foo\")\n\t\t\t\treturn perms\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname:       \"ok with multiple users\",\n\t\t\tuser:       \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\tpermission: testPermA,\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New()\n\t\t\t\tperms.AddRole(\"foo\", testPermA)\n\t\t\t\tperms.SetUserRoles(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\", \"foo\")\n\t\t\t\tperms.SetUserRoles(\"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\", \"foo\")\n\t\t\t\treturn perms\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname:       \"ok with multiple roles\",\n\t\t\tuser:       \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\tpermission: testPermB,\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New()\n\t\t\t\tperms.AddRole(\"foo\", testPermA)\n\t\t\t\tperms.AddRole(\"baz\", testPermB)\n\t\t\t\tperms.SetUserRoles(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\", \"foo\", \"baz\")\n\t\t\t\treturn perms\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname:       \"no permission\",\n\t\t\tuser:       \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\tpermission: testPermB,\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New()\n\t\t\t\tperms.AddRole(\"foo\", testPermA)\n\t\t\t\tperms.SetUserRoles(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\", \"foo\")\n\t\t\t\treturn perms\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tperms := tc.setup()\n\t\t\tgot := perms.HasPermission(tc.user, tc.permission)\n\t\t\tuassert.Equal(t, tc.want, got)\n\t\t})\n\t}\n}\n\nfunc TestBasicPermissionsSetUserRoles(cur realm, t *testing.T) {\n\tcases := []struct {\n\t\tname          string\n\t\tuser          address\n\t\texpectedRoles []boards.Role\n\t\tsetup         func() *Permissions\n\t\terr           string\n\t}{\n\t\t{\n\t\t\tname:          \"add user\",\n\t\t\tuser:          address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"),\n\t\t\texpectedRoles: []boards.Role{\"a\"},\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New()\n\t\t\t\tperms.AddRole(\"a\", testPermA)\n\t\t\t\treturn perms\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"add user with multiple roles\",\n\t\t\tuser:          address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"),\n\t\t\texpectedRoles: []boards.Role{\"a\", \"b\"},\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New()\n\t\t\t\tperms.AddRole(\"a\", testPermA)\n\t\t\t\tperms.AddRole(\"b\", testPermB)\n\t\t\t\treturn perms\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"add when other users exists\",\n\t\t\tuser:          address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"),\n\t\t\texpectedRoles: []boards.Role{\"a\"},\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New()\n\t\t\t\tperms.AddRole(\"a\", testPermA)\n\t\t\t\tperms.SetUserRoles(\"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\", \"a\")\n\t\t\t\tperms.SetUserRoles(\"g1w4ek2u3jta047h6lta047h6lta047h6l9huexc\")\n\t\t\t\treturn perms\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"add user using single role\",\n\t\t\tuser:          address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"),\n\t\t\texpectedRoles: []boards.Role{\"a\"},\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New(UseSingleUserRole())\n\t\t\t\tperms.AddRole(\"a\", testPermA)\n\t\t\t\treturn perms\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"update user roles\",\n\t\t\tuser:          address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"),\n\t\t\texpectedRoles: []boards.Role{\"a\", \"b\"},\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New()\n\t\t\t\tperms.AddRole(\"a\", testPermA)\n\t\t\t\tperms.AddRole(\"b\", testPermB)\n\t\t\t\tperms.AddRole(\"c\", testPermB)\n\t\t\t\tperms.SetUserRoles(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\", \"c\")\n\t\t\t\treturn perms\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"update user roles using single role\",\n\t\t\tuser:          address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"),\n\t\t\texpectedRoles: []boards.Role{\"b\"},\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New(UseSingleUserRole())\n\t\t\t\tperms.AddRole(\"a\", testPermA)\n\t\t\t\tperms.AddRole(\"b\", testPermB)\n\t\t\t\tperms.SetUserRoles(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\", \"a\")\n\t\t\t\treturn perms\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"clear user roles\",\n\t\t\tuser:          address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"),\n\t\t\texpectedRoles: []boards.Role{},\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New()\n\t\t\t\tperms.AddRole(\"a\", testPermA)\n\t\t\t\tperms.AddRole(\"b\", testPermB)\n\t\t\t\tperms.SetUserRoles(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\", \"a\", \"b\")\n\t\t\t\treturn perms\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"set invalid role\",\n\t\t\tuser:          address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"),\n\t\t\texpectedRoles: []boards.Role{\"a\", \"foo\"},\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New()\n\t\t\t\tperms.AddRole(\"a\", testPermA)\n\t\t\t\treturn perms\n\t\t\t},\n\t\t\terr: \"invalid role: foo\",\n\t\t},\n\t\t{\n\t\t\tname:          \"use single role error\",\n\t\t\tuser:          address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"),\n\t\t\texpectedRoles: []boards.Role{\"a\", \"b\"},\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New(UseSingleUserRole())\n\t\t\t\tperms.AddRole(\"a\", testPermA)\n\t\t\t\tperms.AddRole(\"b\", testPermB)\n\t\t\t\treturn perms\n\t\t\t},\n\t\t\terr: \"user can only have one role\",\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tperms := tc.setup()\n\n\t\t\tsetUserRoles := func() {\n\t\t\t\tperms.SetUserRoles(tc.user, tc.expectedRoles...)\n\t\t\t}\n\n\t\t\tif tc.err != \"\" {\n\t\t\t\turequire.PanicsWithMessage(t, cur, tc.err, setUserRoles, \"expected an error\")\n\t\t\t\treturn\n\t\t\t} else {\n\t\t\t\turequire.NotPanics(t, cur, setUserRoles, \"expected no error\")\n\t\t\t}\n\n\t\t\troles := perms.GetUserRoles(tc.user)\n\t\t\tuassert.Equal(t, len(tc.expectedRoles), len(roles))\n\t\t\tfor i, r := range roles {\n\t\t\t\turequire.Equal(t, string(tc.expectedRoles[i]), string(r))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBasicPermissionsAddRoleOverwrite(t *testing.T) {\n\tuser := address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\n\tperms := New()\n\tperms.AddRole(\"foo\", testPermA)\n\tperms.SetUserRoles(user, \"foo\")\n\n\t// Re-adding an existing role replaces its permissions and keeps members\n\tperms.AddRole(\"foo\", testPermB)\n\tuassert.True(t, perms.HasRole(user, \"foo\"), \"expect role members to be kept\")\n\tuassert.False(t, perms.HasPermission(user, testPermA), \"expect old permissions to be revoked\")\n\tuassert.True(t, perms.HasPermission(user, testPermB), \"expect new permissions to be granted\")\n}\n\nfunc TestBasicPermissionsAddRoleEmptyName(cur realm, t *testing.T) {\n\turequire.PanicsWithMessage(t, cur, \"role name is required\", func() {\n\t\tNew().AddRole(\"  \", testPermA)\n\t}, \"expect whitespace only role names to be rejected\")\n\n\turequire.PanicsWithMessage(t, cur, \"permissions super role name is required\", func() {\n\t\tNew(WithSuperRole(\"  \"))\n\t}, \"expect whitespace only super role names to be rejected\")\n\n\t// The empty string is rejected before the superRole sentinel check, so\n\t// the outcome doesn't depend on whether a super role is configured.\n\turequire.PanicsWithMessage(t, cur, \"role name is required\", func() {\n\t\tNew().AddRole(\"\", testPermA)\n\t}, \"expect empty role name to be rejected without a super role\")\n\turequire.PanicsWithMessage(t, cur, \"role name is required\", func() {\n\t\tNew(WithSuperRole(\"owner\")).AddRole(\"\", testPermA)\n\t}, \"expect empty role name to be rejected with a super role\")\n}\n\nfunc TestBasicPermissionsSuperRole(t *testing.T) {\n\tuser := address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\tsuper := boards.Role(\"owner\")\n\n\tperms := New(WithSuperRole(super))\n\turequire.True(t, perms.RoleExists(super), \"expect super role to exist\")\n\turequire.False(t, perms.RoleExists(\"unknown\"))\n\n\t// Super role members get every permission without explicit mapping\n\tperms.SetUserRoles(user, super)\n\turequire.True(t, perms.HasRole(user, super))\n\tuassert.True(t, perms.HasUser(user))\n\tuassert.True(t, perms.HasPermission(user, testPermA))\n\tuassert.True(t, perms.HasPermission(user, testPermC))\n\n\t// AddRole on the super role is a no-op: the super role branch grants\n\t// every permission regardless of any meta a mapping would set\n\tperms.AddRole(super, testPermB)\n\tuassert.True(t, perms.HasPermission(user, testPermA), \"expect super role to keep all permissions\")\n}\n\nfunc TestBasicPermissionsHasUserUsersCount(t *testing.T) {\n\tmember := address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\tguest := address(\"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\")\n\n\tperms := New()\n\tperms.AddRole(\"foo\", testPermA)\n\turequire.Equal(t, 0, perms.UsersCount())\n\turequire.False(t, perms.HasUser(member))\n\n\t// A user added only through a role counts as a user\n\tperms.SetUserRoles(member, \"foo\")\n\turequire.True(t, perms.HasUser(member), \"expect role holder to be a user\")\n\turequire.Equal(t, 1, perms.UsersCount())\n\n\t// A guest without roles counts as a user\n\tperms.SetUserRoles(guest)\n\turequire.True(t, perms.HasUser(guest))\n\turequire.Equal(t, 2, perms.UsersCount())\n\n\t// Clearing roles keeps the user and doesn't change the count\n\tperms.SetUserRoles(member)\n\turequire.True(t, perms.HasUser(member), \"expect user to remain after roles are cleared\")\n\turequire.Equal(t, 2, perms.UsersCount())\n\n\t// Re-assigning roles must not double count\n\tperms.SetUserRoles(member, \"foo\")\n\turequire.Equal(t, 2, perms.UsersCount())\n}\n\nfunc TestBasicPermissionsSetUserRolesPanicKeepsState(cur realm, t *testing.T) {\n\tuser := address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\n\tperms := New()\n\tperms.AddRole(\"a\", testPermA)\n\tperms.SetUserRoles(user, \"a\")\n\n\turequire.PanicsWithMessage(t, cur, \"invalid role: nope\", func() {\n\t\tperms.SetUserRoles(user, \"nope\")\n\t}, \"expect invalid role to panic\")\n\n\t// The failed call must not have mutated state\n\tuassert.True(t, perms.HasUser(user))\n\tuassert.True(t, perms.HasRole(user, \"a\"))\n\tuassert.Equal(t, 1, perms.UsersCount())\n}\n\nfunc TestBasicPermissionsRemoveUser(t *testing.T) {\n\tcases := []struct {\n\t\tname  string\n\t\tuser  address\n\t\tsetup func() *Permissions\n\t\twant  bool\n\t}{\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\tuser: address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"),\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New()\n\t\t\t\tperms.SetUserRoles(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\t\t\t\treturn perms\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"user not found\",\n\t\t\tuser: address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"),\n\t\t\tsetup: func() *Permissions {\n\t\t\t\treturn New()\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tperms := tc.setup()\n\t\t\tgot := perms.RemoveUser(tc.user)\n\t\t\tuassert.Equal(t, tc.want, got)\n\t\t})\n\t}\n}\n\nfunc TestBasicPermissionsRemoveUserWithRoles(t *testing.T) {\n\tuser := address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\n\tperms := New()\n\tperms.AddRole(\"foo\", testPermA)\n\tperms.AddRole(\"bar\", testPermB)\n\tperms.SetUserRoles(user, \"foo\", \"bar\")\n\n\turequire.True(t, perms.RemoveUser(user), \"expect role holder to be removed\")\n\n\tuassert.False(t, perms.HasUser(user), \"expect removed user to not exist\")\n\tuassert.False(t, perms.HasRole(user, \"foo\"), \"expect removed user to keep no roles\")\n\tuassert.False(t, perms.HasPermission(user, testPermA), \"expect removed user to keep no permissions\")\n\tuassert.Equal(t, 0, len(perms.GetUserRoles(user)))\n\tuassert.Equal(t, 0, perms.UsersCount())\n\tuassert.False(t, perms.RemoveUser(user), \"expect second removal to report user not found\")\n}\n\nfunc TestBasicPermissionsIterateUsers(t *testing.T) {\n\t// Users are listed sorted by address, including a roleless guest.\n\tusers := []boards.User{\n\t\t{\n\t\t\tAddress: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\tRoles:   []boards.Role{\"foo\"},\n\t\t},\n\t\t{\n\t\t\tAddress: \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\",\n\t\t\tRoles:   []boards.Role{\"bar\", \"foo\"},\n\t\t},\n\t\t{\n\t\t\tAddress: \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\",\n\t\t\tRoles:   []boards.Role{\"bar\"},\n\t\t},\n\t\t{\n\t\t\tAddress: \"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\",\n\t\t},\n\t}\n\n\tperms := New()\n\tperms.AddRole(\"foo\", testPermA)\n\tperms.AddRole(\"bar\", testPermB)\n\t// Add users in reverse order to pin that iteration is address-sorted,\n\t// not insertion-ordered.\n\tfor i := len(users) - 1; i \u003e= 0; i-- {\n\t\tperms.SetUserRoles(users[i].Address, users[i].Roles...)\n\t}\n\n\tcases := []struct {\n\t\tname               string\n\t\tstart, count, want int\n\t}{\n\t\t{\n\t\t\tname:  \"exceed users count\",\n\t\t\tcount: 50,\n\t\t\twant:  4,\n\t\t},\n\t\t{\n\t\t\tname:  \"exact users count\",\n\t\t\tcount: 4,\n\t\t\twant:  4,\n\t\t},\n\t\t{\n\t\t\tname:  \"two users\",\n\t\t\tstart: 1,\n\t\t\tcount: 2,\n\t\t\twant:  2,\n\t\t},\n\t\t{\n\t\t\tname:  \"one user\",\n\t\t\tstart: 1,\n\t\t\tcount: 1,\n\t\t\twant:  1,\n\t\t},\n\t\t{\n\t\t\tname:  \"no iteration\",\n\t\t\tstart: 50,\n\t\t\tcount: 1,\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar visited int\n\t\t\tstopped := perms.IterateUsers(tc.start, tc.count, func(u boards.User) bool {\n\t\t\t\ti := tc.start + visited\n\t\t\t\turequire.True(t, i \u003c len(users), \"expect iterator to respect number of users\")\n\t\t\t\tuassert.Equal(t, users[i].Address, u.Address)\n\n\t\t\t\turequire.Equal(t, len(users[i].Roles), len(u.Roles), \"expect number of roles to match\")\n\t\t\t\tfor j := range u.Roles {\n\t\t\t\t\tuassert.Equal(t, string(users[i].Roles[j]), string(u.Roles[j]))\n\t\t\t\t}\n\n\t\t\t\tvisited++\n\t\t\t\treturn false\n\t\t\t})\n\n\t\t\tuassert.False(t, stopped, \"expect full iteration not to report an early stop\")\n\t\t\tuassert.Equal(t, tc.want, visited, \"expect iterator to visit the windowed users\")\n\t\t})\n\t}\n\n\t// Early stop is reported, and iteration actually halts.\n\tvar visited int\n\tstopped := perms.IterateUsers(0, 10, func(boards.User) bool {\n\t\tvisited++\n\t\treturn true\n\t})\n\tuassert.True(t, stopped, \"expect early stop to be reported\")\n\tuassert.Equal(t, 1, visited, \"expect iteration to halt on early stop\")\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"QVWft1AdImMIxv/5j00L5c+QbdorUOheUsNnGPQWUoMGL4FbJFoEBCAphGRxWXnHnIgLKcyTDhE17NsBwgXKnw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"hub","path":"gno.land/p/gnoland/boards/exts/hub/v0","files":[{"name":"README.md","body":"# hub\n\nSimplified, read-only view types over `gno.land/p/gnoland/boards/v0` 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/v0\"\n    hubexts \"gno.land/p/gnoland/boards/exts/hub/v0\"\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/v0\"\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":"board_test.gno","body":"package hub_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/exts/hub/v0\"\n\t\"gno.land/p/gnoland/boards/exts/permissions/v0\"\n\t\"gno.land/p/gnoland/boards/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestNewSafeBoard(t *testing.T) {\n\ttests := []struct {\n\t\tname                     string\n\t\tsetup                    func() *boards.Board\n\t\tthreadCount, memberCount int\n\t\tcreatedAt                int64\n\t}{\n\t\t{\n\t\t\tname:      \"new board\",\n\t\t\tsetup:     func() *boards.Board { return boards.New(1) },\n\t\t\tcreatedAt: testTime,\n\t\t},\n\t\t{\n\t\t\tname: \"with threads\",\n\t\t\tsetup: func() *boards.Board {\n\t\t\t\tb := boards.New(1)\n\t\t\t\tb.Threads.Add(boards.MustNewThread(b, alice, \"Title 1\", \"Body 1\"))\n\t\t\t\tb.Threads.Add(boards.MustNewThread(b, bob, \"Title 2\", \"Body 2\"))\n\t\t\t\treturn b\n\t\t\t},\n\t\t\tthreadCount: 2,\n\t\t\tcreatedAt:   testTime,\n\t\t},\n\t\t{\n\t\t\tname: \"with members\",\n\t\t\tsetup: func() *boards.Board {\n\t\t\t\tb := boards.New(1)\n\t\t\t\tperms := permissions.New()\n\t\t\t\tperms.SetUserRoles(alice)\n\t\t\t\tperms.SetUserRoles(bob)\n\t\t\t\tb.Permissions = perms\n\t\t\t\treturn b\n\t\t\t},\n\t\t\tmemberCount: 2,\n\t\t\tcreatedAt:   testTime,\n\t\t},\n\t\t{\n\t\t\t// A board with no storages assigned must not panic: the counts\n\t\t\t// are skipped and the zero creation time maps to a zero Unix time.\n\t\t\tname:  \"bare board without storages\",\n\t\t\tsetup: func() *boards.Board { return \u0026boards.Board{ID: 1} },\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tref := tt.setup()\n\t\t\tref.Name = \"test123\"\n\t\t\tref.Creator = alice\n\n\t\t\tboard := hub.NewSafeBoard(ref)\n\n\t\t\turequire.Equal(t, uint64(1), board.ID(), \"expect ID to match\")\n\t\t\turequire.Equal(t, \"test123\", board.Name(), \"expect name to match\")\n\t\t\turequire.Equal(t, alice, board.Creator(), \"expect creator to match\")\n\t\t\turequire.False(t, board.Readonly(), \"expect board not to be readonly\")\n\t\t\turequire.Equal(t, tt.threadCount, board.ThreadCount(), \"expect thread count to match\")\n\t\t\turequire.Equal(t, tt.memberCount, board.MemberCount(), \"expect member count to match\")\n\t\t\turequire.Equal(t, tt.createdAt, board.CreatedAt(), \"expect creation time to match\")\n\t\t\turequire.Equal(t, int64(0), board.UpdatedAt(), \"expect zero update time to map to zero\")\n\t\t})\n\t}\n}\n\n// TestNewSafeBoardSnapshotsAliases asserts the invariant this package exists\n// for: a safe type is a snapshot, so later writes to the source board must not\n// be observable through a value already handed to a caller.\nfunc TestNewSafeBoardSnapshotsAliases(t *testing.T) {\n\tref := boards.New(1)\n\tref.Name = \"current\"\n\tref.Aliases = []string{\"previous\"}\n\n\tboard := hub.NewSafeBoard(ref)\n\n\tref.Aliases[0] = \"mutated\"\n\tref.Aliases = append(ref.Aliases, \"added\")\n\tref.Name = \"renamed\"\n\tref.Readonly = true\n\n\taliases := board.Aliases()\n\turequire.Equal(t, 1, len(aliases), \"expect alias count to be unaffected\")\n\turequire.Equal(t, \"previous\", aliases[0], \"expect alias to be unaffected\")\n\turequire.Equal(t, \"current\", board.Name(), \"expect name to be unaffected\")\n\turequire.False(t, board.Readonly(), \"expect readonly to be unaffected\")\n}\n\nfunc TestNewSafeBoardNilRef(cur realm, t *testing.T) {\n\turequire.PanicsWithMessage(t, cur, \"board reference is nil\", func() {\n\t\thub.NewSafeBoard(nil)\n\t}, \"expect a nil board reference to be rejected\")\n}\n"},{"name":"comment.gno","body":"package hub\n\nimport (\n\t\"gno.land/p/gnoland/boards/v0\"\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":"comment_test.gno","body":"package hub_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/exts/hub/v0\"\n\t\"gno.land/p/gnoland/boards/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\n// Addresses and the fixed time the test VM reports for time.Now().\nconst (\n\talice    address = \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"\n\tbob      address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\ttestTime int64   = 1234567890\n)\n\nfunc TestNewSafeComment(t *testing.T) {\n\tboard := boards.New(1)\n\tthread := boards.MustNewThread(board, alice, \"Title\", \"Body\")\n\tref := boards.MustNewReply(thread, bob, \"Comment\")\n\tref.Flags.Add(boards.Flag{User: alice, Reason: \"Spam\"})\n\n\tcomment := hub.NewSafeComment(ref)\n\n\turequire.Equal(t, uint64(ref.ID), comment.ID(), \"expect ID to match\")\n\turequire.Equal(t, uint64(1), comment.BoardID(), \"expect board ID to match\")\n\turequire.Equal(t, uint64(thread.ID), comment.ThreadID(), \"expect thread ID to match\")\n\turequire.Equal(t, \"Comment\", comment.Body(), \"expect body to match\")\n\turequire.Equal(t, bob, comment.Creator(), \"expect creator to match\")\n\turequire.False(t, comment.Hidden(), \"expect comment not to be hidden\")\n\turequire.Equal(t, 0, comment.ReplyCount(), \"expect no replies\")\n\turequire.Equal(t, 1, comment.FlagCount(), \"expect one flag\")\n\turequire.Equal(t, testTime, comment.CreatedAt(), \"expect creation time to match\")\n\turequire.Equal(t, int64(0), comment.UpdatedAt(), \"expect zero update time to map to zero\")\n}\n\n// TestNewSafeCommentParentID documents that a top level comment reports its\n// thread as parent rather than a zero parent, so callers must compare\n// ParentID against ThreadID to tell comments and replies apart.\nfunc TestNewSafeCommentParentID(t *testing.T) {\n\tboard := boards.New(1)\n\tthread := boards.MustNewThread(board, alice, \"Title\", \"Body\")\n\tcommentRef := boards.MustNewReply(thread, bob, \"Comment\")\n\treplyRef := boards.MustNewReply(commentRef, alice, \"Reply\")\n\n\tcomment := hub.NewSafeComment(commentRef)\n\treply := hub.NewSafeComment(replyRef)\n\n\turequire.Equal(t, comment.ThreadID(), comment.ParentID(), \"expect a comment's parent to be its thread\")\n\turequire.Equal(t, comment.ID(), reply.ParentID(), \"expect a reply's parent to be its comment\")\n\turequire.Equal(t, comment.ThreadID(), reply.ThreadID(), \"expect a reply to keep the thread ID\")\n}\n\nfunc TestNewSafeCommentReplyCount(t *testing.T) {\n\tboard := boards.New(1)\n\tthread := boards.MustNewThread(board, alice, \"Title\", \"Body\")\n\tref := boards.MustNewReply(thread, bob, \"Comment\")\n\tref.Replies.Add(boards.MustNewReply(ref, alice, \"Reply 1\"))\n\tref.Replies.Add(boards.MustNewReply(ref, alice, \"Reply 2\"))\n\n\tcomment := hub.NewSafeComment(ref)\n\n\turequire.Equal(t, 2, comment.ReplyCount(), \"expect reply count to match\")\n}\n\n// TestNewSafeCommentNilStorages covers the branches that skip counting when a\n// post carries no reply or flag storage.\nfunc TestNewSafeCommentNilStorages(t *testing.T) {\n\tref := \u0026boards.Post{ID: 2, ParentID: 1, ThreadID: 1, Board: boards.New(1)}\n\n\tcomment := hub.NewSafeComment(ref)\n\n\turequire.Equal(t, 0, comment.ReplyCount(), \"expect no replies\")\n\turequire.Equal(t, 0, comment.FlagCount(), \"expect no flags\")\n\turequire.Equal(t, int64(0), comment.CreatedAt(), \"expect zero creation time to map to zero\")\n}\n\nfunc TestNewSafeCommentRejectsInvalidRefs(cur realm, t *testing.T) {\n\tboard := boards.New(1)\n\tthread := boards.MustNewThread(board, alice, \"Title\", \"Body\")\n\n\turequire.PanicsWithMessage(t, cur, \"post reference is nil\", func() {\n\t\thub.NewSafeComment(nil)\n\t}, \"expect a nil post reference to be rejected\")\n\n\turequire.PanicsWithMessage(t, cur, \"post is not a comment or reply\", func() {\n\t\thub.NewSafeComment(thread)\n\t}, \"expect a thread to be rejected\")\n}\n"},{"name":"flag.gno","body":"package hub\n\nimport (\n\t\"gno.land/p/gnoland/boards/v0\"\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/v0\"\ngno = \"0.9\"\n"},{"name":"member.gno","body":"package hub\n\nimport (\n\t\"gno.land/p/gnoland/boards/v0\"\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":"member_test.gno","body":"package hub_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/exts/hub/v0\"\n\t\"gno.land/p/gnoland/boards/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestNewSafeMember(t *testing.T) {\n\tref := boards.User{\n\t\tAddress: alice,\n\t\tRoles:   []boards.Role{\"owner\", \"admin\"},\n\t}\n\n\tmember := hub.NewSafeMember(ref)\n\n\turequire.Equal(t, alice.String(), member.Address().String(), \"expect address to match\")\n\turequire.Equal(t, 2, len(member.Roles()), \"expect role count to match\")\n\turequire.Equal(t, \"owner\", member.Roles()[0], \"expect first role to match\")\n\turequire.Equal(t, \"admin\", member.Roles()[1], \"expect second role to match\")\n}\n\nfunc TestNewSafeMemberWithoutRoles(t *testing.T) {\n\tmember := hub.NewSafeMember(boards.User{Address: bob})\n\n\turequire.Equal(t, bob.String(), member.Address().String(), \"expect address to match\")\n\turequire.Equal(t, 0, len(member.Roles()), \"expect no roles\")\n}\n\nfunc TestSafeMemberIsASnapshot(t *testing.T) {\n\troles := []boards.Role{\"owner\"}\n\tmember := hub.NewSafeMember(boards.User{Address: alice, Roles: roles})\n\n\t// Mutating the source must not change what the member reports.\n\troles[0] = \"guest\"\n\turequire.Equal(t, \"owner\", member.Roles()[0], \"expect source mutation to be ignored\")\n\n\t// Mutating a returned slice must not change what the member reports.\n\tmember.Roles()[0] = \"guest\"\n\turequire.Equal(t, \"owner\", member.Roles()[0], \"expect returned slice to be a copy\")\n}\n"},{"name":"thread.gno","body":"package hub\n\nimport (\n\t\"gno.land/p/gnoland/boards/v0\"\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"},{"name":"thread_test.gno","body":"package hub_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/exts/hub/v0\"\n\t\"gno.land/p/gnoland/boards/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestNewSafeThread(t *testing.T) {\n\tboard := boards.New(1)\n\tref := boards.MustNewThread(board, alice, \"Title\", \"Body\")\n\tref.Flags.Add(boards.Flag{User: bob, Reason: \"Spam\"})\n\n\tthread := hub.NewSafeThread(ref)\n\n\turequire.Equal(t, uint64(ref.ID), thread.ID(), \"expect ID to match\")\n\turequire.Equal(t, uint64(1), thread.BoardID(), \"expect board ID to match\")\n\turequire.Equal(t, \"Title\", thread.Title(), \"expect title to match\")\n\turequire.Equal(t, \"Body\", thread.Body(), \"expect body to match\")\n\turequire.Equal(t, alice, thread.Creator(), \"expect creator to match\")\n\turequire.False(t, thread.Hidden(), \"expect thread not to be hidden\")\n\turequire.False(t, thread.Readonly(), \"expect thread not to be readonly\")\n\turequire.Equal(t, 0, thread.CommentCount(), \"expect no comments\")\n\turequire.Equal(t, 0, thread.RepostCount(), \"expect no reposts\")\n\turequire.Equal(t, 1, thread.FlagCount(), \"expect one flag\")\n\turequire.Equal(t, testTime, thread.CreatedAt(), \"expect creation time to match\")\n\turequire.Equal(t, int64(0), thread.UpdatedAt(), \"expect zero update time to map to zero\")\n}\n\nfunc TestNewSafeThreadCounts(t *testing.T) {\n\tboard := boards.New(1)\n\tref := boards.MustNewThread(board, alice, \"Title\", \"Body\")\n\tref.Replies.Add(boards.MustNewReply(ref, bob, \"Comment 1\"))\n\tref.Replies.Add(boards.MustNewReply(ref, bob, \"Comment 2\"))\n\n\tthread := hub.NewSafeThread(ref)\n\n\turequire.Equal(t, 2, thread.CommentCount(), \"expect comment count to match\")\n\turequire.Equal(t, 0, thread.FlagCount(), \"expect no flags\")\n}\n\n// TestNewSafeThreadNilStorages covers the branches that skip counting when a\n// post carries no reply, repost or flag storage.\nfunc TestNewSafeThreadNilStorages(t *testing.T) {\n\tref := \u0026boards.Post{ID: 1, ThreadID: 1, Board: boards.New(1)}\n\n\tthread := hub.NewSafeThread(ref)\n\n\turequire.Equal(t, 0, thread.CommentCount(), \"expect no comments\")\n\turequire.Equal(t, 0, thread.RepostCount(), \"expect no reposts\")\n\turequire.Equal(t, 0, thread.FlagCount(), \"expect no flags\")\n\turequire.Equal(t, int64(0), thread.CreatedAt(), \"expect zero creation time to map to zero\")\n}\n\nfunc TestNewSafeThreadRejectsInvalidRefs(cur realm, t *testing.T) {\n\tboard := boards.New(1)\n\tthread := boards.MustNewThread(board, alice, \"Title\", \"Body\")\n\tcomment := boards.MustNewReply(thread, alice, \"Comment\")\n\n\turequire.PanicsWithMessage(t, cur, \"post reference is nil\", func() {\n\t\thub.NewSafeThread(nil)\n\t}, \"expect a nil post reference to be rejected\")\n\n\turequire.PanicsWithMessage(t, cur, \"post is not a thread\", func() {\n\t\thub.NewSafeThread(comment)\n\t}, \"expect a comment to be rejected\")\n}\n\nfunc TestNewSafeFlag(t *testing.T) {\n\tflag := hub.NewSafeFlag(boards.Flag{User: alice, Reason: \"Spam\"})\n\n\turequire.Equal(t, alice, flag.User(), \"expect user to match\")\n\turequire.Equal(t, \"Spam\", flag.Reason(), \"expect reason to match\")\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"p6t69pFWjFZNDzqYKIwmxoY6jXNcugI2RxzfJ/LV+zVbSRP0gGTdJNxQv/4KvsrBNAkiY0QHflvDhKDPsB1e+A=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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":"allowancesender_test.gno","body":"package allowancesender\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\n// cur is a zero-value realm used as a placeholder when forwarding to\n// uassert/urequire dispatch helpers that gained an `rlm realm` param.\n// These tests pass `func()` callbacks (no crossing inside the callback),\n// so rlm is ignored — a nil realm here is safe.\nvar cur realm\n\n// mockBanker implements banker.Banker for unit testing without\n// touching real bank state. Records every SendCoins call.\n//\n// Since the public New constructor rejects non-canonical bankers via\n// banker.IsCanonical, mockBanker cannot flow through New. Tests that\n// need a controllable mock build the AllowanceSender via newWithMock,\n// which uses in-package struct-literal construction. Same-package\n// authorship is the trust boundary; the public API remains unforgeable\n// to other packages.\ntype mockBanker struct {\n\tsends    []sendCall\n\tfailNext bool   // if true, next SendCoins panics\n\tfailMsg  string // panic message for failNext\n}\n\ntype sendCall struct {\n\tfrom, to address\n\tamt      chain.Coins\n}\n\nfunc (m *mockBanker) GetCoins(addr address) chain.Coins        { return nil }\nfunc (m *mockBanker) GetCoin(addr address, denom string) int64 { return 0 }\nfunc (m *mockBanker) SendCoins(from, to address, amt chain.Coins) {\n\tif m.failNext {\n\t\tm.failNext = false\n\t\tpanic(m.failMsg)\n\t}\n\tm.sends = append(m.sends, sendCall{from, to, amt})\n}\nfunc (m *mockBanker) TotalCoin(denom string) int64                        { return 0 }\nfunc (m *mockBanker) IssueCoin(addr address, denom string, amount int64)  {}\nfunc (m *mockBanker) RemoveCoin(addr address, denom string, amount int64) {}\n\n// newWithMock constructs an *AllowanceSender wrapping a mockBanker via\n// struct-literal initialization, bypassing New (which would reject the\n// mock as non-canonical). Used only in this test file.\nfunc newWithMock(mb *mockBanker, p address, limit chain.Coins) *AllowanceSender {\n\treturn \u0026AllowanceSender{\n\t\tinner: mb,\n\t\tpayer: p,\n\t\tlimit: limit,\n\t}\n}\n\nvar (\n\tpayer = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\tbob   = address(\"g1mtmrdmqfu0aryqfl4aw65n35haw2wdjkh5p4cp\")\n\tcarol = address(\"g16dverxg6s6vzha89k22c3zusx6sug5ycjyjq2j\")\n)\n\n// ---------------------------------------------------------------------\n// Constructor tests\n// ---------------------------------------------------------------------\n\nfunc TestNew_Valid(t *testing.T) {\n\tmb := \u0026mockBanker{}\n\tlimit := chain.NewCoins(chain.NewCoin(\"ugnot\", 1000))\n\tal := newWithMock(mb, payer, limit)\n\n\tuassert.Equal(t, false, al.Closed())\n\tuassert.Equal(t, payer.String(), al.Payer().String())\n\tuassert.Equal(t, limit.String(), al.Cap().String())\n}\n\nfunc TestNew_NonCanonicalBanker(cur realm, t *testing.T) {\n\t// A hand-rolled banker.Banker (mockBanker) is not the canonical\n\t// chain/banker.Banker; New must reject it. This is the security\n\t// hole the IsCanonical gate closes — without it, a malicious caller\n\t// could pass a no-op banker and cause AllowanceSender.Send to\n\t// silently succeed without moving real coins.\n\tmb := \u0026mockBanker{}\n\tuassert.PanicsWithMessage(t, cur,\n\t\t\"allowancesender: inner banker is not the canonical chain/banker.Banker\",\n\t\tfunc() {\n\t\t\tNew(mb, payer, chain.NewCoins(chain.NewCoin(\"ugnot\", 1)))\n\t\t})\n}\n\nfunc TestNew_NilInner(cur realm, t *testing.T) {\n\t// Nil also fails IsCanonical (nil interface assertion is false).\n\tuassert.PanicsWithMessage(t, cur,\n\t\t\"allowancesender: inner banker is not the canonical chain/banker.Banker\",\n\t\tfunc() {\n\t\t\tNew(nil, payer, chain.NewCoins(chain.NewCoin(\"ugnot\", 1)))\n\t\t})\n}\n\nfunc TestNew_EmptyCap(t *testing.T) {\n\t// Empty cap is valid at construction (every Send will panic with\n\t// \"cap exceeded\"). Useful for \"preallocated zero-allowance\" cases.\n\tmb := \u0026mockBanker{}\n\tal := newWithMock(mb, payer, chain.Coins{})\n\tuassert.Equal(t, false, al.Closed())\n}\n\n// ---------------------------------------------------------------------\n// Send happy-path tests\n// ---------------------------------------------------------------------\n\nfunc TestSend_WithinCap(cur realm, t *testing.T) {\n\tmb := \u0026mockBanker{}\n\tal := newWithMock(mb, payer, chain.NewCoins(chain.NewCoin(\"ugnot\", 1000)))\n\n\turequire.NotPanics(t, cur, func() {\n\t\tal.Send(bob, chain.NewCoins(chain.NewCoin(\"ugnot\", 400)))\n\t})\n\n\tuassert.Equal(t, 1, len(mb.sends))\n\tuassert.Equal(t, payer.String(), mb.sends[0].from.String())\n\tuassert.Equal(t, bob.String(), mb.sends[0].to.String())\n\tuassert.Equal(t, int64(400), mb.sends[0].amt.AmountOf(\"ugnot\"))\n}\n\nfunc TestSend_MultipleWithinCap(cur realm, t *testing.T) {\n\tmb := \u0026mockBanker{}\n\tal := newWithMock(mb, payer, chain.NewCoins(chain.NewCoin(\"ugnot\", 1000)))\n\n\turequire.NotPanics(t, cur, func() {\n\t\tal.Send(bob, chain.NewCoins(chain.NewCoin(\"ugnot\", 300)))\n\t})\n\turequire.NotPanics(t, cur, func() {\n\t\tal.Send(carol, chain.NewCoins(chain.NewCoin(\"ugnot\", 600)))\n\t})\n\n\tuassert.Equal(t, 2, len(mb.sends))\n\tuassert.Equal(t, int64(900), al.Spent().AmountOf(\"ugnot\"))\n\tuassert.Equal(t, int64(100), al.Remaining().AmountOf(\"ugnot\"))\n}\n\nfunc TestSend_ExactlyAtCap(cur realm, t *testing.T) {\n\tmb := \u0026mockBanker{}\n\tal := newWithMock(mb, payer, chain.NewCoins(chain.NewCoin(\"ugnot\", 1000)))\n\n\turequire.NotPanics(t, cur, func() {\n\t\tal.Send(bob, chain.NewCoins(chain.NewCoin(\"ugnot\", 1000)))\n\t})\n\tuassert.Equal(t, int64(1000), al.Spent().AmountOf(\"ugnot\"))\n\tuassert.Equal(t, 0, len(al.Remaining()))\n}\n\n// ---------------------------------------------------------------------\n// Send rejection tests\n// ---------------------------------------------------------------------\n\nfunc TestSend_ExceedsCapInOneCall(cur realm, t *testing.T) {\n\tmb := \u0026mockBanker{}\n\tal := newWithMock(mb, payer, chain.NewCoins(chain.NewCoin(\"ugnot\", 1000)))\n\n\tuassert.PanicsWithMessage(t, cur, \"allowancesender: cap exceeded for denom ugnot\", func() {\n\t\tal.Send(bob, chain.NewCoins(chain.NewCoin(\"ugnot\", 1001)))\n\t})\n\t// State must not change on rejection.\n\tuassert.Equal(t, 0, len(mb.sends))\n\tuassert.Equal(t, int64(0), al.Spent().AmountOf(\"ugnot\"))\n}\n\nfunc TestSend_ExceedsCapAcrossCalls(cur realm, t *testing.T) {\n\tmb := \u0026mockBanker{}\n\tal := newWithMock(mb, payer, chain.NewCoins(chain.NewCoin(\"ugnot\", 1000)))\n\n\turequire.NotPanics(t, cur, func() {\n\t\tal.Send(bob, chain.NewCoins(chain.NewCoin(\"ugnot\", 700)))\n\t})\n\tuassert.PanicsWithMessage(t, cur, \"allowancesender: cap exceeded for denom ugnot\", func() {\n\t\tal.Send(carol, chain.NewCoins(chain.NewCoin(\"ugnot\", 400)))\n\t})\n\t// First send happened, second didn't.\n\tuassert.Equal(t, 1, len(mb.sends))\n\tuassert.Equal(t, int64(700), al.Spent().AmountOf(\"ugnot\"))\n}\n\nfunc TestSend_DenomNotInCap(cur realm, t *testing.T) {\n\tmb := \u0026mockBanker{}\n\tal := newWithMock(mb, payer, chain.NewCoins(chain.NewCoin(\"ugnot\", 1000)))\n\n\t// \"atom\" is not in cap; capAmt=0; any positive amount panics.\n\tuassert.PanicsWithMessage(t, cur, \"allowancesender: cap exceeded for denom atom\", func() {\n\t\tal.Send(bob, chain.NewCoins(chain.NewCoin(\"atom\", 1)))\n\t})\n}\n\nfunc TestSend_ZeroCap(cur realm, t *testing.T) {\n\tmb := \u0026mockBanker{}\n\tal := newWithMock(mb, payer, chain.Coins{}) // empty cap\n\n\tuassert.PanicsWithMessage(t, cur, \"allowancesender: cap exceeded for denom ugnot\", func() {\n\t\tal.Send(bob, chain.NewCoins(chain.NewCoin(\"ugnot\", 1)))\n\t})\n}\n\nfunc TestSend_AfterClose(cur realm, t *testing.T) {\n\tmb := \u0026mockBanker{}\n\tal := newWithMock(mb, payer, chain.NewCoins(chain.NewCoin(\"ugnot\", 1000)))\n\n\tal.Close()\n\tuassert.True(t, al.Closed())\n\n\tuassert.PanicsWithMessage(t, cur, \"allowancesender: closed\", func() {\n\t\tal.Send(bob, chain.NewCoins(chain.NewCoin(\"ugnot\", 100)))\n\t})\n\tuassert.Equal(t, 0, len(mb.sends))\n}\n\n// ---------------------------------------------------------------------\n// Multi-denom tests\n// ---------------------------------------------------------------------\n\nfunc TestSend_MultiDenomCap(cur realm, t *testing.T) {\n\tmb := \u0026mockBanker{}\n\tal := newWithMock(mb, payer, chain.NewCoins(\n\t\tchain.NewCoin(\"ugnot\", 1000),\n\t\tchain.NewCoin(\"foo\", 50),\n\t))\n\n\t// Spend on both denoms within their caps.\n\turequire.NotPanics(t, cur, func() {\n\t\tal.Send(bob, chain.NewCoins(\n\t\t\tchain.NewCoin(\"ugnot\", 600),\n\t\t\tchain.NewCoin(\"foo\", 30),\n\t\t))\n\t})\n\n\tuassert.Equal(t, int64(600), al.Spent().AmountOf(\"ugnot\"))\n\tuassert.Equal(t, int64(30), al.Spent().AmountOf(\"foo\"))\n\n\trem := al.Remaining()\n\tuassert.Equal(t, int64(400), rem.AmountOf(\"ugnot\"))\n\tuassert.Equal(t, int64(20), rem.AmountOf(\"foo\"))\n}\n\nfunc TestSend_OneDenomExceedsBlocksAll(cur realm, t *testing.T) {\n\tmb := \u0026mockBanker{}\n\tal := newWithMock(mb, payer, chain.NewCoins(\n\t\tchain.NewCoin(\"ugnot\", 1000),\n\t\tchain.NewCoin(\"foo\", 50),\n\t))\n\n\t// foo would exceed (51 \u003e 50); whole send rejected, ugnot doesn't go through.\n\tuassert.PanicsWithMessage(t, cur, \"allowancesender: cap exceeded for denom foo\", func() {\n\t\tal.Send(bob, chain.NewCoins(\n\t\t\tchain.NewCoin(\"ugnot\", 100),\n\t\t\tchain.NewCoin(\"foo\", 51),\n\t\t))\n\t})\n\n\t// Neither denom should have been spent.\n\tuassert.Equal(t, int64(0), al.Spent().AmountOf(\"ugnot\"))\n\tuassert.Equal(t, int64(0), al.Spent().AmountOf(\"foo\"))\n\tuassert.Equal(t, 0, len(mb.sends))\n}\n\n// ---------------------------------------------------------------------\n// Close behavior\n// ---------------------------------------------------------------------\n\nfunc TestClose_Idempotent(cur realm, t *testing.T) {\n\tmb := \u0026mockBanker{}\n\tal := newWithMock(mb, payer, chain.NewCoins(chain.NewCoin(\"ugnot\", 1000)))\n\n\tal.Close()\n\turequire.NotPanics(t, cur, func() { al.Close() })\n\turequire.NotPanics(t, cur, func() { al.Close() })\n\tuassert.True(t, al.Closed())\n}\n\nfunc TestClose_DoesNotInterfereWithReadMethods(cur realm, t *testing.T) {\n\tmb := \u0026mockBanker{}\n\tlimit := chain.NewCoins(chain.NewCoin(\"ugnot\", 1000))\n\tal := newWithMock(mb, payer, limit)\n\n\turequire.NotPanics(t, cur, func() {\n\t\tal.Send(bob, chain.NewCoins(chain.NewCoin(\"ugnot\", 250)))\n\t})\n\tal.Close()\n\n\t// Read methods must keep working after close.\n\tuassert.Equal(t, limit.String(), al.Cap().String())\n\tuassert.Equal(t, int64(250), al.Spent().AmountOf(\"ugnot\"))\n\tuassert.Equal(t, int64(750), al.Remaining().AmountOf(\"ugnot\"))\n\tuassert.True(t, al.Closed())\n}\n\n// ---------------------------------------------------------------------\n// Inner-banker panic tests\n// ---------------------------------------------------------------------\n\nfunc TestSend_InnerPanicRollsBackSpent(cur realm, t *testing.T) {\n\tmb := \u0026mockBanker{failNext: true, failMsg: \"insufficient funds\"}\n\tal := newWithMock(mb, payer, chain.NewCoins(chain.NewCoin(\"ugnot\", 1000)))\n\n\tuassert.PanicsWithMessage(t, cur, \"insufficient funds\", func() {\n\t\tal.Send(bob, chain.NewCoins(chain.NewCoin(\"ugnot\", 500)))\n\t})\n\n\t// On inner panic, spent must be rolled back.\n\tuassert.Equal(t, int64(0), al.Spent().AmountOf(\"ugnot\"))\n\tuassert.Equal(t, int64(1000), al.Remaining().AmountOf(\"ugnot\"))\n\n\t// And subsequent valid Send still works.\n\turequire.NotPanics(t, cur, func() {\n\t\tal.Send(bob, chain.NewCoins(chain.NewCoin(\"ugnot\", 200)))\n\t})\n\tuassert.Equal(t, int64(200), al.Spent().AmountOf(\"ugnot\"))\n}\n\n// ---------------------------------------------------------------------\n// Remaining / Spent edge cases\n// ---------------------------------------------------------------------\n\nfunc TestRemaining_EmptyCap(t *testing.T) {\n\tmb := \u0026mockBanker{}\n\tal := newWithMock(mb, payer, chain.Coins{})\n\n\tuassert.Equal(t, 0, len(al.Remaining()))\n}\n\nfunc TestRemaining_FullySpent(cur realm, t *testing.T) {\n\tmb := \u0026mockBanker{}\n\tal := newWithMock(mb, payer, chain.NewCoins(chain.NewCoin(\"ugnot\", 100)))\n\n\turequire.NotPanics(t, cur, func() {\n\t\tal.Send(bob, chain.NewCoins(chain.NewCoin(\"ugnot\", 100)))\n\t})\n\tuassert.Equal(t, 0, len(al.Remaining()))\n}\n\nfunc TestRemaining_PartialAcrossDenoms(cur realm, t *testing.T) {\n\tmb := \u0026mockBanker{}\n\tal := newWithMock(mb, payer, chain.NewCoins(\n\t\tchain.NewCoin(\"ugnot\", 1000),\n\t\tchain.NewCoin(\"foo\", 50),\n\t))\n\n\t// Spend all of foo, half of ugnot.\n\turequire.NotPanics(t, cur, func() {\n\t\tal.Send(bob, chain.NewCoins(\n\t\t\tchain.NewCoin(\"ugnot\", 500),\n\t\t\tchain.NewCoin(\"foo\", 50),\n\t\t))\n\t})\n\n\trem := al.Remaining()\n\t// foo should be omitted (zero remainder); ugnot has 500 left.\n\tuassert.Equal(t, 1, len(rem))\n\tuassert.Equal(t, int64(500), rem.AmountOf(\"ugnot\"))\n\tuassert.Equal(t, int64(0), rem.AmountOf(\"foo\"))\n}\n\n// ---------------------------------------------------------------------\n// Pointer semantics: shared state across copies\n// ---------------------------------------------------------------------\n\nfunc TestPointerSemantics_CloseVisibleViaAlias(cur realm, t *testing.T) {\n\tmb := \u0026mockBanker{}\n\tal := newWithMock(mb, payer, chain.NewCoins(chain.NewCoin(\"ugnot\", 1000)))\n\n\t// Aliasing the pointer should share state.\n\talias := al\n\turequire.NotPanics(t, cur, func() {\n\t\talias.Send(bob, chain.NewCoins(chain.NewCoin(\"ugnot\", 100)))\n\t})\n\tuassert.Equal(t, int64(100), al.Spent().AmountOf(\"ugnot\"))\n\n\t// Close via either reference.\n\talias.Close()\n\tuassert.True(t, al.Closed())\n\tuassert.True(t, alias.Closed())\n\n\tuassert.PanicsWithMessage(t, cur, \"allowancesender: closed\", func() {\n\t\tal.Send(bob, chain.NewCoins(chain.NewCoin(\"ugnot\", 1)))\n\t})\n}\n\n// ---------------------------------------------------------------------\n// Defensive copy: mutating returned Coins must not leak into state\n// ---------------------------------------------------------------------\n\nfunc TestCap_ReturnsDefensiveCopy(cur realm, t *testing.T) {\n\tmb := \u0026mockBanker{}\n\tal := newWithMock(mb, payer, chain.NewCoins(chain.NewCoin(\"ugnot\", 1000)))\n\n\t// Mutate the returned cap; internal state must not change.\n\tgot := al.Cap()\n\tif len(got) \u003e 0 {\n\t\tgot[0].Amount = 9_999_999_999\n\t}\n\n\t// Confirm internal state intact: re-read Cap.\n\tagain := al.Cap()\n\tuassert.Equal(t, int64(1000), again.AmountOf(\"ugnot\"))\n\n\t// Confirm enforcement still works: 1001 should still be rejected.\n\tuassert.PanicsWithMessage(t, cur, \"allowancesender: cap exceeded for denom ugnot\", func() {\n\t\tal.Send(bob, chain.NewCoins(chain.NewCoin(\"ugnot\", 1001)))\n\t})\n}\n\nfunc TestSpent_ReturnsDefensiveCopy(cur realm, t *testing.T) {\n\tmb := \u0026mockBanker{}\n\tal := newWithMock(mb, payer, chain.NewCoins(chain.NewCoin(\"ugnot\", 1000)))\n\n\turequire.NotPanics(t, cur, func() {\n\t\tal.Send(bob, chain.NewCoins(chain.NewCoin(\"ugnot\", 300)))\n\t})\n\n\tgot := al.Spent()\n\tif len(got) \u003e 0 {\n\t\tgot[0].Amount = 0 // try to \"rewind\" spent\n\t}\n\n\t// Internal spent must remain accurate.\n\tuassert.Equal(t, int64(300), al.Spent().AmountOf(\"ugnot\"))\n\tuassert.Equal(t, int64(700), al.Remaining().AmountOf(\"ugnot\"))\n}\n\n// ---------------------------------------------------------------------\n// Independence of separate AllowanceSenders\n// ---------------------------------------------------------------------\n\nfunc TestMultipleAllowances_Independent(cur realm, t *testing.T) {\n\tmb := \u0026mockBanker{}\n\ta1 := newWithMock(mb, payer, chain.NewCoins(chain.NewCoin(\"ugnot\", 100)))\n\ta2 := newWithMock(mb, payer, chain.NewCoins(chain.NewCoin(\"ugnot\", 200)))\n\n\turequire.NotPanics(t, cur, func() {\n\t\ta1.Send(bob, chain.NewCoins(chain.NewCoin(\"ugnot\", 80)))\n\t})\n\turequire.NotPanics(t, cur, func() {\n\t\ta2.Send(carol, chain.NewCoins(chain.NewCoin(\"ugnot\", 150)))\n\t})\n\n\tuassert.Equal(t, int64(80), a1.Spent().AmountOf(\"ugnot\"))\n\tuassert.Equal(t, int64(150), a2.Spent().AmountOf(\"ugnot\"))\n\n\t// Closing one must not close the other.\n\ta1.Close()\n\tuassert.True(t, a1.Closed())\n\tuassert.False(t, a2.Closed())\n\n\turequire.NotPanics(t, cur, func() {\n\t\ta2.Send(carol, chain.NewCoins(chain.NewCoin(\"ugnot\", 50)))\n\t})\n\tuassert.PanicsWithMessage(t, cur, \"allowancesender: closed\", func() {\n\t\ta1.Send(bob, chain.NewCoins(chain.NewCoin(\"ugnot\", 1)))\n\t})\n}\n\n// ---------------------------------------------------------------------\n// Overflow / negative-amount adversarial inputs\n// ---------------------------------------------------------------------\n\n// Adversary attempts to bypass the cap check by passing a value that\n// overflows int64 when added to an existing spent counter. Must panic\n// with the overflow-specific message rather than silently wrapping\n// (which would let the cap check pass and allow an absurd send).\nfunc TestSend_OverflowAttempt(cur realm, t *testing.T) {\n\tmb := \u0026mockBanker{}\n\tconst maxInt64 = int64(9223372036854775807)\n\tal := newWithMock(mb, payer, chain.NewCoins(chain.NewCoin(\"ugnot\", maxInt64)))\n\n\t// First spend uses most of the cap.\n\turequire.NotPanics(t, cur, func() {\n\t\tal.Send(bob, chain.NewCoins(chain.NewCoin(\"ugnot\", maxInt64-100)))\n\t})\n\n\t// Now spent ≈ MaxInt64 - 100. An adversary tries to pass amount\n\t// 200, which would overflow when added to spent. Must panic with\n\t// \"amount overflow\", not silently wrap to negative and bypass cap.\n\tuassert.PanicsWithMessage(t, cur, \"allowancesender: amount overflow on spent+amt for denom ugnot\", func() {\n\t\tal.Send(bob, chain.NewCoins(chain.NewCoin(\"ugnot\", 200)))\n\t})\n\n\t// State must not have changed.\n\tuassert.Equal(t, maxInt64-100, al.Spent().AmountOf(\"ugnot\"))\n\t// Bank-side recorded only the first valid send.\n\tuassert.Equal(t, 1, len(mb.sends))\n}\n\nfunc TestSend_NegativeAmount(cur realm, t *testing.T) {\n\tmb := \u0026mockBanker{}\n\tal := newWithMock(mb, payer, chain.NewCoins(chain.NewCoin(\"ugnot\", 1000)))\n\n\t// Spend something so we have a non-zero spent.\n\turequire.NotPanics(t, cur, func() {\n\t\tal.Send(bob, chain.NewCoins(chain.NewCoin(\"ugnot\", 200)))\n\t})\n\n\t// Adversary tries to \"uncharge\" the allowance by passing a negative\n\t// amount. Without this guard, spent would decrease, allowing more\n\t// future spends than originally permitted.\n\tuassert.PanicsWithMessage(t, cur, \"allowancesender: negative amount not allowed for denom ugnot\", func() {\n\t\tal.Send(bob, chain.Coins{chain.Coin{Denom: \"ugnot\", Amount: -50}})\n\t})\n\n\t// Spent is unchanged after the rejected attempt.\n\tuassert.Equal(t, int64(200), al.Spent().AmountOf(\"ugnot\"))\n}\n\n// Adversary's defer-recover swallows the panic from inner.SendCoins to\n// hide a failed send. Verifies that even with recovery, spent has been\n// rolled back — i.e., a recovered panic doesn't burn allowance against\n// a transfer that never moved funds.\nfunc TestSend_RecoveredPanicDoesNotBurnAllowance(cur realm, t *testing.T) {\n\tmb := \u0026mockBanker{failNext: true, failMsg: \"insufficient funds\"}\n\tal := newWithMock(mb, payer, chain.NewCoins(chain.NewCoin(\"ugnot\", 1000)))\n\n\t// Caller deliberately recovers from the inner panic.\n\tfunc() {\n\t\tdefer func() {\n\t\t\t_ = recover() // swallow\n\t\t}()\n\t\tal.Send(bob, chain.NewCoins(chain.NewCoin(\"ugnot\", 500)))\n\t}()\n\n\t// After the recovered panic: spent must be 0 (no funds moved →\n\t// allowance must not be charged).\n\tuassert.Equal(t, int64(0), al.Spent().AmountOf(\"ugnot\"))\n\tuassert.Equal(t, int64(1000), al.Remaining().AmountOf(\"ugnot\"))\n\n\t// And subsequent valid Send (with non-failing inner) succeeds with\n\t// full cap available.\n\turequire.NotPanics(t, cur, func() {\n\t\tal.Send(bob, chain.NewCoins(chain.NewCoin(\"ugnot\", 800)))\n\t})\n\tuassert.Equal(t, int64(800), al.Spent().AmountOf(\"ugnot\"))\n}\n\n// ---------------------------------------------------------------------\n// Empty/zero-amount edge cases\n// ---------------------------------------------------------------------\n\nfunc TestSend_EmptyAmt(cur realm, t *testing.T) {\n\tmb := \u0026mockBanker{}\n\tal := newWithMock(mb, payer, chain.NewCoins(chain.NewCoin(\"ugnot\", 1000)))\n\n\t// Sending zero coins should not panic; just no-op forwards to inner.\n\turequire.NotPanics(t, cur, func() {\n\t\tal.Send(bob, chain.Coins{})\n\t})\n\n\t// inner.SendCoins was still called (the wrapper doesn't filter\n\t// zero sends; that's the inner banker's choice). State unchanged.\n\tuassert.Equal(t, int64(0), al.Spent().AmountOf(\"ugnot\"))\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/jaekwon/allowancesender/v0\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"tM8dfLCbSMBTn7xGmNcgxujWzI9wbYNIiQ3dZdUu0dYyzOHS7yz9ww0Txu3AUKvKd3o4dqBgIpp/PA1f/xd5Tw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"avlhelpers","path":"gno.land/p/jefft0/avlhelpers/v0","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":"example_test.gno","body":"package avlhelpers\n\nimport (\n\t\"encoding/hex\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nfunc ExampleTextMatch() {\n\ttree := avl.NewTree()\n\n\t{\n\t\t// Empty tree.\n\t\tmatches := ListByteStringKeysByPrefix(tree, \"\", 10)\n\t\tprintln(ufmt.Sprintf(\"# matches: %d\", len(matches)))\n\t}\n\n\ttree.Set(\"alice\", \"\")\n\ttree.Set(\"andy\", \"\")\n\ttree.Set(\"bob\", \"\")\n\n\t{\n\t\t// Match only alice.\n\t\tmatches := ListByteStringKeysByPrefix(tree, \"al\", 10)\n\t\tprintln(ufmt.Sprintf(\"# matches: %d\", len(matches)))\n\t\tprintln(\"match: \" + matches[0])\n\t}\n\n\t{\n\t\t// Match alice and andy.\n\t\tmatches := ListByteStringKeysByPrefix(tree, \"a\", 10)\n\t\tprintln(ufmt.Sprintf(\"# matches: %d\", len(matches)))\n\t\tprintln(\"match: \" + matches[0])\n\t\tprintln(\"match: \" + matches[1])\n\t}\n\n\t{\n\t\t// Match alice and andy limited to 1.\n\t\tmatches := ListByteStringKeysByPrefix(tree, \"a\", 1)\n\t\tprintln(ufmt.Sprintf(\"# matches: %d\", len(matches)))\n\t\tprintln(\"match: \" + matches[0])\n\t}\n\n\t// Output:\n\t// # matches: 0\n\t// # matches: 1\n\t// match: alice\n\t// # matches: 2\n\t// match: alice\n\t// match: andy\n\t// # matches: 1\n\t// match: alice\n}\n\nfunc ExampleBinaryMatch() {\n\ttree := avl.NewTree()\n\ttree.Set(\"a\\xff\", \"\")\n\ttree.Set(\"a\\xff\\xff\", \"\")\n\ttree.Set(\"b\", \"\")\n\ttree.Set(\"\\xff\\xff\\x00\", \"\")\n\n\t{\n\t\t// Match only \"a\\xff\\xff\".\n\t\tmatches := ListByteStringKeysByPrefix(tree, \"a\\xff\\xff\", 10)\n\t\tprintln(ufmt.Sprintf(\"# matches: %d\", len(matches)))\n\t\tprintln(ufmt.Sprintf(\"match: %s\", hex.EncodeToString([]byte(matches[0]))))\n\t}\n\n\t{\n\t\t// Match \"a\\xff\" and \"a\\xff\\xff\".\n\t\tmatches := ListByteStringKeysByPrefix(tree, \"a\\xff\", 10)\n\t\tprintln(ufmt.Sprintf(\"# matches: %d\", len(matches)))\n\t\tprintln(ufmt.Sprintf(\"match: %s\", hex.EncodeToString([]byte(matches[0]))))\n\t\tprintln(ufmt.Sprintf(\"match: %s\", hex.EncodeToString([]byte(matches[1]))))\n\t}\n\n\t{\n\t\t// Edge case: Match only \"\\xff\\xff\\x00\".\n\t\tmatches := ListByteStringKeysByPrefix(tree, \"\\xff\\xff\", 10)\n\t\tprintln(ufmt.Sprintf(\"# matches: %d\", len(matches)))\n\t\tprintln(ufmt.Sprintf(\"match: %s\", hex.EncodeToString([]byte(matches[0]))))\n\t}\n\n\t// Output:\n\t// # matches: 1\n\t// match: 61ffff\n\t// # matches: 2\n\t// match: 61ff\n\t// match: 61ffff\n\t// # matches: 1\n\t// match: ffff00\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/jefft0/avlhelpers/v0\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"87LRNCh+eryfudiQ7ebbu/pcj1OuUmQH+lBPPIsX4msL3wDu9/jt9PE9t+oL1mgiUf9NPosN05y2MzIZL96dNQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"expect","path":"gno.land/p/jeronimoalbi/expect/v0","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":"boolean_test.gno","body":"package expect_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nfunc TestBooleanChecker(t *testing.T) {\n\tt.Run(\"to be truthy\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewBooleanChecker(ctx, true).ToBeTruthy()\n\t})\n\n\tt.Run(\"not to be truthy\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewBooleanChecker(ctx, false).Not().ToBeTruthy()\n\t})\n\n\tt.Run(\"to be falsy\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewBooleanChecker(ctx, false).ToBeFalsy()\n\t})\n\n\tt.Run(\"not to be falsy\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewBooleanChecker(ctx, true).Not().ToBeFalsy()\n\t})\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":"float_test.gno","body":"package expect_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nfunc TestFloatChecker(t *testing.T) {\n\tt.Run(\"to equal\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewFloatChecker(ctx, 1.2).ToEqual(1.2)\n\t})\n\n\tt.Run(\"not to equal\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewFloatChecker(ctx, 1.2).Not().ToEqual(3.4)\n\t})\n\n\tt.Run(\"to be greater than\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewFloatChecker(ctx, 1.2).ToBeGreaterThan(1)\n\t})\n\n\tt.Run(\"not to be greater than\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewFloatChecker(ctx, 1.2).Not().ToBeGreaterThan(1.3)\n\t})\n\n\tt.Run(\"to be greater or equal than\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewFloatChecker(ctx, 1.2).ToBeGreaterOrEqualThan(1.2)\n\t\texpect.NewFloatChecker(ctx, 1.2).ToBeGreaterOrEqualThan(1.1)\n\t})\n\n\tt.Run(\"not to be greater or equal than\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewFloatChecker(ctx, 1.2).Not().ToBeGreaterOrEqualThan(1.3)\n\t})\n\n\tt.Run(\"to be lower than\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewFloatChecker(ctx, 1.2).ToBeLowerThan(1.3)\n\t})\n\n\tt.Run(\"not to be lower than\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewFloatChecker(ctx, 1.2).Not().ToBeLowerThan(1)\n\t})\n\n\tt.Run(\"to be lower or equal than\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewFloatChecker(ctx, 1.2).ToBeLowerOrEqualThan(1.2)\n\t\texpect.NewFloatChecker(ctx, 1.2).ToBeLowerOrEqualThan(1.3)\n\t})\n\n\tt.Run(\"not to be lower or equal than\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewFloatChecker(ctx, 1.2).Not().ToBeLowerOrEqualThan(1.1)\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":"func_test.gno","body":"package expect_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nfunc TestFunction(t *testing.T) {\n\tt.Run(\"not to fail\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpect.Func(t, func() error {\n\t\t\treturn nil\n\t\t}).Not().ToFail()\n\t})\n\n\tt.Run(\"to fail\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpect.Func(t, func() error {\n\t\t\treturn errors.New(\"Foo\")\n\t\t}).ToFail()\n\t})\n\n\tt.Run(\"to fail with mesasge\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpect.Func(t, func() error {\n\t\t\treturn errors.New(\"Foo\")\n\t\t}).ToFail().WithMessage(\"Foo\")\n\t})\n\n\tt.Run(\"to fail with different message\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpect.Func(t, func() error {\n\t\t\treturn errors.New(\"Bar\")\n\t\t}).ToFail().Not().WithMessage(\"Foo\")\n\t})\n\n\tt.Run(\"to fail with error\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpect.Func(t, func() error {\n\t\t\treturn errors.New(\"Foo\")\n\t\t}).ToFail().WithError(errors.New(\"Foo\"))\n\t})\n\n\tt.Run(\"to fail with different error\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpect.Func(t, func() error {\n\t\t\treturn errors.New(\"Bar\")\n\t\t}).ToFail().Not().WithError(errors.New(\"Foo\"))\n\t})\n\n\tt.Run(\"not to panic\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpect.Func(t, func() error {\n\t\t\treturn nil\n\t\t}).Not().ToPanic()\n\t})\n\n\tt.Run(\"to panic\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpect.Func(t, func() error {\n\t\t\tpanic(\"Foo\")\n\t\t}).ToPanic()\n\t})\n\n\tt.Run(\"to panic with message\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpect.Func(t, func() error {\n\t\t\tpanic(\"Foo\")\n\t\t}).ToPanic().WithMessage(\"Foo\")\n\t})\n\n\tt.Run(\"to panich with different message\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpect.Func(t, func() error {\n\t\t\tpanic(\"Foo\")\n\t\t}).ToPanic().Not().WithMessage(\"Bar\")\n\t})\n\n\tt.Run(\"to return value\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpect.Func(t, func() any {\n\t\t\treturn \"foo\"\n\t\t}).ToReturn(\"foo\")\n\n\t\texpect.Func(t, func() (any, error) {\n\t\t\treturn \"foo\", nil\n\t\t}).ToReturn(\"foo\")\n\t})\n\n\tt.Run(\"not to return value\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpect.Func(t, func() any {\n\t\t\treturn \"foo\"\n\t\t}).Not().ToReturn(\"bar\")\n\n\t\texpect.Func(t, func() (any, error) {\n\t\t\treturn \"foo\", nil\n\t\t}).Not().ToReturn(\"bar\")\n\t})\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/jeronimoalbi/expect/v0\"\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":"int_test.gno","body":"package expect_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nfunc TestIntChecker(t *testing.T) {\n\tt.Run(\"to equal\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewIntChecker(ctx, 1).ToEqual(1)\n\t})\n\n\tt.Run(\"not to equal\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewIntChecker(ctx, 1).Not().ToEqual(2)\n\t})\n\n\tt.Run(\"to be greater than\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewIntChecker(ctx, 2).ToBeGreaterThan(1)\n\t})\n\n\tt.Run(\"not to be greater than\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewIntChecker(ctx, 1).Not().ToBeGreaterThan(2)\n\t})\n\n\tt.Run(\"to be greater or equal than\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewIntChecker(ctx, 2).ToBeGreaterOrEqualThan(2)\n\t\texpect.NewIntChecker(ctx, 2).ToBeGreaterOrEqualThan(1)\n\t})\n\n\tt.Run(\"not to be greater or equal than\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewIntChecker(ctx, 1).Not().ToBeGreaterOrEqualThan(2)\n\t})\n\n\tt.Run(\"to be lower than\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewIntChecker(ctx, 1).ToBeLowerThan(2)\n\t})\n\n\tt.Run(\"not to be lower than\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewIntChecker(ctx, 1).Not().ToBeLowerThan(1)\n\t})\n\n\tt.Run(\"to be lower or equal than\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewIntChecker(ctx, 1).ToBeLowerOrEqualThan(1)\n\t\texpect.NewIntChecker(ctx, 1).ToBeLowerOrEqualThan(2)\n\t})\n\n\tt.Run(\"not to be lower or equal than\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewIntChecker(ctx, 2).Not().ToBeLowerOrEqualThan(1)\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":"string_test.gno","body":"package expect_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nfunc TestStringChecker(t *testing.T) {\n\tt.Run(\"to equal\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewStringChecker(ctx, \"foo\").ToEqual(\"foo\")\n\t})\n\n\tt.Run(\"not to equal\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewStringChecker(ctx, \"foo\").Not().ToEqual(\"bar\")\n\t})\n\n\tt.Run(\"to be empty\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewStringChecker(ctx, \"\").ToBeEmpty()\n\t})\n\n\tt.Run(\"not to be empty\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewStringChecker(ctx, \"foo\").Not().ToBeEmpty()\n\t})\n\n\tt.Run(\"same length\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewStringChecker(ctx, \"foo\").ToHaveLength(3)\n\t})\n\n\tt.Run(\"different length\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewStringChecker(ctx, \"foo\").Not().ToHaveLength(1)\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":"uint_test.gno","body":"package expect_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nfunc TestUintChecker(t *testing.T) {\n\tt.Run(\"to equal\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewUintChecker(ctx, 1).ToEqual(1)\n\t})\n\n\tt.Run(\"not to equal\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewUintChecker(ctx, 1).Not().ToEqual(2)\n\t})\n\n\tt.Run(\"to be greater than\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewUintChecker(ctx, 2).ToBeGreaterThan(1)\n\t})\n\n\tt.Run(\"not to be greater than\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewUintChecker(ctx, 1).Not().ToBeGreaterThan(2)\n\t})\n\n\tt.Run(\"to be greater or equal than\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewUintChecker(ctx, 2).ToBeGreaterOrEqualThan(2)\n\t\texpect.NewUintChecker(ctx, 2).ToBeGreaterOrEqualThan(1)\n\t})\n\n\tt.Run(\"not to be greater or equal than\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewUintChecker(ctx, 1).Not().ToBeGreaterOrEqualThan(2)\n\t})\n\n\tt.Run(\"to be lower than\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewUintChecker(ctx, 1).ToBeLowerThan(2)\n\t})\n\n\tt.Run(\"not to be lower than\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewUintChecker(ctx, 1).Not().ToBeLowerThan(1)\n\t})\n\n\tt.Run(\"to be lower or equal than\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewUintChecker(ctx, 1).ToBeLowerOrEqualThan(1)\n\t\texpect.NewUintChecker(ctx, 1).ToBeLowerOrEqualThan(2)\n\t})\n\n\tt.Run(\"not to be lower or equal than\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := expect.NewContext(t)\n\t\texpect.NewUintChecker(ctx, 2).Not().ToBeLowerOrEqualThan(1)\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"},{"name":"value_test.gno","body":"package expect_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nfunc TestValue(t *testing.T) {\n\tt.Run(\"equal\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpect.Value(t, \"foo\").ToEqual(\"foo\")\n\t\texpect.Value(t, []byte(\"foo\")).ToEqual([]byte(\"foo\"))\n\t\texpect.Value(t, stringer(\"foo\")).ToEqual(stringer(\"foo\"))\n\t\texpect.Value(t, true).ToEqual(true)\n\t\texpect.Value(t, float32(1)).ToEqual(float32(1))\n\t\texpect.Value(t, float64(1)).ToEqual(float64(1))\n\t\texpect.Value(t, uint(1)).ToEqual(uint(1))\n\t\texpect.Value(t, uint8(1)).ToEqual(uint8(1))\n\t\texpect.Value(t, uint16(1)).ToEqual(uint16(1))\n\t\texpect.Value(t, uint32(1)).ToEqual(uint32(1))\n\t\texpect.Value(t, uint64(1)).ToEqual(uint64(1))\n\t\texpect.Value(t, int(1)).ToEqual(int(1))\n\t\texpect.Value(t, int8(1)).ToEqual(int8(1))\n\t\texpect.Value(t, int16(1)).ToEqual(int16(1))\n\t\texpect.Value(t, int32(1)).ToEqual(int32(1))\n\t\texpect.Value(t, int64(1)).ToEqual(int64(1))\n\t\texpect.Value(t, errors.New(\"foo\")).ToEqual(errors.New(\"foo\"))\n\t\texpect.Value(t, errors.New(\"foo bar\")).ToContainErrorString(\"foo\")\n\t})\n\n\tt.Run(\"not to equal\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpect.Value(t, \"foo\").Not().ToEqual(\"bar\")\n\t\texpect.Value(t, []byte(\"foo\")).Not().ToEqual([]byte(\"bar\"))\n\t\texpect.Value(t, stringer(\"foo\")).Not().ToEqual(stringer(\"bar\"))\n\t\texpect.Value(t, true).Not().ToEqual(false)\n\t\texpect.Value(t, float32(1)).Not().ToEqual(float32(2))\n\t\texpect.Value(t, float64(1)).Not().ToEqual(float64(2))\n\t\texpect.Value(t, uint(1)).Not().ToEqual(uint(2))\n\t\texpect.Value(t, uint8(1)).Not().ToEqual(uint8(2))\n\t\texpect.Value(t, uint16(1)).Not().ToEqual(uint16(2))\n\t\texpect.Value(t, uint32(1)).Not().ToEqual(uint32(2))\n\t\texpect.Value(t, uint64(1)).Not().ToEqual(uint64(2))\n\t\texpect.Value(t, int(1)).Not().ToEqual(int(2))\n\t\texpect.Value(t, int8(1)).Not().ToEqual(int8(2))\n\t\texpect.Value(t, int16(1)).Not().ToEqual(int16(2))\n\t\texpect.Value(t, int32(1)).Not().ToEqual(int32(2))\n\t\texpect.Value(t, int64(1)).Not().ToEqual(int64(2))\n\t\texpect.Value(t, errors.New(\"foo\")).Not().ToEqual(errors.New(\"bar\"))\n\t\texpect.Value(t, errors.New(\"foo\")).Not().ToContainErrorString(\"bar\")\n\t})\n\n\tt.Run(\"to be nil\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\texpect.Value(t, nil).ToBeNil()\n\t\texpect.Value(t, (*int)(nil)).ToBeNil() // typed nil\n\t})\n\n\tt.Run(\"not to be nil\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\texpect.Value(t, \"\").Not().ToBeNil()\n\t})\n\n\tt.Run(\"to be truthy\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpect.Value(t, \"true\").AsBoolean().ToBeTruthy()\n\t\texpect.Value(t, \"TRUE\").AsBoolean().ToBeTruthy()\n\t\texpect.Value(t, \"t\").AsBoolean().ToBeTruthy()\n\t\texpect.Value(t, \"1\").AsBoolean().ToBeTruthy()\n\t\texpect.Value(t, []byte(\"true\")).AsBoolean().ToBeTruthy()\n\t})\n\n\tt.Run(\"not to be truthy\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpect.Value(t, \"\").AsBoolean().Not().ToBeTruthy()\n\t\texpect.Value(t, \"false\").AsBoolean().Not().ToBeTruthy()\n\t\texpect.Value(t, \"FALSE\").AsBoolean().Not().ToBeTruthy()\n\t\texpect.Value(t, \"f\").AsBoolean().Not().ToBeTruthy()\n\t\texpect.Value(t, \"0\").AsBoolean().Not().ToBeTruthy()\n\t\texpect.Value(t, []byte(nil)).AsBoolean().Not().ToBeTruthy()\n\t\texpect.Value(t, []byte(\"false\")).AsBoolean().Not().ToBeTruthy()\n\t})\n\n\tt.Run(\"to be falsy\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpect.Value(t, \"false\").AsBoolean().ToBeFalsy()\n\t\texpect.Value(t, \"FALSE\").AsBoolean().ToBeFalsy()\n\t\texpect.Value(t, \"f\").AsBoolean().ToBeFalsy()\n\t\texpect.Value(t, \"0\").AsBoolean().ToBeFalsy()\n\t\texpect.Value(t, \"\").AsBoolean().ToBeFalsy()\n\t\texpect.Value(t, []byte(nil)).AsBoolean().ToBeFalsy()\n\t})\n\n\tt.Run(\"not to be falsy\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpect.Value(t, \"true\").AsBoolean().Not().ToBeFalsy()\n\t\texpect.Value(t, \"TRUE\").AsBoolean().Not().ToBeFalsy()\n\t\texpect.Value(t, \"t\").AsBoolean().Not().ToBeFalsy()\n\t\texpect.Value(t, \"1\").AsBoolean().Not().ToBeFalsy()\n\t\texpect.Value(t, []byte(\"true\")).AsBoolean().Not().ToBeFalsy()\n\t})\n\n\tt.Run(\"to equal stringer\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpect.Value(t, address(\"foo\")).AsString().ToEqual(\"foo\")\n\t})\n\n\tt.Run(\"not to equal stringer\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpect.Value(t, address(\"foo\")).AsString().Not().ToEqual(\"bar\")\n\t})\n}\n\ntype stringer string\n\nfunc (s stringer) String() string { return string(s) }\n"},{"name":"z_boolean_0_filetest.gno","body":"package main\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc main() {\n\texpect.Value(t, true).AsBoolean().ToEqual(false)\n\texpect.Value(t, true).AsBoolean().Not().ToEqual(true)\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected values to match\n// Got: true\n// Want: false\n// Expected values to be different\n// Got: true\n"},{"name":"z_boolean_1_filetest.gno","body":"package main\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\ntype intStringer struct{ value int }\n\nfunc (v intStringer) String() string {\n\treturn strconv.Itoa(v.value)\n}\n\nfunc main() {\n\texpect.Value(t, true).AsBoolean().ToBeFalsy()\n\texpect.Value(t, false).AsBoolean().Not().ToBeFalsy()\n\n\texpect.Value(t, \"TRUE\").AsBoolean().ToBeFalsy()\n\texpect.Value(t, \"FALSE\").AsBoolean().Not().ToBeFalsy()\n\n\texpect.Value(t, []byte(\"TRUE\")).AsBoolean().ToBeFalsy()\n\texpect.Value(t, []byte(\"FALSE\")).AsBoolean().Not().ToBeFalsy()\n\n\texpect.Value(t, intStringer{1}).AsBoolean().ToBeFalsy()\n\texpect.Value(t, intStringer{0}).AsBoolean().Not().ToBeFalsy()\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected value to be falsy\n// Expected value not to be falsy\n// Expected value to be falsy\n// Expected value not to be falsy\n// Expected value to be falsy\n// Expected value not to be falsy\n// Expected value to be falsy\n// Expected value not to be falsy\n"},{"name":"z_boolean_2_filetest.gno","body":"package main\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\ntype intStringer struct{ value int }\n\nfunc (v intStringer) String() string {\n\treturn strconv.Itoa(v.value)\n}\n\nfunc main() {\n\texpect.Value(t, false).AsBoolean().ToBeTruthy()\n\texpect.Value(t, true).AsBoolean().Not().ToBeTruthy()\n\n\texpect.Value(t, \"FALSE\").AsBoolean().ToBeTruthy()\n\texpect.Value(t, \"TRUE\").AsBoolean().Not().ToBeTruthy()\n\n\texpect.Value(t, []byte(\"FALSE\")).AsBoolean().ToBeTruthy()\n\texpect.Value(t, []byte(\"TRUE\")).AsBoolean().Not().ToBeTruthy()\n\n\texpect.Value(t, intStringer{0}).AsBoolean().ToBeTruthy()\n\texpect.Value(t, intStringer{1}).AsBoolean().Not().ToBeTruthy()\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected value to be truthy\n// Expected value not to be truthy\n// Expected value to be truthy\n// Expected value not to be truthy\n// Expected value to be truthy\n// Expected value not to be truthy\n// Expected value to be truthy\n// Expected value not to be truthy\n"},{"name":"z_error_0_filetest.gno","body":"package main\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput  strings.Builder\n\tt       = expect.MockTestingT(\u0026output)\n\ttestErr = errors.New(\"test\")\n)\n\nfunc main() {\n\texpect.Func(t, func() error {\n\t\treturn testErr\n\t}).ToFail().WithMessage(\"foo\")\n\n\texpect.Func(t, func() error {\n\t\treturn testErr\n\t}).ToFail().Not().WithMessage(\"test\")\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected error message to match\n// Got: test\n// Want: foo\n// Expected error message to be different\n// Got: test\n"},{"name":"z_error_1_filetest.gno","body":"package main\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput  strings.Builder\n\tt       = expect.MockTestingT(\u0026output)\n\ttestErr = errors.New(\"test\")\n)\n\nfunc main() {\n\texpect.Func(t, func() error {\n\t\treturn testErr\n\t}).ToFail().WithError(errors.New(\"foo\"))\n\n\texpect.Func(t, func() error {\n\t\treturn testErr\n\t}).ToFail().Not().WithError(testErr)\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected errors to match\n// Got: test\n// Want: foo\n// Expected errors to be different\n// Got: test\n"},{"name":"z_float_0_filetest.gno","body":"package main\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc main() {\n\texpect.Value(t, 1.2).AsFloat().ToEqual(1.1)\n\texpect.Value(t, 1.2).AsFloat().Not().ToEqual(1.2)\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected values to match\n// Got: 1.2\n// Want: 1.1\n// Expected value to be different\n// Got: 1.2\n"},{"name":"z_float_1_filetest.gno","body":"package main\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc main() {\n\texpect.Value(t, 1.2).AsFloat().ToBeGreaterThan(1.3)\n\texpect.Value(t, 1.2).AsFloat().Not().ToBeGreaterThan(1.1)\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected values to be gerater than 1.3\n// Got: 1.2\n// Expected value to not to be greater than 1.1\n// Got: 1.2\n"},{"name":"z_float_2_filetest.gno","body":"package main\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc main() {\n\texpect.Value(t, 1.2).AsFloat().ToBeGreaterOrEqualThan(1.3)\n\texpect.Value(t, 1.2).AsFloat().Not().ToBeGreaterOrEqualThan(1.2)\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected values to be greater or equal than 1.3\n// Got: 1.2\n// Expected value to not to be greater or equal than 1.2\n// Got: 1.2\n"},{"name":"z_float_3_filetest.gno","body":"package main\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc main() {\n\texpect.Value(t, 1.2).AsFloat().ToBeLowerThan(1.1)\n\texpect.Value(t, 1.2).AsFloat().Not().ToBeLowerThan(1.3)\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected values to be lower than 1.1\n// Got: 1.2\n// Expected value to not to be lower than 1.3\n// Got: 1.2\n"},{"name":"z_float_4_filetest.gno","body":"package main\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc main() {\n\texpect.Value(t, 1.2).AsFloat().ToBeLowerOrEqualThan(1.1)\n\texpect.Value(t, 1.2).AsFloat().Not().ToBeLowerOrEqualThan(1.2)\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected values to be lower or equal than 1.1\n// Got: 1.2\n// Expected value to not to be lower or equal than 1.2\n// Got: 1.2\n"},{"name":"z_func_0_filetest.gno","body":"package main\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc main() {\n\tgotMsg := \"Boom!\"\n\tgotErr := errors.New(gotMsg)\n\twantMsg := \"Tick Tock\"\n\twantErr := errors.New(wantMsg)\n\n\texpect.Func(t, func() error { return nil }).ToFail()\n\texpect.Func(t, func() error { return gotErr }).ToFail().WithMessage(wantMsg)\n\texpect.Func(t, func() error { return gotErr }).ToFail().WithError(wantErr)\n\n\texpect.Func(t, func() (any, error) { return nil, nil }).ToFail()\n\texpect.Func(t, func() (any, error) { return nil, gotErr }).ToFail().WithMessage(wantMsg)\n\texpect.Func(t, func() (any, error) { return nil, gotErr }).ToFail().WithError(wantErr)\n\n\texpect.Func(t, func() int { return 0 }).ToFail()\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected func to return an error\n// Expected error message to match\n// Got: Boom!\n// Want: Tick Tock\n// Expected errors to match\n// Got: Boom!\n// Want: Tick Tock\n// Expected func to return an error\n// Expected error message to match\n// Got: Boom!\n// Want: Tick Tock\n// Expected errors to match\n// Got: Boom!\n// Want: Tick Tock\n// Unsupported error func type\n// Got: unknown\n"},{"name":"z_func_1_filetest.gno","body":"package main\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc main() {\n\tmsg := \"Boom!\"\n\terr := errors.New(msg)\n\n\texpect.Func(t, func() error { return err }).Not().ToFail()\n\texpect.Func(t, func() error { return err }).ToFail().Not().WithMessage(msg)\n\texpect.Func(t, func() error { return err }).ToFail().Not().WithError(err)\n\n\texpect.Func(t, func() (any, error) { return nil, err }).Not().ToFail()\n\texpect.Func(t, func() (any, error) { return nil, err }).ToFail().Not().WithMessage(msg)\n\texpect.Func(t, func() (any, error) { return nil, err }).ToFail().Not().WithError(err)\n\n\tprintln(output.String())\n}\n\n// Output:\n// Func failed with error\n// Got: Boom!\n// Expected error message to be different\n// Got: Boom!\n// Expected errors to be different\n// Got: Boom!\n// Func failed with error\n// Got: Boom!\n// Expected error message to be different\n// Got: Boom!\n// Expected errors to be different\n// Got: Boom!\n"},{"name":"z_func_2_filetest.gno","body":"package main\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc main() {\n\tgotMsg := \"Boom!\"\n\twantMsg := \"Tick Tock\"\n\n\texpect.Func(t, func() {}).ToPanic()\n\texpect.Func(t, func() { panic(gotMsg) }).ToPanic().WithMessage(wantMsg)\n\n\texpect.Func(t, func() error { return nil }).ToPanic()\n\texpect.Func(t, func() error { panic(gotMsg) }).ToPanic().WithMessage(wantMsg)\n\n\texpect.Func(t, func() any { return nil }).ToPanic()\n\texpect.Func(t, func() any { panic(gotMsg) }).ToPanic().WithMessage(wantMsg)\n\n\texpect.Func(t, func() (any, error) { return nil, nil }).ToPanic()\n\texpect.Func(t, func() (any, error) { panic(gotMsg) }).ToPanic().WithMessage(wantMsg)\n\n\texpect.Func(t, func() int { return 0 }).ToPanic()\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected function to panic\n// Expected panic message to match\n// Got: Boom!\n// Want: Tick Tock\n// Expected function to panic\n// Expected panic message to match\n// Got: Boom!\n// Want: Tick Tock\n// Expected function to panic\n// Expected panic message to match\n// Got: Boom!\n// Want: Tick Tock\n// Expected function to panic\n// Expected panic message to match\n// Got: Boom!\n// Want: Tick Tock\n// Unsupported func type\n// Got: unknown\n"},{"name":"z_func_3_filetest.gno","body":"package main\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc main() {\n\tmsg := \"Boom!\"\n\n\texpect.Func(t, func() { panic(msg) }).Not().ToPanic()\n\texpect.Func(t, func() { panic(msg) }).ToPanic().Not().WithMessage(msg)\n\n\texpect.Func(t, func() error { panic(msg) }).Not().ToPanic()\n\texpect.Func(t, func() error { panic(msg) }).ToPanic().Not().WithMessage(msg)\n\n\texpect.Func(t, func() any { panic(msg) }).Not().ToPanic()\n\texpect.Func(t, func() any { panic(msg) }).ToPanic().Not().WithMessage(msg)\n\n\texpect.Func(t, func() (any, error) { panic(msg) }).Not().ToPanic()\n\texpect.Func(t, func() (any, error) { panic(msg) }).ToPanic().Not().WithMessage(msg)\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected func not to panic\n// Got: Boom!\n// Expected panic message to be different\n// Got: Boom!\n// Expected func not to panic\n// Got: Boom!\n// Expected panic message to be different\n// Got: Boom!\n// Expected func not to panic\n// Got: Boom!\n// Expected panic message to be different\n// Got: Boom!\n// Expected func not to panic\n// Got: Boom!\n// Expected panic message to be different\n// Got: Boom!\n"},{"name":"z_func_4_filetest.gno","body":"package main\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc main() {\n\texpect.Func(t, func() any { return \"foo\" }).ToReturn(\"bar\")\n\texpect.Func(t, func() any { return []byte(\"foo\") }).ToReturn([]byte(\"bar\"))\n\texpect.Func(t, func() any { return true }).ToReturn(false)\n\texpect.Func(t, func() any { return float32(1) }).ToReturn(float32(2))\n\texpect.Func(t, func() any { return float64(1.1) }).ToReturn(float64(1.2))\n\texpect.Func(t, func() any { return uint(1) }).ToReturn(uint(2))\n\texpect.Func(t, func() any { return uint8(1) }).ToReturn(uint8(2))\n\texpect.Func(t, func() any { return uint16(1) }).ToReturn(uint16(2))\n\texpect.Func(t, func() any { return uint32(1) }).ToReturn(uint32(2))\n\texpect.Func(t, func() any { return uint64(1) }).ToReturn(uint64(2))\n\texpect.Func(t, func() any { return int(1) }).ToReturn(int(2))\n\texpect.Func(t, func() any { return int8(1) }).ToReturn(int8(2))\n\texpect.Func(t, func() any { return int16(1) }).ToReturn(int16(2))\n\texpect.Func(t, func() any { return int32(1) }).ToReturn(int32(2))\n\texpect.Func(t, func() any { return int64(1) }).ToReturn(int64(2))\n\n\texpect.Func(t, func() (any, error) { return \"foo\", nil }).ToReturn(\"bar\")\n\texpect.Func(t, func() (any, error) { return []byte(\"foo\"), nil }).ToReturn([]byte(\"bar\"))\n\texpect.Func(t, func() (any, error) { return true, nil }).ToReturn(false)\n\texpect.Func(t, func() (any, error) { return float32(1), nil }).ToReturn(float32(2))\n\texpect.Func(t, func() (any, error) { return float64(1.1), nil }).ToReturn(float64(1.2))\n\texpect.Func(t, func() (any, error) { return uint(1), nil }).ToReturn(uint(2))\n\texpect.Func(t, func() (any, error) { return uint8(1), nil }).ToReturn(uint8(2))\n\texpect.Func(t, func() (any, error) { return uint16(1), nil }).ToReturn(uint16(2))\n\texpect.Func(t, func() (any, error) { return uint32(1), nil }).ToReturn(uint32(2))\n\texpect.Func(t, func() (any, error) { return uint64(1), nil }).ToReturn(uint64(2))\n\texpect.Func(t, func() (any, error) { return int(1), nil }).ToReturn(int(2))\n\texpect.Func(t, func() (any, error) { return int8(1), nil }).ToReturn(int8(2))\n\texpect.Func(t, func() (any, error) { return int16(1), nil }).ToReturn(int16(2))\n\texpect.Func(t, func() (any, error) { return int32(1), nil }).ToReturn(int32(2))\n\texpect.Func(t, func() (any, error) { return int64(1), nil }).ToReturn(int64(2))\n\n\texpect.Func(t, func() (any, error) { return 0, errors.New(\"Boom!\") }).ToReturn(1)\n\texpect.Func(t, func() {}).ToReturn(1)\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected values to match\n// Got: foo\n// Want: bar\n// Expected values to match\n// Got: foo\n// Want: bar\n// Expected values to match\n// Got: true\n// Want: false\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: 1.1\n// Want: 1.2\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: foo\n// Want: bar\n// Expected values to match\n// Got: foo\n// Want: bar\n// Expected values to match\n// Got: true\n// Want: false\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: 1.1\n// Want: 1.2\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: 1\n// Want: 2\n// Function returned unexpected error\n// Got: Boom!\n// Unsupported func type\n// Got: unknown\n"},{"name":"z_func_5_filetest.gno","body":"package main\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc main() {\n\texpect.Func(t, func() any { return \"foo\" }).Not().ToReturn(\"foo\")\n\texpect.Func(t, func() any { return []byte(\"foo\") }).Not().ToReturn([]byte(\"foo\"))\n\texpect.Func(t, func() any { return true }).Not().ToReturn(true)\n\texpect.Func(t, func() any { return float32(1) }).Not().ToReturn(float32(1))\n\texpect.Func(t, func() any { return float64(1.1) }).Not().ToReturn(float64(1.1))\n\texpect.Func(t, func() any { return uint(1) }).Not().ToReturn(uint(1))\n\texpect.Func(t, func() any { return uint8(1) }).Not().ToReturn(uint8(1))\n\texpect.Func(t, func() any { return uint16(1) }).Not().ToReturn(uint16(1))\n\texpect.Func(t, func() any { return uint32(1) }).Not().ToReturn(uint32(1))\n\texpect.Func(t, func() any { return uint64(1) }).Not().ToReturn(uint64(1))\n\texpect.Func(t, func() any { return int(1) }).Not().ToReturn(int(1))\n\texpect.Func(t, func() any { return int8(1) }).Not().ToReturn(int8(1))\n\texpect.Func(t, func() any { return int16(1) }).Not().ToReturn(int16(1))\n\texpect.Func(t, func() any { return int32(1) }).Not().ToReturn(int32(1))\n\texpect.Func(t, func() any { return int64(1) }).Not().ToReturn(int64(1))\n\n\texpect.Func(t, func() (any, error) { return \"foo\", nil }).Not().ToReturn(\"foo\")\n\texpect.Func(t, func() (any, error) { return []byte(\"foo\"), nil }).Not().ToReturn([]byte(\"foo\"))\n\texpect.Func(t, func() (any, error) { return true, nil }).Not().ToReturn(true)\n\texpect.Func(t, func() (any, error) { return float32(1), nil }).Not().ToReturn(float32(1))\n\texpect.Func(t, func() (any, error) { return float64(1.1), nil }).Not().ToReturn(float64(1.1))\n\texpect.Func(t, func() (any, error) { return uint(1), nil }).Not().ToReturn(uint(1))\n\texpect.Func(t, func() (any, error) { return uint8(1), nil }).Not().ToReturn(uint8(1))\n\texpect.Func(t, func() (any, error) { return uint16(1), nil }).Not().ToReturn(uint16(1))\n\texpect.Func(t, func() (any, error) { return uint32(1), nil }).Not().ToReturn(uint32(1))\n\texpect.Func(t, func() (any, error) { return uint64(1), nil }).Not().ToReturn(uint64(1))\n\texpect.Func(t, func() (any, error) { return int(1), nil }).Not().ToReturn(int(1))\n\texpect.Func(t, func() (any, error) { return int8(1), nil }).Not().ToReturn(int8(1))\n\texpect.Func(t, func() (any, error) { return int16(1), nil }).Not().ToReturn(int16(1))\n\texpect.Func(t, func() (any, error) { return int32(1), nil }).Not().ToReturn(int32(1))\n\texpect.Func(t, func() (any, error) { return int64(1), nil }).Not().ToReturn(int64(1))\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected values to be different\n// Got: foo\n// Expected values to be different\n// Got: foo\n// Expected values to be different\n// Got: true\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1.1\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1\n// Expected values to be different\n// Got: foo\n// Expected values to be different\n// Got: foo\n// Expected values to be different\n// Got: true\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1.1\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1\n"},{"name":"z_func_6_filetest.gno","body":"// PKGPATH: gno.land/r/demo/test\npackage test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nconst (\n\tcaller = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\")\n\tmsg    = \"Boom!\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc Fail(realm) {\n\tpanic(msg)\n}\n\nfunc Success(realm) {\n\t// No panic\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(caller))\n\n\texpect.Func(t, func() { Fail(cross(cur)) }).ToCrossPanic()\n\texpect.Func(t, func() { Fail(cross(cur)) }).ToCrossPanic().WithMessage(msg)\n\n\texpect.Func(t, func() error { Fail(cross(cur)); return nil }).ToCrossPanic()\n\texpect.Func(t, func() error { Fail(cross(cur)); return nil }).ToCrossPanic().WithMessage(msg)\n\n\texpect.Func(t, func() any { Fail(cross(cur)); return nil }).ToCrossPanic()\n\texpect.Func(t, func() any { Fail(cross(cur)); return nil }).ToCrossPanic().WithMessage(msg)\n\n\texpect.Func(t, func() (any, error) { Fail(cross(cur)); return nil, nil }).ToCrossPanic()\n\texpect.Func(t, func() (any, error) { Fail(cross(cur)); return nil, nil }).ToCrossPanic().WithMessage(msg)\n\n\texpect.Func(t, func() { Success(cross(cur)) }).Not().ToCrossPanic()\n\texpect.Func(t, func() error { Success(cross(cur)); return nil }).Not().ToCrossPanic()\n\texpect.Func(t, func() any { Success(cross(cur)); return nil }).Not().ToCrossPanic()\n\texpect.Func(t, func() (any, error) { Success(cross(cur)); return nil, nil }).Not().ToCrossPanic()\n\n\t// None should fail, output should be empty\n\tprint(output.String())\n}\n\n// Output:\n"},{"name":"z_func_7_filetest.gno","body":"// PKGPATH: gno.land/r/demo/test\npackage test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nconst (\n\tcaller = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\")\n\tmsg    = \"Boom!\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc Fail(realm) {\n\tpanic(msg)\n}\n\nfunc Success(realm) {\n\t// No panic\n}\n\nfunc main(cur realm) {\n\twantMsg := \"Tick Tock\"\n\n\ttesting.SetRealm(testing.NewUserRealm(caller))\n\n\texpect.Func(t, func() { Success(cross(cur)) }).ToCrossPanic()\n\texpect.Func(t, func() { Fail(cross(cur)) }).ToCrossPanic().WithMessage(wantMsg)\n\n\texpect.Func(t, func() error { Success(cross(cur)); return nil }).ToCrossPanic()\n\texpect.Func(t, func() error { Fail(cross(cur)); return nil }).ToCrossPanic().WithMessage(wantMsg)\n\n\texpect.Func(t, func() any { Success(cross(cur)); return nil }).ToCrossPanic()\n\texpect.Func(t, func() any { Fail(cross(cur)); return nil }).ToCrossPanic().WithMessage(wantMsg)\n\n\texpect.Func(t, func() (any, error) { Success(cross(cur)); return nil, nil }).ToCrossPanic()\n\texpect.Func(t, func() (any, error) { Fail(cross(cur)); return nil, nil }).ToCrossPanic().WithMessage(wantMsg)\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected function to cross panic\n// Expected cross panic message to match\n// Got: Boom!\n// Want: Tick Tock\n// Expected function to cross panic\n// Expected cross panic message to match\n// Got: Boom!\n// Want: Tick Tock\n// Expected function to cross panic\n// Expected cross panic message to match\n// Got: Boom!\n// Want: Tick Tock\n// Expected function to cross panic\n// Expected cross panic message to match\n// Got: Boom!\n// Want: Tick Tock\n"},{"name":"z_func_8_filetest.gno","body":"// PKGPATH: gno.land/r/demo/test\npackage test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nconst (\n\tcaller = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\")\n\tmsg    = \"Boom!\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc Fail(realm) {\n\tpanic(msg)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(caller))\n\n\texpect.Func(t, func() { Fail(cross(cur)) }).Not().ToCrossPanic()\n\texpect.Func(t, func() { Fail(cross(cur)) }).ToCrossPanic().Not().WithMessage(msg)\n\n\texpect.Func(t, func() error { Fail(cross(cur)); return nil }).Not().ToCrossPanic()\n\texpect.Func(t, func() error { Fail(cross(cur)); return nil }).ToCrossPanic().Not().WithMessage(msg)\n\n\texpect.Func(t, func() any { Fail(cross(cur)); return nil }).Not().ToCrossPanic()\n\texpect.Func(t, func() any { Fail(cross(cur)); return nil }).ToCrossPanic().Not().WithMessage(msg)\n\n\texpect.Func(t, func() (any, error) { Fail(cross(cur)); return nil, nil }).Not().ToCrossPanic()\n\texpect.Func(t, func() (any, error) { Fail(cross(cur)); return nil, nil }).ToCrossPanic().Not().WithMessage(msg)\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected func not to cross panic\n// Got: Boom!\n// Expected cross panic message to be different\n// Got: Boom!\n// Expected func not to cross panic\n// Got: Boom!\n// Expected cross panic message to be different\n// Got: Boom!\n// Expected func not to cross panic\n// Got: Boom!\n// Expected cross panic message to be different\n// Got: Boom!\n// Expected func not to cross panic\n// Got: Boom!\n// Expected cross panic message to be different\n// Got: Boom!\n"},{"name":"z_int_0_filetest.gno","body":"package main\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc main() {\n\texpect.Value(t, 1).AsInt().ToEqual(2)\n\texpect.Value(t, 1).AsInt().Not().ToEqual(1)\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected value to be different\n// Got: 1\n"},{"name":"z_int_1_filetest.gno","body":"package main\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc main() {\n\texpect.Value(t, 1).AsInt().ToBeGreaterThan(2)\n\texpect.Value(t, 1).AsInt().Not().ToBeGreaterThan(0)\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected values to be gerater than 2\n// Got: 1\n// Expected value to not to be greater than 0\n// Got: 1\n"},{"name":"z_int_2_filetest.gno","body":"package main\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc main() {\n\texpect.Value(t, 1).AsInt().ToBeGreaterOrEqualThan(2)\n\texpect.Value(t, 1).AsInt().Not().ToBeGreaterOrEqualThan(1)\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected values to be greater or equal than 2\n// Got: 1\n// Expected value to not to be greater or equal than 1\n// Got: 1\n"},{"name":"z_int_3_filetest.gno","body":"package main\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc main() {\n\texpect.Value(t, 1).AsInt().ToBeLowerThan(1)\n\texpect.Value(t, 1).AsInt().Not().ToBeLowerThan(2)\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected values to be lower than 1\n// Got: 1\n// Expected value to not to be lower than 2\n// Got: 1\n"},{"name":"z_int_4_filetest.gno","body":"package main\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc main() {\n\texpect.Value(t, 1).AsInt().ToBeLowerOrEqualThan(0)\n\texpect.Value(t, 1).AsInt().Not().ToBeLowerOrEqualThan(1)\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected values to be lower or equal than 0\n// Got: 1\n// Expected value to not to be lower or equal than 1\n// Got: 1\n"},{"name":"z_string_0_filetest.gno","body":"package main\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc main() {\n\texpect.Value(t, \"foo\").AsString().ToEqual(\"bar\")\n\texpect.Value(t, \"foo\").AsString().Not().ToEqual(\"foo\")\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected values to match\n// Got: foo\n// Want: bar\n// Expected values to be different\n// Got: foo\n"},{"name":"z_string_1_filetest.gno","body":"package main\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc main() {\n\texpect.Value(t, \"foo\").AsString().ToBeEmpty()\n\texpect.Value(t, \"\").AsString().Not().ToBeEmpty()\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected string to be empty\n// Got: foo\n// Unexpected empty string\n"},{"name":"z_string_2_filetest.gno","body":"package main\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc main() {\n\texpect.Value(t, \"foo\").AsString().ToHaveLength(2)\n\texpect.Value(t, \"foo\").AsString().Not().ToHaveLength(3)\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected string length to match\n// Got: 3\n// Want: 2\n// Expected string lengths to be different\n// Got: 3\n"},{"name":"z_uint_0_filetest.gno","body":"package main\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc main() {\n\texpect.Value(t, 1).AsUint().ToEqual(2)\n\texpect.Value(t, 1).AsUint().Not().ToEqual(1)\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected value to be different\n// Got: 1\n"},{"name":"z_uint_1_filetest.gno","body":"package main\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc main() {\n\texpect.Value(t, 1).AsUint().ToBeGreaterThan(2)\n\texpect.Value(t, 1).AsUint().Not().ToBeGreaterThan(0)\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected values to be gerater than 2\n// Got: 1\n// Expected value to not to be greater than 0\n// Got: 1\n"},{"name":"z_uint_2_filetest.gno","body":"package main\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc main() {\n\texpect.Value(t, 1).AsUint().ToBeGreaterOrEqualThan(2)\n\texpect.Value(t, 1).AsUint().Not().ToBeGreaterOrEqualThan(1)\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected values to be greater or equal than 2\n// Got: 1\n// Expected value to not to be greater or equal than 1\n// Got: 1\n"},{"name":"z_uint_3_filetest.gno","body":"package main\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc main() {\n\texpect.Value(t, 1).AsUint().ToBeLowerThan(1)\n\texpect.Value(t, 1).AsUint().Not().ToBeLowerThan(2)\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected values to be lower than 1\n// Got: 1\n// Expected value to not to be lower than 2\n// Got: 1\n"},{"name":"z_uint_4_filetest.gno","body":"package main\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc main() {\n\texpect.Value(t, 1).AsUint().ToBeLowerOrEqualThan(0)\n\texpect.Value(t, 1).AsUint().Not().ToBeLowerOrEqualThan(1)\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected values to be lower or equal than 0\n// Got: 1\n// Expected value to not to be lower or equal than 1\n// Got: 1\n"},{"name":"z_value_0_filetest.gno","body":"package main\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\ntype stringer string\n\nfunc (s stringer) String() string { return string(s) }\n\nfunc main() {\n\texpect.Value(t, \"foo\").ToEqual(\"bar\")\n\texpect.Value(t, []byte(\"foo\")).ToEqual([]byte(\"bar\"))\n\texpect.Value(t, stringer(\"foo\")).ToEqual(stringer(\"bar\"))\n\texpect.Value(t, true).ToEqual(false)\n\texpect.Value(t, float32(1)).ToEqual(float32(2))\n\texpect.Value(t, float64(1.1)).ToEqual(float64(1.2))\n\texpect.Value(t, uint(1)).ToEqual(uint(2))\n\texpect.Value(t, uint8(1)).ToEqual(uint8(2))\n\texpect.Value(t, uint16(1)).ToEqual(uint16(2))\n\texpect.Value(t, uint32(1)).ToEqual(uint32(2))\n\texpect.Value(t, uint64(1)).ToEqual(uint64(2))\n\texpect.Value(t, int(1)).ToEqual(int(2))\n\texpect.Value(t, int8(1)).ToEqual(int8(2))\n\texpect.Value(t, int16(1)).ToEqual(int16(2))\n\texpect.Value(t, int32(1)).ToEqual(int32(2))\n\texpect.Value(t, int64(1)).ToEqual(int64(2))\n\texpect.Value(t, errors.New(\"foo\")).ToEqual(errors.New(\"bar\"))\n\texpect.Value(t, errors.New(\"foo\")).ToContainErrorString(\"bar\")\n\n\texpect.Value(t, 0).ToEqual(errors.New(\"foo\"))\n\texpect.Value(t, 0).ToEqual([]string{})\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected values to match\n// Got: foo\n// Want: bar\n// Expected values to match\n// Got: foo\n// Want: bar\n// Expected values to match\n// Got: foo\n// Want: bar\n// Expected values to match\n// Got: true\n// Want: false\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: 1.1\n// Want: 1.2\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected values to match\n// Got: 1\n// Want: 2\n// Expected errors to match\n// Got: foo\n// Want: bar\n// Expected error message to contain: bar\n// Got: foo\n// Error is not equal to value\n// Got: foo\n// Unsupported type: unknown\n"},{"name":"z_value_1_filetest.gno","body":"package main\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\ntype stringer string\n\nfunc (s stringer) String() string { return string(s) }\n\nfunc main() {\n\texpect.Value(t, \"foo\").Not().ToEqual(\"foo\")\n\texpect.Value(t, []byte(\"foo\")).Not().ToEqual([]byte(\"foo\"))\n\texpect.Value(t, stringer(\"foo\")).Not().ToEqual(stringer(\"foo\"))\n\texpect.Value(t, true).Not().ToEqual(true)\n\texpect.Value(t, float32(1)).Not().ToEqual(float32(1))\n\texpect.Value(t, float64(1)).Not().ToEqual(float64(1))\n\texpect.Value(t, uint(1)).Not().ToEqual(uint(1))\n\texpect.Value(t, uint8(1)).Not().ToEqual(uint8(1))\n\texpect.Value(t, uint16(1)).Not().ToEqual(uint16(1))\n\texpect.Value(t, uint32(1)).Not().ToEqual(uint32(1))\n\texpect.Value(t, uint64(1)).Not().ToEqual(uint64(1))\n\texpect.Value(t, int(1)).Not().ToEqual(int(1))\n\texpect.Value(t, int8(1)).Not().ToEqual(int8(1))\n\texpect.Value(t, int16(1)).Not().ToEqual(int16(1))\n\texpect.Value(t, int32(1)).Not().ToEqual(int32(1))\n\texpect.Value(t, int64(1)).Not().ToEqual(int64(1))\n\texpect.Value(t, errors.New(\"foo\")).Not().ToEqual(errors.New(\"foo\"))\n\texpect.Value(t, errors.New(\"foo bar\")).Not().ToContainErrorString(\"bar\")\n\n\texpect.Value(t, 0).Not().ToEqual(errors.New(\"foo\"))\n\texpect.Value(t, 0).Not().ToEqual([]string{})\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected values to be different\n// Got: foo\n// Expected values to be different\n// Got: foo\n// Expected values to be different\n// Got: foo\n// Expected values to be different\n// Got: true\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1\n// Expected value to be different\n// Got: 1\n// Expected errors to be different\n// Got: foo\n// Expected error message not to contain: bar\n// Got: foo bar\n// Error is not equal to value\n// Got: foo\n// Unsupported type: unknown\n"},{"name":"z_value_2_filetest.gno","body":"package main\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc main() {\n\texpect.Value(t, \"foo\").ToBeNil()\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected value to be nil\n// Got: foo\n"},{"name":"z_value_3_filetest.gno","body":"package main\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc main() {\n\texpect.Value(t, nil).Not().ToBeNil()\n\texpect.Value(t, (*int)(nil)).Not().ToBeNil()\n\n\tprintln(output.String())\n}\n\n// Output:\n// Expected a non nil value\n// Expected a non nil value\n"},{"name":"z_value_4_filetest.gno","body":"package main\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc main() {\n\texpect.Value(t, nil).WithFailPrefix(\"Foo prefix\").Not().ToBeNil()\n\texpect.Value(t, (*int)(nil)).WithFailPrefix(\"Foo prefix\").Not().ToBeNil()\n\n\tprintln(output.String())\n}\n\n// Output:\n// Foo prefix - Expected a non nil value\n// Foo prefix - Expected a non nil value\n"},{"name":"z_value_5_filetest.gno","body":"package main\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n)\n\nvar (\n\toutput strings.Builder\n\tt      = expect.MockTestingT(\u0026output)\n)\n\nfunc main() {\n\texpect.Value(t, 1).AsString()\n\texpect.Value(t, 1).AsBoolean()\n\texpect.Value(t, 1).AsFloat()\n\texpect.Value(t, 1).AsUint()\n\texpect.Value(t, \"\").AsInt()\n\n\tprintln(output.String())\n}\n\n// Output:\n// Failed: incompatible type: expected a string value\n// Got: int\n// Failed: incompatible type: expected a boolean value\n// Got: int\n// incompatible type: expected a float value\n// Got: int\n// Failed: incompatible type: expected an int value\n// Got: string\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"UyhDHXga6RrqH0O/QFfWqSUimU9dCqEycFxrFV97++Uika+rUx+uFY1V+V3sIXk34XS891DbzKqKBGGldUDX+w=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"mdform","path":"gno.land/p/jeronimoalbi/mdform/v0","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/v0\"\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/v0\"\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"},{"name":"mdform_filetest.gno","body":"package main\n\nimport \"gno.land/p/jeronimoalbi/mdform/v0\"\n\nfunc main() {\n\tform := mdform.\n\t\tNew(\n\t\t\t\"exec\", \"FunctionName\",\n\t\t).\n\t\tInput(\n\t\t\t\"name\",\n\t\t\t\"placeholder\", \"Name\",\n\t\t\t\"value\", \"John Doe\",\n\t\t).\n\t\tSelect(\n\t\t\t\"country\",\n\t\t\t\"United States\",\n\t\t\t\"description\", \"Select your country\",\n\t\t).\n\t\tSelect(\n\t\t\t\"country\",\n\t\t\t\"Spain\",\n\t\t).\n\t\tSelect(\n\t\t\t\"country\",\n\t\t\t\"Germany\",\n\t\t).\n\t\tCheckbox(\n\t\t\t\"interests\",\n\t\t\t\"music\",\n\t\t\t\"description\", \"What do you like to do?\",\n\t\t).\n\t\tCheckbox(\n\t\t\t\"interests\",\n\t\t\t\"tech\",\n\t\t\t\"checked\", \"true\",\n\t\t)\n\tprintln(form.String())\n}\n\n// Output:\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"},{"name":"mdform_test.gno","body":"package mdform_test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/jeronimoalbi/mdform/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\n// cur is a zero-value realm used as a placeholder when forwarding to\n// uassert/urequire dispatch helpers that gained an `rlm realm` param.\n// These tests pass `func()` callbacks (no crossing inside the callback),\n// so rlm is ignored — a nil realm here is safe.\nvar cur realm\n\nfunc TestNew(cur realm, t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tattrs    []string\n\t\tmarkdown string\n\t\terr      string\n\t}{\n\t\t{\n\t\t\tname:     \"ok\",\n\t\t\tattrs:    []string{\"exec\", \"FunctionName\"},\n\t\t\tmarkdown: `\u003cgno-form exec=\"FunctionName\"\u003e\u003c/gno-form\u003e`,\n\t\t},\n\t\t{\n\t\t\tname:     \"no attributes\",\n\t\t\tmarkdown: `\u003cgno-form\u003e\u003c/gno-form\u003e`,\n\t\t},\n\t\t{\n\t\t\tname:  \"uneven attributes\",\n\t\t\tattrs: []string{\"exec\"},\n\t\t\terr:   \"expected an even number of attribute arguments\",\n\t\t},\n\t\t{\n\t\t\tname:  \"invalid attribute\",\n\t\t\tattrs: []string{\"foo\", \"\"},\n\t\t\terr:   \"invalid attribute: foo\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar markdown string\n\t\t\tfn := func() {\n\t\t\t\tf := mdform.New(tc.attrs...)\n\t\t\t\tmarkdown = strings.ReplaceAll(f.String(), \"\\n\", \"\")\n\t\t\t}\n\n\t\t\tif tc.err != \"\" {\n\t\t\t\turequire.PanicsWithMessage(t, cur, tc.err, fn, \"expect form to fail\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.NotPanics(t, cur, fn, \"expect form to be created\")\n\t\t\tuassert.Equal(t, tc.markdown, markdown, \"expected markdown to match\")\n\t\t})\n\t}\n}\n\nfunc TestFormInput(cur realm, t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tinputName string\n\t\tattrs     []string\n\t\tmarkdown  string\n\t\terr       string\n\t}{\n\t\t{\n\t\t\tname:      \"ok\",\n\t\t\tinputName: \"test\",\n\t\t\tattrs:     []string{\"value\", \"foo\"},\n\t\t\tmarkdown:  `\u003cgno-form\u003e\u003cgno-input name=\"test\" value=\"foo\" /\u003e\u003c/gno-form\u003e`,\n\t\t},\n\t\t{\n\t\t\tname:      \"no attributes\",\n\t\t\tinputName: \"test\",\n\t\t\tmarkdown:  `\u003cgno-form\u003e\u003cgno-input name=\"test\" /\u003e\u003c/gno-form\u003e`,\n\t\t},\n\t\t{\n\t\t\tname:      \"empty name\",\n\t\t\tinputName: \"  \",\n\t\t\terr:       \"form input name is required\",\n\t\t},\n\t\t{\n\t\t\tname:      \"radio type\",\n\t\t\tinputName: \"test\",\n\t\t\tattrs:     []string{\"type\", \"radio\"},\n\t\t\terr:       \"use form.Radio() to create inputs of type radio\",\n\t\t},\n\t\t{\n\t\t\tname:      \"checkbox type\",\n\t\t\tinputName: \"test\",\n\t\t\tattrs:     []string{\"type\", \"checkbox\"},\n\t\t\terr:       \"use form.Checkbox() to create inputs of type checkbox\",\n\t\t},\n\t\t{\n\t\t\tname:      \"uneven attributes\",\n\t\t\tinputName: \"test\",\n\t\t\tattrs:     []string{\"value\"},\n\t\t\terr:       \"expected an even number of attribute arguments\",\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid attribute\",\n\t\t\tinputName: \"test\",\n\t\t\tattrs:     []string{\"foo\", \"\"},\n\t\t\terr:       \"invalid attribute: foo\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar markdown string\n\t\t\tfn := func() {\n\t\t\t\tf := mdform.New()\n\t\t\t\tf.Input(tc.inputName, tc.attrs...)\n\t\t\t\tmarkdown = strings.ReplaceAll(f.String(), \"\\n\", \"\")\n\t\t\t}\n\n\t\t\tif tc.err != \"\" {\n\t\t\t\turequire.PanicsWithMessage(t, cur, tc.err, fn, \"expect form input to fail\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.NotPanics(t, cur, fn, \"expect form input to be created\")\n\t\t\tuassert.Equal(t, tc.markdown, markdown, \"expected markdown to match\")\n\t\t})\n\t}\n}\n\nfunc TestFormRadio(cur realm, t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tinputName string\n\t\tvalue     string\n\t\tattrs     []string\n\t\tmarkdown  string\n\t\terr       string\n\t}{\n\t\t{\n\t\t\tname:      \"ok\",\n\t\t\tinputName: \"test\",\n\t\t\tvalue:     \"foo\",\n\t\t\tattrs:     []string{\"readonly\", \"true\"},\n\t\t\tmarkdown:  `\u003cgno-form\u003e\u003cgno-input type=\"radio\" name=\"test\" value=\"foo\" readonly=\"true\" /\u003e\u003c/gno-form\u003e`,\n\t\t},\n\t\t{\n\t\t\tname:      \"no attributes\",\n\t\t\tinputName: \"test\",\n\t\t\tvalue:     \"foo\",\n\t\t\tmarkdown:  `\u003cgno-form\u003e\u003cgno-input type=\"radio\" name=\"test\" value=\"foo\" /\u003e\u003c/gno-form\u003e`,\n\t\t},\n\t\t{\n\t\t\tname:      \"empty name\",\n\t\t\tinputName: \"  \",\n\t\t\terr:       \"form radio input name is required\",\n\t\t},\n\t\t{\n\t\t\tname:      \"ignore type attribute\",\n\t\t\tinputName: \"test\",\n\t\t\tattrs:     []string{\"type\", \"text\"},\n\t\t\tmarkdown:  `\u003cgno-form\u003e\u003cgno-input type=\"radio\" name=\"test\" value=\"\" /\u003e\u003c/gno-form\u003e`,\n\t\t},\n\t\t{\n\t\t\tname:      \"ignore value attribute\",\n\t\t\tinputName: \"test\",\n\t\t\tattrs:     []string{\"value\", \"foo\"},\n\t\t\tmarkdown:  `\u003cgno-form\u003e\u003cgno-input type=\"radio\" name=\"test\" value=\"\" /\u003e\u003c/gno-form\u003e`,\n\t\t},\n\t\t{\n\t\t\tname:      \"uneven attributes\",\n\t\t\tinputName: \"test\",\n\t\t\tattrs:     []string{\"readonly\"},\n\t\t\terr:       \"expected an even number of attribute arguments\",\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid attribute\",\n\t\t\tinputName: \"test\",\n\t\t\tattrs:     []string{\"foo\", \"\"},\n\t\t\terr:       \"invalid attribute: foo\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar markdown string\n\t\t\tfn := func() {\n\t\t\t\tf := mdform.New()\n\t\t\t\tf.Radio(tc.inputName, tc.value, tc.attrs...)\n\t\t\t\tmarkdown = strings.ReplaceAll(f.String(), \"\\n\", \"\")\n\t\t\t}\n\n\t\t\tif tc.err != \"\" {\n\t\t\t\turequire.PanicsWithMessage(t, cur, tc.err, fn, \"expect form radio input to fail\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.NotPanics(t, cur, fn, \"expect form radio input to be created\")\n\t\t\tuassert.Equal(t, tc.markdown, markdown, \"expected markdown to match\")\n\t\t})\n\t}\n}\n\nfunc TestFormCheckbox(cur realm, t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tinputName string\n\t\tvalue     string\n\t\tattrs     []string\n\t\tmarkdown  string\n\t\terr       string\n\t}{\n\t\t{\n\t\t\tname:      \"ok\",\n\t\t\tinputName: \"test\",\n\t\t\tvalue:     \"foo\",\n\t\t\tattrs:     []string{\"readonly\", \"true\"},\n\t\t\tmarkdown:  `\u003cgno-form\u003e\u003cgno-input type=\"checkbox\" name=\"test\" value=\"foo\" readonly=\"true\" /\u003e\u003c/gno-form\u003e`,\n\t\t},\n\t\t{\n\t\t\tname:      \"no attributes\",\n\t\t\tinputName: \"test\",\n\t\t\tvalue:     \"foo\",\n\t\t\tmarkdown:  `\u003cgno-form\u003e\u003cgno-input type=\"checkbox\" name=\"test\" value=\"foo\" /\u003e\u003c/gno-form\u003e`,\n\t\t},\n\t\t{\n\t\t\tname:      \"empty name\",\n\t\t\tinputName: \"  \",\n\t\t\terr:       \"form checkbox input name is required\",\n\t\t},\n\t\t{\n\t\t\tname:      \"ignore type attribute\",\n\t\t\tinputName: \"test\",\n\t\t\tattrs:     []string{\"type\", \"text\"},\n\t\t\tmarkdown:  `\u003cgno-form\u003e\u003cgno-input type=\"checkbox\" name=\"test\" value=\"\" /\u003e\u003c/gno-form\u003e`,\n\t\t},\n\t\t{\n\t\t\tname:      \"ignore value attribute\",\n\t\t\tinputName: \"test\",\n\t\t\tattrs:     []string{\"value\", \"foo\"},\n\t\t\tmarkdown:  `\u003cgno-form\u003e\u003cgno-input type=\"checkbox\" name=\"test\" value=\"\" /\u003e\u003c/gno-form\u003e`,\n\t\t},\n\t\t{\n\t\t\tname:      \"uneven attributes\",\n\t\t\tinputName: \"test\",\n\t\t\tattrs:     []string{\"readonly\"},\n\t\t\terr:       \"expected an even number of attribute arguments\",\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid attribute\",\n\t\t\tinputName: \"test\",\n\t\t\tattrs:     []string{\"foo\", \"\"},\n\t\t\terr:       \"invalid attribute: foo\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar markdown string\n\t\t\tfn := func() {\n\t\t\t\tf := mdform.New()\n\t\t\t\tf.Checkbox(tc.inputName, tc.value, tc.attrs...)\n\t\t\t\tmarkdown = strings.ReplaceAll(f.String(), \"\\n\", \"\")\n\t\t\t}\n\n\t\t\tif tc.err != \"\" {\n\t\t\t\turequire.PanicsWithMessage(t, cur, tc.err, fn, \"expect form checkbox input to fail\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.NotPanics(t, cur, fn, \"expect form checkbox input to be created\")\n\t\t\tuassert.Equal(t, tc.markdown, markdown, \"expected markdown to match\")\n\t\t})\n\t}\n}\n\nfunc TestFormTextarea(cur realm, t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tinputName string\n\t\tattrs     []string\n\t\tmarkdown  string\n\t\terr       string\n\t}{\n\t\t{\n\t\t\tname:      \"ok\",\n\t\t\tinputName: \"test\",\n\t\t\tattrs:     []string{\"value\", \"foo\"},\n\t\t\tmarkdown:  `\u003cgno-form\u003e\u003cgno-textarea name=\"test\" value=\"foo\" /\u003e\u003c/gno-form\u003e`,\n\t\t},\n\t\t{\n\t\t\tname:      \"no attributes\",\n\t\t\tinputName: \"test\",\n\t\t\tmarkdown:  `\u003cgno-form\u003e\u003cgno-textarea name=\"test\" /\u003e\u003c/gno-form\u003e`,\n\t\t},\n\t\t{\n\t\t\tname:      \"empty name\",\n\t\t\tinputName: \"  \",\n\t\t\terr:       \"form textarea name is required\",\n\t\t},\n\t\t{\n\t\t\tname:      \"uneven attributes\",\n\t\t\tinputName: \"test\",\n\t\t\tattrs:     []string{\"value\"},\n\t\t\terr:       \"expected an even number of attribute arguments\",\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid attribute\",\n\t\t\tinputName: \"test\",\n\t\t\tattrs:     []string{\"foo\", \"\"},\n\t\t\terr:       \"invalid attribute: foo\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar markdown string\n\t\t\tfn := func() {\n\t\t\t\tf := mdform.New()\n\t\t\t\tf.Textarea(tc.inputName, tc.attrs...)\n\t\t\t\tmarkdown = strings.ReplaceAll(f.String(), \"\\n\", \"\")\n\t\t\t}\n\n\t\t\tif tc.err != \"\" {\n\t\t\t\turequire.PanicsWithMessage(t, cur, tc.err, fn, \"expect form textarea to fail\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.NotPanics(t, cur, fn, \"expect form textarea to be created\")\n\t\t\tuassert.Equal(t, tc.markdown, markdown, \"expected markdown to match\")\n\t\t})\n\t}\n}\n\nfunc TestFormSelect(cur realm, t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tinputName string\n\t\tvalue     string\n\t\tattrs     []string\n\t\tmarkdown  string\n\t\terr       string\n\t}{\n\t\t{\n\t\t\tname:      \"ok\",\n\t\t\tinputName: \"test\",\n\t\t\tvalue:     \"foo\",\n\t\t\tattrs:     []string{\"readonly\", \"true\"},\n\t\t\tmarkdown:  `\u003cgno-form\u003e\u003cgno-select name=\"test\" value=\"foo\" readonly=\"true\" /\u003e\u003c/gno-form\u003e`,\n\t\t},\n\t\t{\n\t\t\tname:      \"no attributes\",\n\t\t\tinputName: \"test\",\n\t\t\tvalue:     \"foo\",\n\t\t\tmarkdown:  `\u003cgno-form\u003e\u003cgno-select name=\"test\" value=\"foo\" /\u003e\u003c/gno-form\u003e`,\n\t\t},\n\t\t{\n\t\t\tname:      \"empty name\",\n\t\t\tinputName: \"  \",\n\t\t\terr:       \"form select name is required\",\n\t\t},\n\t\t{\n\t\t\tname:      \"uneven attributes\",\n\t\t\tinputName: \"test\",\n\t\t\tattrs:     []string{\"value\"},\n\t\t\terr:       \"expected an even number of attribute arguments\",\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid attribute\",\n\t\t\tinputName: \"test\",\n\t\t\tattrs:     []string{\"foo\", \"\"},\n\t\t\terr:       \"invalid attribute: foo\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar markdown string\n\t\t\tfn := func() {\n\t\t\t\tf := mdform.New()\n\t\t\t\tf.Select(tc.inputName, tc.value, tc.attrs...)\n\t\t\t\tmarkdown = strings.ReplaceAll(f.String(), \"\\n\", \"\")\n\t\t\t}\n\n\t\t\tif tc.err != \"\" {\n\t\t\t\turequire.PanicsWithMessage(t, cur, tc.err, fn, \"expect form textarea to fail\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.NotPanics(t, cur, fn, \"expect form textarea to be created\")\n\t\t\tuassert.Equal(t, tc.markdown, markdown, \"expected markdown to match\")\n\t\t})\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"4ikm+8/Cag9XBJhpephhGpKabpiNJRjAXGM9CyfkhHVm8TlxtC8r2iDG3/PKi3OoPBO6YBEb9d8MMsdJbY3JNQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","package":{"name":"typeutil","path":"gno.land/p/moul/typeutil/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/typeutil/v0\"\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"},{"name":"typeutil_test.gno","body":"package typeutil\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n)\n\ntype testStringer struct {\n\tvalue string\n}\n\nfunc (t testStringer) String() string {\n\treturn \"test:\" + t.value\n}\n\nfunc TestToString(t *testing.T) {\n\t// setup test data\n\tstr := \"hello\"\n\tnum := 42\n\tb := true\n\tnow := time.Now()\n\taddr := address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\tstringer := testStringer{value: \"hello\"}\n\n\ttype testCase struct {\n\t\tname     string\n\t\tinput    any\n\t\texpected string\n\t}\n\n\ttests := []testCase{\n\t\t// basic types\n\t\t{\"string\", \"hello\", \"hello\"},\n\t\t{\"empty_string\", \"\", \"\"},\n\t\t{\"nil\", nil, \"\"},\n\n\t\t// integer types\n\t\t{\"int\", 42, \"42\"},\n\t\t{\"int8\", int8(8), \"8\"},\n\t\t{\"int16\", int16(16), \"16\"},\n\t\t{\"int32\", int32(32), \"32\"},\n\t\t{\"int64\", int64(64), \"64\"},\n\t\t{\"uint\", uint(42), \"42\"},\n\t\t{\"uint8\", uint8(8), \"8\"},\n\t\t{\"uint16\", uint16(16), \"16\"},\n\t\t{\"uint32\", uint32(32), \"32\"},\n\t\t{\"uint64\", uint64(64), \"64\"},\n\n\t\t// float types\n\t\t{\"float32\", float32(3.14), \"3.14\"},\n\t\t{\"float64\", 3.14159, \"3.14159\"},\n\n\t\t// boolean\n\t\t{\"bool_true\", true, \"true\"},\n\t\t{\"bool_false\", false, \"false\"},\n\n\t\t// special types\n\t\t{\"time\", now, now.String()},\n\t\t{\"address\", addr, string(addr)},\n\t\t{\"bytes\", []byte(\"hello\"), \"hello\"},\n\t\t{\"stringer\", stringer, \"test:hello\"},\n\n\t\t// slices\n\t\t{\"empty_slice\", []string{}, \"[]\"},\n\t\t{\"string_slice\", []string{\"a\", \"b\"}, \"[a b]\"},\n\t\t{\"int_slice\", []int{1, 2}, \"[1 2]\"},\n\t\t{\"int32_slice\", []int32{1, 2}, \"[1 2]\"},\n\t\t{\"int64_slice\", []int64{1, 2}, \"[1 2]\"},\n\t\t{\"float32_slice\", []float32{1.1, 2.2}, \"[1.1 2.2]\"},\n\t\t{\"float64_slice\", []float64{1.1, 2.2}, \"[1.1 2.2]\"},\n\t\t{\"bytes_slice\", [][]byte{[]byte(\"a\"), []byte(\"b\")}, \"[a b]\"},\n\t\t{\"time_slice\", []time.Time{now, now}, \"[\" + now.String() + \" \" + now.String() + \"]\"},\n\t\t{\"address_slice\", []address{addr, addr}, \"[\" + string(addr) + \" \" + string(addr) + \"]\"},\n\t\t{\"interface_slice\", []any{1, \"a\", true}, \"[1 a true]\"},\n\n\t\t// empty slices\n\t\t{\"empty_string_slice\", []string{}, \"[]\"},\n\t\t{\"empty_int_slice\", []int{}, \"[]\"},\n\t\t{\"empty_int32_slice\", []int32{}, \"[]\"},\n\t\t{\"empty_int64_slice\", []int64{}, \"[]\"},\n\t\t{\"empty_float32_slice\", []float32{}, \"[]\"},\n\t\t{\"empty_float64_slice\", []float64{}, \"[]\"},\n\t\t{\"empty_bytes_slice\", [][]byte{}, \"[]\"},\n\t\t{\"empty_time_slice\", []time.Time{}, \"[]\"},\n\t\t{\"empty_address_slice\", []address{}, \"[]\"},\n\t\t{\"empty_interface_slice\", []any{}, \"[]\"},\n\n\t\t// maps\n\t\t{\"empty_string_map\", map[string]string{}, \"map[]\"},\n\t\t{\"string_map\", map[string]string{\"a\": \"1\", \"b\": \"2\"}, \"map[a:1 b:2]\"},\n\t\t{\"empty_interface_map\", map[string]any{}, \"map[]\"},\n\t\t{\"interface_map\", map[string]any{\"a\": 1, \"b\": \"2\"}, \"map[a:1 b:2]\"},\n\n\t\t// edge cases\n\t\t{\"empty_bytes\", []byte{}, \"\"},\n\t\t{\"nil_interface\", any(nil), \"\"},\n\t\t{\"empty_struct\", struct{}{}, \"{}\"},\n\t\t{\"unknown_type\", struct{ foo string }{}, \"\u003cunknown\u003e\"},\n\n\t\t// pointer types\n\t\t{\"nil_string_ptr\", (*string)(nil), \"\"},\n\t\t{\"string_ptr\", \u0026str, \"hello\"},\n\t\t{\"nil_int_ptr\", (*int)(nil), \"\"},\n\t\t{\"int_ptr\", \u0026num, \"42\"},\n\t\t{\"nil_bool_ptr\", (*bool)(nil), \"\"},\n\t\t{\"bool_ptr\", \u0026b, \"true\"},\n\t\t// {\"nil_time_ptr\", (*time.Time)(nil), \"\"}, // TODO: fix this\n\t\t{\"time_ptr\", \u0026now, now.String()},\n\t\t// {\"nil_address_ptr\", (*address)(nil), \"\"}, // TODO: fix this\n\t\t{\"address_ptr\", \u0026addr, string(addr)},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := ToString(tt.input)\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"%s: ToString(%v) = %q, want %q\", tt.name, tt.input, got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestToBool(t *testing.T) {\n\tstr := \"true\"\n\tnum := 42\n\tb := true\n\tnow := time.Now()\n\taddr := address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\tzero := 0\n\tempty := \"\"\n\tfalseVal := false\n\n\ttype testCase struct {\n\t\tname     string\n\t\tinput    any\n\t\texpected bool\n\t}\n\n\ttests := []testCase{\n\t\t// basic types\n\t\t{\"true\", true, true},\n\t\t{\"false\", false, false},\n\t\t{\"nil\", nil, false},\n\n\t\t// strings\n\t\t{\"empty_string\", \"\", false},\n\t\t{\"zero_string\", \"0\", false},\n\t\t{\"false_string\", \"false\", false},\n\t\t{\"f_string\", \"f\", false},\n\t\t{\"no_string\", \"no\", false},\n\t\t{\"n_string\", \"n\", false},\n\t\t{\"off_string\", \"off\", false},\n\t\t{\"space_string\", \" \", false},\n\t\t{\"true_string\", \"true\", true},\n\t\t{\"yes_string\", \"yes\", true},\n\t\t{\"random_string\", \"hello\", true},\n\n\t\t// numbers\n\t\t{\"zero_int\", 0, false},\n\t\t{\"positive_int\", 1, true},\n\t\t{\"negative_int\", -1, true},\n\t\t{\"zero_float\", 0.0, false},\n\t\t{\"positive_float\", 0.1, true},\n\t\t{\"negative_float\", -0.1, true},\n\n\t\t// special types\n\t\t{\"empty_bytes\", []byte{}, false},\n\t\t{\"non_empty_bytes\", []byte{1}, true},\n\t\t/*{\"zero_time\", time.Time{}, false},*/ // TODO: fix this\n\t\t{\"empty_address\", address(\"\"), false},\n\n\t\t// slices\n\t\t{\"empty_slice\", []string{}, false},\n\t\t{\"non_empty_slice\", []string{\"a\"}, true},\n\n\t\t// maps\n\t\t{\"empty_map\", map[string]string{}, false},\n\t\t{\"non_empty_map\", map[string]string{\"a\": \"b\"}, true},\n\n\t\t// pointer types\n\t\t{\"nil_bool_ptr\", (*bool)(nil), false},\n\t\t{\"true_ptr\", \u0026b, true},\n\t\t{\"false_ptr\", \u0026falseVal, false},\n\t\t{\"nil_string_ptr\", (*string)(nil), false},\n\t\t{\"string_ptr\", \u0026str, true},\n\t\t{\"empty_string_ptr\", \u0026empty, false},\n\t\t{\"nil_int_ptr\", (*int)(nil), false},\n\t\t{\"int_ptr\", \u0026num, true},\n\t\t{\"zero_int_ptr\", \u0026zero, false},\n\t\t// {\"nil_time_ptr\", (*time.Time)(nil), false}, // TODO: fix this\n\t\t{\"time_ptr\", \u0026now, true},\n\t\t// {\"nil_address_ptr\", (*address)(nil), false}, // TODO: fix this\n\t\t{\"address_ptr\", \u0026addr, true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := ToBool(tt.input)\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"%s: ToBool(%v) = %v, want %v\", tt.name, tt.input, got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIsZero(t *testing.T) {\n\tstr := \"hello\"\n\tnum := 42\n\tb := true\n\tnow := time.Now()\n\taddr := address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\tzero := 0\n\tempty := \"\"\n\tfalseVal := false\n\n\ttype testCase struct {\n\t\tname     string\n\t\tinput    any\n\t\texpected bool\n\t}\n\n\ttests := []testCase{\n\t\t// basic types\n\t\t{\"true\", true, false},\n\t\t{\"false\", false, true},\n\t\t{\"nil\", nil, true},\n\n\t\t// strings\n\t\t{\"empty_string\", \"\", true},\n\t\t{\"non_empty_string\", \"hello\", false},\n\n\t\t// numbers\n\t\t{\"zero_int\", 0, true},\n\t\t{\"non_zero_int\", 1, false},\n\t\t{\"zero_float\", 0.0, true},\n\t\t{\"non_zero_float\", 0.1, false},\n\n\t\t// special types\n\t\t{\"empty_bytes\", []byte{}, true},\n\t\t{\"non_empty_bytes\", []byte{1}, false},\n\t\t/*{\"zero_time\", time.Time{}, true},*/ // TODO: fix this\n\t\t{\"empty_address\", address(\"\"), true},\n\n\t\t// slices\n\t\t{\"empty_slice\", []string{}, true},\n\t\t{\"non_empty_slice\", []string{\"a\"}, false},\n\n\t\t// maps\n\t\t{\"empty_map\", map[string]string{}, true},\n\t\t{\"non_empty_map\", map[string]string{\"a\": \"b\"}, false},\n\n\t\t// pointer types\n\t\t{\"nil_bool_ptr\", (*bool)(nil), true},\n\t\t{\"false_ptr\", \u0026falseVal, true},\n\t\t{\"true_ptr\", \u0026b, false},\n\t\t{\"nil_string_ptr\", (*string)(nil), true},\n\t\t{\"empty_string_ptr\", \u0026empty, true},\n\t\t{\"string_ptr\", \u0026str, false},\n\t\t{\"nil_int_ptr\", (*int)(nil), true},\n\t\t{\"zero_int_ptr\", \u0026zero, true},\n\t\t{\"int_ptr\", \u0026num, false},\n\t\t// {\"nil_time_ptr\", (*time.Time)(nil), true}, // TODO: fix this\n\t\t{\"time_ptr\", \u0026now, false},\n\t\t// {\"nil_address_ptr\", (*address)(nil), true}, // TODO: fix this\n\t\t{\"address_ptr\", \u0026addr, false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := IsZero(tt.input)\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"%s: IsZero(%v) = %v, want %v\", tt.name, tt.input, got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestToInterfaceSlice(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    any\n\t\texpected []any\n\t\tcompare  func([]any, []any) bool\n\t}{\n\t\t{\n\t\t\tname:     \"nil\",\n\t\t\tinput:    nil,\n\t\t\texpected: nil,\n\t\t\tcompare:  compareNil,\n\t\t},\n\t\t{\n\t\t\tname:     \"empty_interface_slice\",\n\t\t\tinput:    []any{},\n\t\t\texpected: []any{},\n\t\t\tcompare:  compareEmpty,\n\t\t},\n\t\t{\n\t\t\tname:     \"interface_slice\",\n\t\t\tinput:    []any{1, \"two\", true},\n\t\t\texpected: []any{1, \"two\", true},\n\t\t\tcompare:  compareInterfaces,\n\t\t},\n\t\t{\n\t\t\tname:     \"string_slice\",\n\t\t\tinput:    []string{\"a\", \"b\", \"c\"},\n\t\t\texpected: []any{\"a\", \"b\", \"c\"},\n\t\t\tcompare:  compareStrings,\n\t\t},\n\t\t{\n\t\t\tname:     \"int_slice\",\n\t\t\tinput:    []int{1, 2, 3},\n\t\t\texpected: []any{1, 2, 3},\n\t\t\tcompare:  compareInts,\n\t\t},\n\t\t{\n\t\t\tname:     \"int32_slice\",\n\t\t\tinput:    []int32{1, 2, 3},\n\t\t\texpected: []any{int32(1), int32(2), int32(3)},\n\t\t\tcompare:  compareInt32s,\n\t\t},\n\t\t{\n\t\t\tname:     \"int64_slice\",\n\t\t\tinput:    []int64{1, 2, 3},\n\t\t\texpected: []any{int64(1), int64(2), int64(3)},\n\t\t\tcompare:  compareInt64s,\n\t\t},\n\t\t{\n\t\t\tname:     \"float32_slice\",\n\t\t\tinput:    []float32{1.1, 2.2, 3.3},\n\t\t\texpected: []any{float32(1.1), float32(2.2), float32(3.3)},\n\t\t\tcompare:  compareFloat32s,\n\t\t},\n\t\t{\n\t\t\tname:     \"float64_slice\",\n\t\t\tinput:    []float64{1.1, 2.2, 3.3},\n\t\t\texpected: []any{1.1, 2.2, 3.3},\n\t\t\tcompare:  compareFloat64s,\n\t\t},\n\t\t{\n\t\t\tname:     \"bool_slice\",\n\t\t\tinput:    []bool{true, false, true},\n\t\t\texpected: []any{true, false, true},\n\t\t\tcompare:  compareBools,\n\t\t},\n\t\t/* {\n\t\t\tname:     \"time_slice\",\n\t\t\tinput:    []time.Time{now},\n\t\t\texpected: []any{now},\n\t\t\tcompare:  compareTimes,\n\t\t}, */ // TODO: fix this\n\t\t/* {\n\t\t\tname:     \"address_slice\",\n\t\t\tinput:    []address{addr},\n\t\t\texpected: []any{addr},\n\t\t\tcompare:  compareAddresses,\n\t\t},*/ // TODO: fix this\n\t\t/* {\n\t\t\tname:     \"bytes_slice\",\n\t\t\tinput:    [][]byte{[]byte(\"hello\"), []byte(\"world\")},\n\t\t\texpected: []any{[]byte(\"hello\"), []byte(\"world\")},\n\t\t\tcompare:  compareBytes,\n\t\t},*/ // TODO: fix this\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := ToInterfaceSlice(tt.input)\n\t\t\tif !tt.compare(got, tt.expected) {\n\t\t\t\tt.Errorf(\"ToInterfaceSlice() = %v, want %v\", got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc compareNil(a, b []any) bool {\n\treturn a == nil \u0026\u0026 b == nil\n}\n\nfunc compareEmpty(a, b []any) bool {\n\treturn len(a) == 0 \u0026\u0026 len(b) == 0\n}\n\nfunc compareInterfaces(a, b []any) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i := range a {\n\t\tif a[i] != b[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc compareStrings(a, b []any) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i := range a {\n\t\tas, ok1 := a[i].(string)\n\t\tbs, ok2 := b[i].(string)\n\t\tif !ok1 || !ok2 || as != bs {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc compareInts(a, b []any) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i := range a {\n\t\tai, ok1 := a[i].(int)\n\t\tbi, ok2 := b[i].(int)\n\t\tif !ok1 || !ok2 || ai != bi {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc compareInt32s(a, b []any) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i := range a {\n\t\tai, ok1 := a[i].(int32)\n\t\tbi, ok2 := b[i].(int32)\n\t\tif !ok1 || !ok2 || ai != bi {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc compareInt64s(a, b []any) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i := range a {\n\t\tai, ok1 := a[i].(int64)\n\t\tbi, ok2 := b[i].(int64)\n\t\tif !ok1 || !ok2 || ai != bi {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc compareFloat32s(a, b []any) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i := range a {\n\t\tai, ok1 := a[i].(float32)\n\t\tbi, ok2 := b[i].(float32)\n\t\tif !ok1 || !ok2 || ai != bi {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc compareFloat64s(a, b []any) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i := range a {\n\t\tai, ok1 := a[i].(float64)\n\t\tbi, ok2 := b[i].(float64)\n\t\tif !ok1 || !ok2 || ai != bi {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc compareBools(a, b []any) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i := range a {\n\t\tab, ok1 := a[i].(bool)\n\t\tbb, ok2 := b[i].(bool)\n\t\tif !ok1 || !ok2 || ab != bb {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc compareTimes(a, b []any) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i := range a {\n\t\tat, ok1 := a[i].(time.Time)\n\t\tbt, ok2 := b[i].(time.Time)\n\t\tif !ok1 || !ok2 || !at.Equal(bt) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc compareAddresses(a, b []any) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i := range a {\n\t\taa, ok1 := a[i].(address)\n\t\tba, ok2 := b[i].(address)\n\t\tif !ok1 || !ok2 || aa != ba {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc compareBytes(a, b []any) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i := range a {\n\t\tab, ok1 := a[i].([]byte)\n\t\tbb, ok2 := b[i].([]byte)\n\t\tif !ok1 || !ok2 || string(ab) != string(bb) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// compareStringInterfaceMaps compares two map[string]any for equality\nfunc compareStringInterfaceMaps(a, b map[string]any) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor k, v1 := range a {\n\t\tv2, ok := b[k]\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\t\t// Compare values based on their type\n\t\tswitch val1 := v1.(type) {\n\t\tcase string:\n\t\t\tval2, ok := v2.(string)\n\t\t\tif !ok || val1 != val2 {\n\t\t\t\treturn false\n\t\t\t}\n\t\tcase int:\n\t\t\tval2, ok := v2.(int)\n\t\t\tif !ok || val1 != val2 {\n\t\t\t\treturn false\n\t\t\t}\n\t\tcase float64:\n\t\t\tval2, ok := v2.(float64)\n\t\t\tif !ok || val1 != val2 {\n\t\t\t\treturn false\n\t\t\t}\n\t\tcase bool:\n\t\t\tval2, ok := v2.(bool)\n\t\t\tif !ok || val1 != val2 {\n\t\t\t\treturn false\n\t\t\t}\n\t\tcase []any:\n\t\t\tval2, ok := v2.([]any)\n\t\t\tif !ok || len(val1) != len(val2) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tfor i := range val1 {\n\t\t\t\tif val1[i] != val2[i] {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t}\n\t\tcase map[string]any:\n\t\t\tval2, ok := v2.(map[string]any)\n\t\t\tif !ok || !compareStringInterfaceMaps(val1, val2) {\n\t\t\t\treturn false\n\t\t\t}\n\t\tdefault:\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc TestToMapStringInterface(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    any\n\t\texpected map[string]any\n\t\twantErr  bool\n\t}{\n\t\t{\n\t\t\tname: \"map[string]any\",\n\t\t\tinput: map[string]any{\n\t\t\t\t\"key1\": \"value1\",\n\t\t\t\t\"key2\": 42,\n\t\t\t},\n\t\t\texpected: map[string]any{\n\t\t\t\t\"key1\": \"value1\",\n\t\t\t\t\"key2\": 42,\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"map[string]string\",\n\t\t\tinput: map[string]string{\n\t\t\t\t\"key1\": \"value1\",\n\t\t\t\t\"key2\": \"value2\",\n\t\t\t},\n\t\t\texpected: map[string]any{\n\t\t\t\t\"key1\": \"value1\",\n\t\t\t\t\"key2\": \"value2\",\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"map[string]int\",\n\t\t\tinput: map[string]int{\n\t\t\t\t\"key1\": 1,\n\t\t\t\t\"key2\": 2,\n\t\t\t},\n\t\t\texpected: map[string]any{\n\t\t\t\t\"key1\": 1,\n\t\t\t\t\"key2\": 2,\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"map[string]float64\",\n\t\t\tinput: map[string]float64{\n\t\t\t\t\"key1\": 1.1,\n\t\t\t\t\"key2\": 2.2,\n\t\t\t},\n\t\t\texpected: map[string]any{\n\t\t\t\t\"key1\": 1.1,\n\t\t\t\t\"key2\": 2.2,\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"map[string]bool\",\n\t\t\tinput: map[string]bool{\n\t\t\t\t\"key1\": true,\n\t\t\t\t\"key2\": false,\n\t\t\t},\n\t\t\texpected: map[string]any{\n\t\t\t\t\"key1\": true,\n\t\t\t\t\"key2\": false,\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"map[string][]string\",\n\t\t\tinput: map[string][]string{\n\t\t\t\t\"key1\": {\"a\", \"b\"},\n\t\t\t\t\"key2\": {\"c\", \"d\"},\n\t\t\t},\n\t\t\texpected: map[string]any{\n\t\t\t\t\"key1\": []any{\"a\", \"b\"},\n\t\t\t\t\"key2\": []any{\"c\", \"d\"},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"nested map[string]map[string]string\",\n\t\t\tinput: map[string]map[string]string{\n\t\t\t\t\"key1\": {\"nested1\": \"value1\"},\n\t\t\t\t\"key2\": {\"nested2\": \"value2\"},\n\t\t\t},\n\t\t\texpected: map[string]any{\n\t\t\t\t\"key1\": map[string]any{\"nested1\": \"value1\"},\n\t\t\t\t\"key2\": map[string]any{\"nested2\": \"value2\"},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"unsupported type\",\n\t\t\tinput:    42, // not a map\n\t\t\texpected: nil,\n\t\t\twantErr:  true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := ToMapStringInterface(tt.input)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"ToMapStringInterface() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !tt.wantErr {\n\t\t\t\tif !compareStringInterfaceMaps(got, tt.expected) {\n\t\t\t\t\tt.Errorf(\"ToMapStringInterface() = %v, expected %v\", got, tt.expected)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Test error messages\nfunc TestToMapStringInterfaceErrors(t *testing.T) {\n\t_, err := ToMapStringInterface(42)\n\tif err == nil || !strings.Contains(err.Error(), \"unsupported map type\") {\n\t\tt.Errorf(\"Expected error containing 'unsupported map type', got %v\", err)\n\t}\n}\n\n// compareIntInterfaceMaps compares two map[int]any for equality\nfunc compareIntInterfaceMaps(a, b map[int]any) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor k, v1 := range a {\n\t\tv2, ok := b[k]\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\t\t// Compare values based on their type\n\t\tswitch val1 := v1.(type) {\n\t\tcase string:\n\t\t\tval2, ok := v2.(string)\n\t\t\tif !ok || val1 != val2 {\n\t\t\t\treturn false\n\t\t\t}\n\t\tcase int:\n\t\t\tval2, ok := v2.(int)\n\t\t\tif !ok || val1 != val2 {\n\t\t\t\treturn false\n\t\t\t}\n\t\tcase float64:\n\t\t\tval2, ok := v2.(float64)\n\t\t\tif !ok || val1 != val2 {\n\t\t\t\treturn false\n\t\t\t}\n\t\tcase bool:\n\t\t\tval2, ok := v2.(bool)\n\t\t\tif !ok || val1 != val2 {\n\t\t\t\treturn false\n\t\t\t}\n\t\tcase []any:\n\t\t\tval2, ok := v2.([]any)\n\t\t\tif !ok || len(val1) != len(val2) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tfor i := range val1 {\n\t\t\t\tif val1[i] != val2[i] {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t}\n\t\tcase map[string]any:\n\t\t\tval2, ok := v2.(map[string]any)\n\t\t\tif !ok || !compareStringInterfaceMaps(val1, val2) {\n\t\t\t\treturn false\n\t\t\t}\n\t\tdefault:\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc TestToMapIntInterface(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    any\n\t\texpected map[int]any\n\t\twantErr  bool\n\t}{\n\t\t{\n\t\t\tname: \"map[int]any\",\n\t\t\tinput: map[int]any{\n\t\t\t\t1: \"value1\",\n\t\t\t\t2: 42,\n\t\t\t},\n\t\t\texpected: map[int]any{\n\t\t\t\t1: \"value1\",\n\t\t\t\t2: 42,\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"map[int]string\",\n\t\t\tinput: map[int]string{\n\t\t\t\t1: \"value1\",\n\t\t\t\t2: \"value2\",\n\t\t\t},\n\t\t\texpected: map[int]any{\n\t\t\t\t1: \"value1\",\n\t\t\t\t2: \"value2\",\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"map[int]int\",\n\t\t\tinput: map[int]int{\n\t\t\t\t1: 10,\n\t\t\t\t2: 20,\n\t\t\t},\n\t\t\texpected: map[int]any{\n\t\t\t\t1: 10,\n\t\t\t\t2: 20,\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"map[int]float64\",\n\t\t\tinput: map[int]float64{\n\t\t\t\t1: 1.1,\n\t\t\t\t2: 2.2,\n\t\t\t},\n\t\t\texpected: map[int]any{\n\t\t\t\t1: 1.1,\n\t\t\t\t2: 2.2,\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"map[int]bool\",\n\t\t\tinput: map[int]bool{\n\t\t\t\t1: true,\n\t\t\t\t2: false,\n\t\t\t},\n\t\t\texpected: map[int]any{\n\t\t\t\t1: true,\n\t\t\t\t2: false,\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"map[int][]string\",\n\t\t\tinput: map[int][]string{\n\t\t\t\t1: {\"a\", \"b\"},\n\t\t\t\t2: {\"c\", \"d\"},\n\t\t\t},\n\t\t\texpected: map[int]any{\n\t\t\t\t1: []any{\"a\", \"b\"},\n\t\t\t\t2: []any{\"c\", \"d\"},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"map[int]map[string]any\",\n\t\t\tinput: map[int]map[string]any{\n\t\t\t\t1: {\"nested1\": \"value1\"},\n\t\t\t\t2: {\"nested2\": \"value2\"},\n\t\t\t},\n\t\t\texpected: map[int]any{\n\t\t\t\t1: map[string]any{\"nested1\": \"value1\"},\n\t\t\t\t2: map[string]any{\"nested2\": \"value2\"},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"unsupported type\",\n\t\t\tinput:    42, // not a map\n\t\t\texpected: nil,\n\t\t\twantErr:  true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := ToMapIntInterface(tt.input)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"ToMapIntInterface() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !tt.wantErr {\n\t\t\t\tif !compareIntInterfaceMaps(got, tt.expected) {\n\t\t\t\t\tt.Errorf(\"ToMapIntInterface() = %v, expected %v\", got, tt.expected)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestToStringSlice(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    any\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname:     \"nil input\",\n\t\t\tinput:    nil,\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n\t\t\tname:     \"empty slice\",\n\t\t\tinput:    []string{},\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname:     \"string slice\",\n\t\t\tinput:    []string{\"a\", \"b\", \"c\"},\n\t\t\texpected: []string{\"a\", \"b\", \"c\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"int slice\",\n\t\t\tinput:    []int{1, 2, 3},\n\t\t\texpected: []string{\"1\", \"2\", \"3\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"int32 slice\",\n\t\t\tinput:    []int32{1, 2, 3},\n\t\t\texpected: []string{\"1\", \"2\", \"3\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"int64 slice\",\n\t\t\tinput:    []int64{1, 2, 3},\n\t\t\texpected: []string{\"1\", \"2\", \"3\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"uint slice\",\n\t\t\tinput:    []uint{1, 2, 3},\n\t\t\texpected: []string{\"1\", \"2\", \"3\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"uint8 slice\",\n\t\t\tinput:    []uint8{1, 2, 3},\n\t\t\texpected: []string{\"1\", \"2\", \"3\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"uint16 slice\",\n\t\t\tinput:    []uint16{1, 2, 3},\n\t\t\texpected: []string{\"1\", \"2\", \"3\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"uint32 slice\",\n\t\t\tinput:    []uint32{1, 2, 3},\n\t\t\texpected: []string{\"1\", \"2\", \"3\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"uint64 slice\",\n\t\t\tinput:    []uint64{1, 2, 3},\n\t\t\texpected: []string{\"1\", \"2\", \"3\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"float32 slice\",\n\t\t\tinput:    []float32{1.1, 2.2, 3.3},\n\t\t\texpected: []string{\"1.1\", \"2.2\", \"3.3\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"float64 slice\",\n\t\t\tinput:    []float64{1.1, 2.2, 3.3},\n\t\t\texpected: []string{\"1.1\", \"2.2\", \"3.3\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"bool slice\",\n\t\t\tinput:    []bool{true, false, true},\n\t\t\texpected: []string{\"true\", \"false\", \"true\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"[]byte slice\",\n\t\t\tinput:    [][]byte{[]byte(\"hello\"), []byte(\"world\")},\n\t\t\texpected: []string{\"hello\", \"world\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"interface slice\",\n\t\t\tinput:    []any{1, \"hello\", true},\n\t\t\texpected: []string{\"1\", \"hello\", \"true\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"time slice\",\n\t\t\tinput:    []time.Time{{}, {}},\n\t\t\texpected: []string{\"0001-01-01 00:00:00 +0000 UTC\", \"0001-01-01 00:00:00 +0000 UTC\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"address slice\",\n\t\t\tinput:    []address{\"addr1\", \"addr2\"},\n\t\t\texpected: []string{\"addr1\", \"addr2\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"non-slice input\",\n\t\t\tinput:    42,\n\t\t\texpected: nil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := ToStringSlice(tt.input)\n\t\t\tif !slicesEqual(result, tt.expected) {\n\t\t\t\tt.Errorf(\"ToStringSlice(%v) = %v, want %v\", tt.input, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Helper function to compare string slices\nfunc slicesEqual(a, b []string) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i := range a {\n\t\tif a[i] != b[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc TestToStringAdvanced(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    any\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"slice with mixed basic types\",\n\t\t\tinput: []any{\n\t\t\t\t42,\n\t\t\t\t\"hello\",\n\t\t\t\ttrue,\n\t\t\t\t3.14,\n\t\t\t},\n\t\t\texpected: \"[42 hello true 3.14]\",\n\t\t},\n\t\t{\n\t\t\tname: \"map with basic types\",\n\t\t\tinput: map[string]any{\n\t\t\t\t\"int\":   42,\n\t\t\t\t\"str\":   \"hello\",\n\t\t\t\t\"bool\":  true,\n\t\t\t\t\"float\": 3.14,\n\t\t\t},\n\t\t\texpected: \"map[bool:true float:3.14 int:42 str:hello]\",\n\t\t},\n\t\t{\n\t\t\tname: \"mixed types map\",\n\t\t\tinput: map[any]any{\n\t\t\t\t42:         \"number\",\n\t\t\t\t\"string\":   123,\n\t\t\t\ttrue:       []int{1, 2, 3},\n\t\t\t\tstruct{}{}: \"empty\",\n\t\t\t},\n\t\t\texpected: \"map[42:number string:123 true:[1 2 3] {}:empty]\",\n\t\t},\n\t\t{\n\t\t\tname: \"nested maps\",\n\t\t\tinput: map[string]any{\n\t\t\t\t\"a\": map[string]int{\n\t\t\t\t\t\"x\": 1,\n\t\t\t\t\t\"y\": 2,\n\t\t\t\t},\n\t\t\t\t\"b\": []any{1, \"two\", true},\n\t\t\t},\n\t\t\texpected: \"map[a:map[x:1 y:2] b:[1 two true]]\",\n\t\t},\n\t\t{\n\t\t\tname:     \"empty struct\",\n\t\t\tinput:    struct{}{},\n\t\t\texpected: \"{}\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := ToString(tt.input)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"\\nToString(%v) =\\n%v\\nwant:\\n%v\", tt.input, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"suckpSd5+Ay+JYdzZi6k81RRTX26BIpaSrWQoR+3PyEvmbGuynA3EG82gWzfpFMu4C63eTho5fZSlXdmc2ZoRw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","package":{"name":"ulist","path":"gno.land/p/moul/ulist/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/ulist/v0\"\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"},{"name":"ulist_test.gno","body":"package ulist\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/moul/typeutil/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nfunc TestNew(t *testing.T) {\n\tl := New()\n\tuassert.Equal(t, 0, l.Size())\n\tuassert.Equal(t, 0, l.TotalSize())\n}\n\nfunc TestListAppendAndGet(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tsetup    func() *List\n\t\tindex    int\n\t\texpected any\n\t}{\n\t\t{\n\t\t\tname: \"empty list\",\n\t\t\tsetup: func() *List {\n\t\t\t\treturn New()\n\t\t\t},\n\t\t\tindex:    0,\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"single append and get\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(42)\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tindex:    0,\n\t\t\texpected: 42,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple appends and get first\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(1)\n\t\t\t\tl.Append(2)\n\t\t\t\tl.Append(3)\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tindex:    0,\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple appends and get last\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(1)\n\t\t\t\tl.Append(2)\n\t\t\t\tl.Append(3)\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tindex:    2,\n\t\t\texpected: 3,\n\t\t},\n\t\t{\n\t\t\tname: \"get with invalid index\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(1)\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tindex:    1,\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"31 items get first\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tfor i := 0; i \u003c 31; i++ {\n\t\t\t\t\tl.Append(i)\n\t\t\t\t}\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tindex:    0,\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"31 items get last\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tfor i := 0; i \u003c 31; i++ {\n\t\t\t\t\tl.Append(i)\n\t\t\t\t}\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tindex:    30,\n\t\t\texpected: 30,\n\t\t},\n\t\t{\n\t\t\tname: \"31 items get middle\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tfor i := 0; i \u003c 31; i++ {\n\t\t\t\t\tl.Append(i)\n\t\t\t\t}\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tindex:    15,\n\t\t\texpected: 15,\n\t\t},\n\t\t{\n\t\t\tname: \"values around power of 2 boundary\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tfor i := 0; i \u003c 18; i++ {\n\t\t\t\t\tl.Append(i)\n\t\t\t\t}\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tindex:    15,\n\t\t\texpected: 15,\n\t\t},\n\t\t{\n\t\t\tname: \"values at power of 2\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tfor i := 0; i \u003c 18; i++ {\n\t\t\t\t\tl.Append(i)\n\t\t\t\t}\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tindex:    16,\n\t\t\texpected: 16,\n\t\t},\n\t\t{\n\t\t\tname: \"values after power of 2\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tfor i := 0; i \u003c 18; i++ {\n\t\t\t\t\tl.Append(i)\n\t\t\t\t}\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tindex:    17,\n\t\t\texpected: 17,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tl := tt.setup()\n\t\t\tgot := l.Get(tt.index)\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"List.Get() = %v, want %v\", got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// generateSequence creates a slice of integers from 0 to n-1\nfunc generateSequence(n int) []any {\n\tresult := make([]any, n)\n\tfor i := 0; i \u003c n; i++ {\n\t\tresult[i] = i\n\t}\n\treturn result\n}\n\nfunc TestListDelete(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tsetup         func() *List\n\t\tdeleteIndices []int\n\t\texpectedErr   error\n\t\texpectedSize  int\n\t}{\n\t\t{\n\t\t\tname: \"delete single element\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(1, 2, 3)\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tdeleteIndices: []int{1},\n\t\t\texpectedErr:   nil,\n\t\t\texpectedSize:  2,\n\t\t},\n\t\t{\n\t\t\tname: \"delete multiple elements\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(1, 2, 3, 4, 5)\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tdeleteIndices: []int{0, 2, 4},\n\t\t\texpectedErr:   nil,\n\t\t\texpectedSize:  2,\n\t\t},\n\t\t{\n\t\t\tname: \"delete with negative index\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(1)\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tdeleteIndices: []int{-1},\n\t\t\texpectedErr:   ErrOutOfBounds,\n\t\t\texpectedSize:  1,\n\t\t},\n\t\t{\n\t\t\tname: \"delete beyond size\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(1)\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tdeleteIndices: []int{1},\n\t\t\texpectedErr:   ErrOutOfBounds,\n\t\t\texpectedSize:  1,\n\t\t},\n\t\t{\n\t\t\tname: \"delete already deleted element\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(1)\n\t\t\t\tl.Delete(0)\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tdeleteIndices: []int{0},\n\t\t\texpectedErr:   ErrDeleted,\n\t\t\texpectedSize:  0,\n\t\t},\n\t\t{\n\t\t\tname: \"delete multiple elements in reverse\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(1, 2, 3, 4, 5)\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tdeleteIndices: []int{4, 2, 0},\n\t\t\texpectedErr:   nil,\n\t\t\texpectedSize:  2,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tl := tt.setup()\n\t\t\tinitialSize := l.Size()\n\t\t\terr := l.Delete(tt.deleteIndices...)\n\t\t\tif err != nil \u0026\u0026 tt.expectedErr != nil {\n\t\t\t\tuassert.ErrorIs(t, err, tt.expectedErr)\n\t\t\t} else {\n\t\t\t\tuassert.Equal(t, tt.expectedErr, err)\n\t\t\t}\n\t\t\tuassert.Equal(t, tt.expectedSize, l.Size(),\n\t\t\t\tufmt.Sprintf(\"Expected size %d after deleting %d elements from size %d, got %d\",\n\t\t\t\t\ttt.expectedSize, len(tt.deleteIndices), initialSize, l.Size()))\n\t\t})\n\t}\n}\n\nfunc TestListSizeAndTotalSize(t *testing.T) {\n\tt.Run(\"empty list\", func(t *testing.T) {\n\t\tlist := New()\n\t\tuassert.Equal(t, 0, list.Size())\n\t\tuassert.Equal(t, 0, list.TotalSize())\n\t})\n\n\tt.Run(\"list with elements\", func(t *testing.T) {\n\t\tlist := New()\n\t\tlist.Append(1)\n\t\tlist.Append(2)\n\t\tlist.Append(3)\n\t\tuassert.Equal(t, 3, list.Size())\n\t\tuassert.Equal(t, 3, list.TotalSize())\n\t})\n\n\tt.Run(\"list with deleted elements\", func(t *testing.T) {\n\t\tlist := New()\n\t\tlist.Append(1)\n\t\tlist.Append(2)\n\t\tlist.Append(3)\n\t\tlist.Delete(1)\n\t\tuassert.Equal(t, 2, list.Size())\n\t\tuassert.Equal(t, 3, list.TotalSize())\n\t})\n}\n\nfunc TestIterator(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tvalues    []any\n\t\tstart     int\n\t\tend       int\n\t\texpected  []Entry\n\t\twantStop  bool\n\t\tstopAfter int // stop after N elements, -1 for no stop\n\t}{\n\t\t{\n\t\t\tname:      \"empty list\",\n\t\t\tvalues:    []any{},\n\t\t\tstart:     0,\n\t\t\tend:       10,\n\t\t\texpected:  []Entry{},\n\t\t\tstopAfter: -1,\n\t\t},\n\t\t{\n\t\t\tname:      \"nil list\",\n\t\t\tvalues:    nil,\n\t\t\tstart:     0,\n\t\t\tend:       0,\n\t\t\texpected:  []Entry{},\n\t\t\tstopAfter: -1,\n\t\t},\n\t\t{\n\t\t\tname:   \"single element forward\",\n\t\t\tvalues: []any{42},\n\t\t\tstart:  0,\n\t\t\tend:    0,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 0, Value: 42},\n\t\t\t},\n\t\t\tstopAfter: -1,\n\t\t},\n\t\t{\n\t\t\tname:   \"multiple elements forward\",\n\t\t\tvalues: []any{1, 2, 3, 4, 5},\n\t\t\tstart:  0,\n\t\t\tend:    4,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 0, Value: 1},\n\t\t\t\t{Index: 1, Value: 2},\n\t\t\t\t{Index: 2, Value: 3},\n\t\t\t\t{Index: 3, Value: 4},\n\t\t\t\t{Index: 4, Value: 5},\n\t\t\t},\n\t\t\tstopAfter: -1,\n\t\t},\n\t\t{\n\t\t\tname:   \"multiple elements reverse\",\n\t\t\tvalues: []any{1, 2, 3, 4, 5},\n\t\t\tstart:  4,\n\t\t\tend:    0,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 4, Value: 5},\n\t\t\t\t{Index: 3, Value: 4},\n\t\t\t\t{Index: 2, Value: 3},\n\t\t\t\t{Index: 1, Value: 2},\n\t\t\t\t{Index: 0, Value: 1},\n\t\t\t},\n\t\t\tstopAfter: -1,\n\t\t},\n\t\t{\n\t\t\tname:   \"partial range forward\",\n\t\t\tvalues: []any{1, 2, 3, 4, 5},\n\t\t\tstart:  1,\n\t\t\tend:    3,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 1, Value: 2},\n\t\t\t\t{Index: 2, Value: 3},\n\t\t\t\t{Index: 3, Value: 4},\n\t\t\t},\n\t\t\tstopAfter: -1,\n\t\t},\n\t\t{\n\t\t\tname:   \"partial range reverse\",\n\t\t\tvalues: []any{1, 2, 3, 4, 5},\n\t\t\tstart:  3,\n\t\t\tend:    1,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 3, Value: 4},\n\t\t\t\t{Index: 2, Value: 3},\n\t\t\t\t{Index: 1, Value: 2},\n\t\t\t},\n\t\t\tstopAfter: -1,\n\t\t},\n\t\t{\n\t\t\tname:      \"stop iteration early\",\n\t\t\tvalues:    []any{1, 2, 3, 4, 5},\n\t\t\tstart:     0,\n\t\t\tend:       4,\n\t\t\twantStop:  true,\n\t\t\tstopAfter: 2,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 0, Value: 1},\n\t\t\t\t{Index: 1, Value: 2},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"negative start\",\n\t\t\tvalues: []any{1, 2, 3},\n\t\t\tstart:  -1,\n\t\t\tend:    2,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 0, Value: 1},\n\t\t\t\t{Index: 1, Value: 2},\n\t\t\t\t{Index: 2, Value: 3},\n\t\t\t},\n\t\t\tstopAfter: -1,\n\t\t},\n\t\t{\n\t\t\tname:   \"negative end\",\n\t\t\tvalues: []any{1, 2, 3},\n\t\t\tstart:  0,\n\t\t\tend:    -2,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 0, Value: 1},\n\t\t\t},\n\t\t\tstopAfter: -1,\n\t\t},\n\t\t{\n\t\t\tname:      \"start beyond size\",\n\t\t\tvalues:    []any{1, 2, 3},\n\t\t\tstart:     5,\n\t\t\tend:       6,\n\t\t\texpected:  []Entry{},\n\t\t\tstopAfter: -1,\n\t\t},\n\t\t{\n\t\t\tname:   \"end beyond size\",\n\t\t\tvalues: []any{1, 2, 3},\n\t\t\tstart:  0,\n\t\t\tend:    5,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 0, Value: 1},\n\t\t\t\t{Index: 1, Value: 2},\n\t\t\t\t{Index: 2, Value: 3},\n\t\t\t},\n\t\t\tstopAfter: -1,\n\t\t},\n\t\t{\n\t\t\tname:   \"with deleted elements\",\n\t\t\tvalues: []any{1, 2, nil, 4, 5},\n\t\t\tstart:  0,\n\t\t\tend:    4,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 0, Value: 1},\n\t\t\t\t{Index: 1, Value: 2},\n\t\t\t\t{Index: 3, Value: 4},\n\t\t\t\t{Index: 4, Value: 5},\n\t\t\t},\n\t\t\tstopAfter: -1,\n\t\t},\n\t\t{\n\t\t\tname:   \"with deleted elements reverse\",\n\t\t\tvalues: []any{1, nil, 3, nil, 5},\n\t\t\tstart:  4,\n\t\t\tend:    0,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 4, Value: 5},\n\t\t\t\t{Index: 2, Value: 3},\n\t\t\t\t{Index: 0, Value: 1},\n\t\t\t},\n\t\t\tstopAfter: -1,\n\t\t},\n\t\t{\n\t\t\tname:      \"start equals end\",\n\t\t\tvalues:    []any{1, 2, 3},\n\t\t\tstart:     1,\n\t\t\tend:       1,\n\t\t\texpected:  []Entry{{Index: 1, Value: 2}},\n\t\t\tstopAfter: -1,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tlist := New()\n\t\t\tlist.Append(tt.values...)\n\n\t\t\tvar result []Entry\n\t\t\tstopped := list.Iterator(tt.start, tt.end, func(index int, value any) bool {\n\t\t\t\tresult = append(result, Entry{Index: index, Value: value})\n\t\t\t\treturn tt.stopAfter \u003e= 0 \u0026\u0026 len(result) \u003e= tt.stopAfter\n\t\t\t})\n\n\t\t\tuassert.Equal(t, len(result), len(tt.expected), \"comparing length\")\n\n\t\t\tfor i := range result {\n\t\t\t\tuassert.Equal(t, result[i].Index, tt.expected[i].Index, \"comparing index\")\n\t\t\t\tuassert.Equal(t, typeutil.ToString(result[i].Value), typeutil.ToString(tt.expected[i].Value), \"comparing value\")\n\t\t\t}\n\n\t\t\tuassert.Equal(t, stopped, tt.wantStop, \"comparing stopped\")\n\t\t})\n\t}\n}\n\nfunc TestLargeListAppendGetAndDelete(t *testing.T) {\n\tl := New()\n\tsize := 100\n\n\t// Append values from 0 to 99\n\tfor i := 0; i \u003c size; i++ {\n\t\tl.Append(i)\n\t\tval := l.Get(i)\n\t\tuassert.Equal(t, i, val)\n\t}\n\n\t// Verify size\n\tuassert.Equal(t, size, l.Size())\n\tuassert.Equal(t, size, l.TotalSize())\n\n\t// Get and verify each value\n\tfor i := 0; i \u003c size; i++ {\n\t\tval := l.Get(i)\n\t\tuassert.Equal(t, i, val)\n\t}\n\n\t// Get and verify each value\n\tfor i := 0; i \u003c size; i++ {\n\t\terr := l.Delete(i)\n\t\tuassert.Equal(t, nil, err)\n\t}\n\n\t// Verify size\n\tuassert.Equal(t, 0, l.Size())\n\tuassert.Equal(t, size, l.TotalSize())\n\n\t// Get and verify each value\n\tfor i := 0; i \u003c size; i++ {\n\t\tval := l.Get(i)\n\t\tuassert.Equal(t, nil, val)\n\t}\n}\n\nfunc TestEdgeCases(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\ttest func(t *testing.T)\n\t}{\n\t\t{\n\t\t\tname: \"nil list operations\",\n\t\t\ttest: func(t *testing.T) {\n\t\t\t\tvar l *List\n\t\t\t\tuassert.Equal(t, 0, l.Size())\n\t\t\t\tuassert.Equal(t, 0, l.TotalSize())\n\t\t\t\tuassert.Equal(t, nil, l.Get(0))\n\t\t\t\terr := l.Delete(0)\n\t\t\t\tuassert.ErrorIs(t, err, ErrOutOfBounds)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"delete empty indices slice\",\n\t\t\ttest: func(t *testing.T) {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(1)\n\t\t\t\terr := l.Delete()\n\t\t\t\tuassert.Equal(t, nil, err)\n\t\t\t\tuassert.Equal(t, 1, l.Size())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"append nil values\",\n\t\t\ttest: func(t *testing.T) {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(nil, nil)\n\t\t\t\tuassert.Equal(t, 2, l.Size())\n\t\t\t\tuassert.Equal(t, nil, l.Get(0))\n\t\t\t\tuassert.Equal(t, nil, l.Get(1))\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"delete same index multiple times\",\n\t\t\ttest: func(t *testing.T) {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(1, 2, 3)\n\t\t\t\terr := l.Delete(1)\n\t\t\t\tuassert.Equal(t, nil, err)\n\t\t\t\terr = l.Delete(1)\n\t\t\t\tuassert.ErrorIs(t, err, ErrDeleted)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"iterator with all deleted elements\",\n\t\t\ttest: func(t *testing.T) {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(1, 2, 3)\n\t\t\t\tl.Delete(0, 1, 2)\n\t\t\t\tvar count int\n\t\t\t\tl.Iterator(0, 2, func(index int, value any) bool {\n\t\t\t\t\tcount++\n\t\t\t\t\treturn false\n\t\t\t\t})\n\t\t\t\tuassert.Equal(t, 0, count)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"append after delete\",\n\t\t\ttest: func(t *testing.T) {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(1, 2)\n\t\t\t\tl.Delete(1)\n\t\t\t\tl.Append(3)\n\t\t\t\tuassert.Equal(t, 2, l.Size())\n\t\t\t\tuassert.Equal(t, 3, l.TotalSize())\n\t\t\t\tuassert.Equal(t, 1, l.Get(0))\n\t\t\t\tuassert.Equal(t, nil, l.Get(1))\n\t\t\t\tuassert.Equal(t, 3, l.Get(2))\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttt.test(t)\n\t\t})\n\t}\n}\n\nfunc TestIteratorByOffset(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tvalues   []any\n\t\toffset   int\n\t\tcount    int\n\t\texpected []Entry\n\t\twantStop bool\n\t}{\n\t\t{\n\t\t\tname:     \"empty list\",\n\t\t\tvalues:   []any{},\n\t\t\toffset:   0,\n\t\t\tcount:    5,\n\t\t\texpected: []Entry{},\n\t\t\twantStop: false,\n\t\t},\n\t\t{\n\t\t\tname:   \"positive count forward iteration\",\n\t\t\tvalues: []any{1, 2, 3, 4, 5},\n\t\t\toffset: 1,\n\t\t\tcount:  2,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 1, Value: 2},\n\t\t\t\t{Index: 2, Value: 3},\n\t\t\t},\n\t\t\twantStop: false,\n\t\t},\n\t\t{\n\t\t\tname:   \"negative count backward iteration\",\n\t\t\tvalues: []any{1, 2, 3, 4, 5},\n\t\t\toffset: 3,\n\t\t\tcount:  -2,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 3, Value: 4},\n\t\t\t\t{Index: 2, Value: 3},\n\t\t\t},\n\t\t\twantStop: false,\n\t\t},\n\t\t{\n\t\t\tname:   \"count exceeds available elements forward\",\n\t\t\tvalues: []any{1, 2, 3},\n\t\t\toffset: 1,\n\t\t\tcount:  5,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 1, Value: 2},\n\t\t\t\t{Index: 2, Value: 3},\n\t\t\t},\n\t\t\twantStop: false,\n\t\t},\n\t\t{\n\t\t\tname:   \"count exceeds available elements backward\",\n\t\t\tvalues: []any{1, 2, 3},\n\t\t\toffset: 1,\n\t\t\tcount:  -5,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 1, Value: 2},\n\t\t\t\t{Index: 0, Value: 1},\n\t\t\t},\n\t\t\twantStop: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"zero count\",\n\t\t\tvalues:   []any{1, 2, 3},\n\t\t\toffset:   0,\n\t\t\tcount:    0,\n\t\t\texpected: []Entry{},\n\t\t\twantStop: false,\n\t\t},\n\t\t{\n\t\t\tname:   \"negative offset\",\n\t\t\tvalues: []any{1, 2, 3},\n\t\t\toffset: -1,\n\t\t\tcount:  2,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 0, Value: 1},\n\t\t\t\t{Index: 1, Value: 2},\n\t\t\t},\n\t\t\twantStop: false,\n\t\t},\n\t\t{\n\t\t\tname:   \"offset beyond size\",\n\t\t\tvalues: []any{1, 2, 3},\n\t\t\toffset: 5,\n\t\t\tcount:  -2,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 2, Value: 3},\n\t\t\t\t{Index: 1, Value: 2},\n\t\t\t},\n\t\t\twantStop: false,\n\t\t},\n\t\t{\n\t\t\tname:   \"with deleted elements\",\n\t\t\tvalues: []any{1, nil, 3, nil, 5},\n\t\t\toffset: 0,\n\t\t\tcount:  3,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 0, Value: 1},\n\t\t\t\t{Index: 2, Value: 3},\n\t\t\t\t{Index: 4, Value: 5},\n\t\t\t},\n\t\t\twantStop: false,\n\t\t},\n\t\t{\n\t\t\tname:   \"early stop in forward iteration\",\n\t\t\tvalues: []any{1, 2, 3, 4, 5},\n\t\t\toffset: 0,\n\t\t\tcount:  5,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 0, Value: 1},\n\t\t\t\t{Index: 1, Value: 2},\n\t\t\t},\n\t\t\twantStop: true, // The callback will return true after 2 elements\n\t\t},\n\t\t{\n\t\t\tname:   \"early stop in backward iteration\",\n\t\t\tvalues: []any{1, 2, 3, 4, 5},\n\t\t\toffset: 4,\n\t\t\tcount:  -5,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 4, Value: 5},\n\t\t\t\t{Index: 3, Value: 4},\n\t\t\t},\n\t\t\twantStop: true, // The callback will return true after 2 elements\n\t\t},\n\t\t{\n\t\t\tname:     \"nil list\",\n\t\t\tvalues:   nil,\n\t\t\toffset:   0,\n\t\t\tcount:    5,\n\t\t\texpected: []Entry{},\n\t\t\twantStop: false,\n\t\t},\n\t\t{\n\t\t\tname:   \"single element forward\",\n\t\t\tvalues: []any{1},\n\t\t\toffset: 0,\n\t\t\tcount:  5,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 0, Value: 1},\n\t\t\t},\n\t\t\twantStop: false,\n\t\t},\n\t\t{\n\t\t\tname:   \"single element backward\",\n\t\t\tvalues: []any{1},\n\t\t\toffset: 0,\n\t\t\tcount:  -5,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 0, Value: 1},\n\t\t\t},\n\t\t\twantStop: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"all deleted elements\",\n\t\t\tvalues:   []any{nil, nil, nil},\n\t\t\toffset:   0,\n\t\t\tcount:    3,\n\t\t\texpected: []Entry{},\n\t\t\twantStop: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tlist := New()\n\t\t\tlist.Append(tt.values...)\n\n\t\t\tvar result []Entry\n\t\t\tvar cb IterCbFn\n\t\t\tif tt.wantStop {\n\t\t\t\tcb = func(index int, value any) bool {\n\t\t\t\t\tresult = append(result, Entry{Index: index, Value: value})\n\t\t\t\t\treturn len(result) \u003e= 2 // Stop after 2 elements for early stop tests\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tcb = func(index int, value any) bool {\n\t\t\t\t\tresult = append(result, Entry{Index: index, Value: value})\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tstopped := list.IteratorByOffset(tt.offset, tt.count, cb)\n\n\t\t\tuassert.Equal(t, len(tt.expected), len(result), \"comparing length\")\n\t\t\tfor i := range result {\n\t\t\t\tuassert.Equal(t, tt.expected[i].Index, result[i].Index, \"comparing index\")\n\t\t\t\tuassert.Equal(t, typeutil.ToString(tt.expected[i].Value), typeutil.ToString(result[i].Value), \"comparing value\")\n\t\t\t}\n\t\t\tuassert.Equal(t, tt.wantStop, stopped, \"comparing stopped\")\n\t\t})\n\t}\n}\n\nfunc TestMustDelete(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tsetup       func() *List\n\t\tindices     []int\n\t\tshouldPanic bool\n\t\tpanicMsg    string\n\t}{\n\t\t{\n\t\t\tname: \"successful delete\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(1, 2, 3)\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tindices:     []int{1},\n\t\t\tshouldPanic: false,\n\t\t},\n\t\t{\n\t\t\tname: \"out of bounds\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(1)\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tindices:     []int{1},\n\t\t\tshouldPanic: true,\n\t\t\tpanicMsg:    ErrOutOfBounds.Error(),\n\t\t},\n\t\t{\n\t\t\tname: \"already deleted\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(1)\n\t\t\t\tl.Delete(0)\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tindices:     []int{0},\n\t\t\tshouldPanic: true,\n\t\t\tpanicMsg:    ErrDeleted.Error(),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tl := tt.setup()\n\t\t\tif tt.shouldPanic {\n\t\t\t\tdefer func() {\n\t\t\t\t\tr := recover()\n\t\t\t\t\tif r == nil {\n\t\t\t\t\t\tt.Error(\"Expected panic but got none\")\n\t\t\t\t\t}\n\t\t\t\t\terr, ok := r.(error)\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\tt.Errorf(\"Expected error but got %v\", r)\n\t\t\t\t\t}\n\t\t\t\t\tuassert.Equal(t, tt.panicMsg, err.Error())\n\t\t\t\t}()\n\t\t\t}\n\t\t\tl.MustDelete(tt.indices...)\n\t\t\tif tt.shouldPanic {\n\t\t\t\tt.Error(\"Expected panic\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMustGet(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tsetup       func() *List\n\t\tindex       int\n\t\texpected    any\n\t\tshouldPanic bool\n\t\tpanicMsg    string\n\t}{\n\t\t{\n\t\t\tname: \"successful get\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(42)\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tindex:       0,\n\t\t\texpected:    42,\n\t\t\tshouldPanic: false,\n\t\t},\n\t\t{\n\t\t\tname: \"out of bounds negative\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(1)\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tindex:       -1,\n\t\t\tshouldPanic: true,\n\t\t\tpanicMsg:    ErrOutOfBounds.Error(),\n\t\t},\n\t\t{\n\t\t\tname: \"out of bounds positive\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(1)\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tindex:       1,\n\t\t\tshouldPanic: true,\n\t\t\tpanicMsg:    ErrOutOfBounds.Error(),\n\t\t},\n\t\t{\n\t\t\tname: \"deleted element\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(1)\n\t\t\t\tl.Delete(0)\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tindex:       0,\n\t\t\tshouldPanic: true,\n\t\t\tpanicMsg:    ErrDeleted.Error(),\n\t\t},\n\t\t{\n\t\t\tname: \"nil list\",\n\t\t\tsetup: func() *List {\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tindex:       0,\n\t\t\tshouldPanic: true,\n\t\t\tpanicMsg:    ErrOutOfBounds.Error(),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tl := tt.setup()\n\t\t\tif tt.shouldPanic {\n\t\t\t\tdefer func() {\n\t\t\t\t\tr := recover()\n\t\t\t\t\tif r == nil {\n\t\t\t\t\t\tt.Error(\"Expected panic but got none\")\n\t\t\t\t\t}\n\t\t\t\t\terr, ok := r.(error)\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\tt.Errorf(\"Expected error but got %v\", r)\n\t\t\t\t\t}\n\t\t\t\t\tuassert.Equal(t, tt.panicMsg, err.Error())\n\t\t\t\t}()\n\t\t\t}\n\t\t\tresult := l.MustGet(tt.index)\n\t\t\tif tt.shouldPanic {\n\t\t\t\tt.Error(\"Expected panic\")\n\t\t\t}\n\t\t\tuassert.Equal(t, typeutil.ToString(tt.expected), typeutil.ToString(result))\n\t\t})\n\t}\n}\n\nfunc TestGetRange(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tvalues   []any\n\t\tstart    int\n\t\tend      int\n\t\texpected []Entry\n\t}{\n\t\t{\n\t\t\tname:     \"empty list\",\n\t\t\tvalues:   []any{},\n\t\t\tstart:    0,\n\t\t\tend:      10,\n\t\t\texpected: []Entry{},\n\t\t},\n\t\t{\n\t\t\tname:   \"single element\",\n\t\t\tvalues: []any{42},\n\t\t\tstart:  0,\n\t\t\tend:    0,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 0, Value: 42},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"multiple elements forward\",\n\t\t\tvalues: []any{1, 2, 3, 4, 5},\n\t\t\tstart:  1,\n\t\t\tend:    3,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 1, Value: 2},\n\t\t\t\t{Index: 2, Value: 3},\n\t\t\t\t{Index: 3, Value: 4},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"multiple elements reverse\",\n\t\t\tvalues: []any{1, 2, 3, 4, 5},\n\t\t\tstart:  3,\n\t\t\tend:    1,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 3, Value: 4},\n\t\t\t\t{Index: 2, Value: 3},\n\t\t\t\t{Index: 1, Value: 2},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"with deleted elements\",\n\t\t\tvalues: []any{1, nil, 3, nil, 5},\n\t\t\tstart:  0,\n\t\t\tend:    4,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 0, Value: 1},\n\t\t\t\t{Index: 2, Value: 3},\n\t\t\t\t{Index: 4, Value: 5},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"nil list\",\n\t\t\tvalues:   nil,\n\t\t\tstart:    0,\n\t\t\tend:      5,\n\t\t\texpected: []Entry{},\n\t\t},\n\t\t{\n\t\t\tname:     \"negative indices\",\n\t\t\tvalues:   []any{1, 2, 3},\n\t\t\tstart:    -1,\n\t\t\tend:      -2,\n\t\t\texpected: []Entry{},\n\t\t},\n\t\t{\n\t\t\tname:   \"indices beyond size\",\n\t\t\tvalues: []any{1, 2, 3},\n\t\t\tstart:  1,\n\t\t\tend:    5,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 1, Value: 2},\n\t\t\t\t{Index: 2, Value: 3},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tlist := New()\n\t\t\tlist.Append(tt.values...)\n\n\t\t\tresult := list.GetRange(tt.start, tt.end)\n\n\t\t\tuassert.Equal(t, len(tt.expected), len(result), \"comparing length\")\n\t\t\tfor i := range result {\n\t\t\t\tuassert.Equal(t, tt.expected[i].Index, result[i].Index, \"comparing index\")\n\t\t\t\tuassert.Equal(t, typeutil.ToString(tt.expected[i].Value), typeutil.ToString(result[i].Value), \"comparing value\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetByOffset(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tvalues   []any\n\t\toffset   int\n\t\tcount    int\n\t\texpected []Entry\n\t}{\n\t\t{\n\t\t\tname:     \"empty list\",\n\t\t\tvalues:   []any{},\n\t\t\toffset:   0,\n\t\t\tcount:    5,\n\t\t\texpected: []Entry{},\n\t\t},\n\t\t{\n\t\t\tname:   \"positive count forward\",\n\t\t\tvalues: []any{1, 2, 3, 4, 5},\n\t\t\toffset: 1,\n\t\t\tcount:  2,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 1, Value: 2},\n\t\t\t\t{Index: 2, Value: 3},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"negative count backward\",\n\t\t\tvalues: []any{1, 2, 3, 4, 5},\n\t\t\toffset: 3,\n\t\t\tcount:  -2,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 3, Value: 4},\n\t\t\t\t{Index: 2, Value: 3},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"count exceeds available elements\",\n\t\t\tvalues: []any{1, 2, 3},\n\t\t\toffset: 1,\n\t\t\tcount:  5,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 1, Value: 2},\n\t\t\t\t{Index: 2, Value: 3},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"zero count\",\n\t\t\tvalues:   []any{1, 2, 3},\n\t\t\toffset:   0,\n\t\t\tcount:    0,\n\t\t\texpected: []Entry{},\n\t\t},\n\t\t{\n\t\t\tname:   \"with deleted elements\",\n\t\t\tvalues: []any{1, nil, 3, nil, 5},\n\t\t\toffset: 0,\n\t\t\tcount:  3,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 0, Value: 1},\n\t\t\t\t{Index: 2, Value: 3},\n\t\t\t\t{Index: 4, Value: 5},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"negative offset\",\n\t\t\tvalues: []any{1, 2, 3},\n\t\t\toffset: -1,\n\t\t\tcount:  2,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 0, Value: 1},\n\t\t\t\t{Index: 1, Value: 2},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"offset beyond size\",\n\t\t\tvalues: []any{1, 2, 3},\n\t\t\toffset: 5,\n\t\t\tcount:  -2,\n\t\t\texpected: []Entry{\n\t\t\t\t{Index: 2, Value: 3},\n\t\t\t\t{Index: 1, Value: 2},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"nil list\",\n\t\t\tvalues:   nil,\n\t\t\toffset:   0,\n\t\t\tcount:    5,\n\t\t\texpected: []Entry{},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tlist := New()\n\t\t\tlist.Append(tt.values...)\n\n\t\t\tresult := list.GetByOffset(tt.offset, tt.count)\n\n\t\t\tuassert.Equal(t, len(tt.expected), len(result), \"comparing length\")\n\t\t\tfor i := range result {\n\t\t\t\tuassert.Equal(t, tt.expected[i].Index, result[i].Index, \"comparing index\")\n\t\t\t\tuassert.Equal(t, typeutil.ToString(tt.expected[i].Value), typeutil.ToString(result[i].Value), \"comparing value\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMustSet(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tsetup       func() *List\n\t\tindex       int\n\t\tvalue       any\n\t\tshouldPanic bool\n\t\tpanicMsg    string\n\t}{\n\t\t{\n\t\t\tname: \"successful set\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(42)\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tindex:       0,\n\t\t\tvalue:       99,\n\t\t\tshouldPanic: false,\n\t\t},\n\t\t{\n\t\t\tname: \"restore deleted element\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(42)\n\t\t\t\tl.Delete(0)\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tindex:       0,\n\t\t\tvalue:       99,\n\t\t\tshouldPanic: false,\n\t\t},\n\t\t{\n\t\t\tname: \"out of bounds negative\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(1)\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tindex:       -1,\n\t\t\tvalue:       99,\n\t\t\tshouldPanic: true,\n\t\t\tpanicMsg:    ErrOutOfBounds.Error(),\n\t\t},\n\t\t{\n\t\t\tname: \"out of bounds positive\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(1)\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tindex:       1,\n\t\t\tvalue:       99,\n\t\t\tshouldPanic: true,\n\t\t\tpanicMsg:    ErrOutOfBounds.Error(),\n\t\t},\n\t\t{\n\t\t\tname: \"nil list\",\n\t\t\tsetup: func() *List {\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tindex:       0,\n\t\t\tvalue:       99,\n\t\t\tshouldPanic: true,\n\t\t\tpanicMsg:    ErrOutOfBounds.Error(),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tl := tt.setup()\n\t\t\tif tt.shouldPanic {\n\t\t\t\tdefer func() {\n\t\t\t\t\tr := recover()\n\t\t\t\t\tif r == nil {\n\t\t\t\t\t\tt.Error(\"Expected panic but got none\")\n\t\t\t\t\t}\n\t\t\t\t\terr, ok := r.(error)\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\tt.Errorf(\"Expected error but got %v\", r)\n\t\t\t\t\t}\n\t\t\t\t\tuassert.Equal(t, tt.panicMsg, err.Error())\n\t\t\t\t}()\n\t\t\t}\n\t\t\tl.MustSet(tt.index, tt.value)\n\t\t\tif tt.shouldPanic {\n\t\t\t\tt.Error(\"Expected panic\")\n\t\t\t}\n\t\t\t// Verify the value was set correctly for non-panic cases\n\t\t\tif !tt.shouldPanic {\n\t\t\t\tresult := l.Get(tt.index)\n\t\t\t\tuassert.Equal(t, typeutil.ToString(tt.value), typeutil.ToString(result))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSet(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tsetup       func() *List\n\t\tindex       int\n\t\tvalue       any\n\t\texpectedErr error\n\t\tverify      func(t *testing.T, l *List)\n\t}{\n\t\t{\n\t\t\tname: \"set value in empty list\",\n\t\t\tsetup: func() *List {\n\t\t\t\treturn New()\n\t\t\t},\n\t\t\tindex:       0,\n\t\t\tvalue:       42,\n\t\t\texpectedErr: ErrOutOfBounds,\n\t\t\tverify: func(t *testing.T, l *List) {\n\t\t\t\tuassert.Equal(t, 0, l.Size())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set value at valid index\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(1)\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tindex: 0,\n\t\t\tvalue: 42,\n\t\t\tverify: func(t *testing.T, l *List) {\n\t\t\t\tuassert.Equal(t, 42, l.Get(0))\n\t\t\t\tuassert.Equal(t, 1, l.Size())\n\t\t\t\tuassert.Equal(t, 1, l.TotalSize())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set value at negative index\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(1)\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tindex:       -1,\n\t\t\tvalue:       42,\n\t\t\texpectedErr: ErrOutOfBounds,\n\t\t\tverify: func(t *testing.T, l *List) {\n\t\t\t\tuassert.Equal(t, 1, l.Get(0))\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set value beyond size\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(1)\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tindex:       1,\n\t\t\tvalue:       42,\n\t\t\texpectedErr: ErrOutOfBounds,\n\t\t\tverify: func(t *testing.T, l *List) {\n\t\t\t\tuassert.Equal(t, 1, l.Get(0))\n\t\t\t\tuassert.Equal(t, 1, l.Size())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set nil value\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(1)\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tindex: 0,\n\t\t\tvalue: nil,\n\t\t\tverify: func(t *testing.T, l *List) {\n\t\t\t\tuassert.Equal(t, nil, l.Get(0))\n\t\t\t\tuassert.Equal(t, 0, l.Size())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set value at deleted index\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(1, 2, 3)\n\t\t\t\tl.Delete(1)\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tindex: 1,\n\t\t\tvalue: 42,\n\t\t\tverify: func(t *testing.T, l *List) {\n\t\t\t\tuassert.Equal(t, 42, l.Get(1))\n\t\t\t\tuassert.Equal(t, 3, l.Size())\n\t\t\t\tuassert.Equal(t, 3, l.TotalSize())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set value in nil list\",\n\t\t\tsetup: func() *List {\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tindex:       0,\n\t\t\tvalue:       42,\n\t\t\texpectedErr: ErrOutOfBounds,\n\t\t\tverify: func(t *testing.T, l *List) {\n\t\t\t\tuassert.Equal(t, 0, l.Size())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set multiple values at same index\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(1)\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tindex: 0,\n\t\t\tvalue: 42,\n\t\t\tverify: func(t *testing.T, l *List) {\n\t\t\t\tuassert.Equal(t, 42, l.Get(0))\n\t\t\t\terr := l.Set(0, 99)\n\t\t\t\tuassert.Equal(t, nil, err)\n\t\t\t\tuassert.Equal(t, 99, l.Get(0))\n\t\t\t\tuassert.Equal(t, 1, l.Size())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set value at last index\",\n\t\t\tsetup: func() *List {\n\t\t\t\tl := New()\n\t\t\t\tl.Append(1, 2, 3)\n\t\t\t\treturn l\n\t\t\t},\n\t\t\tindex: 2,\n\t\t\tvalue: 42,\n\t\t\tverify: func(t *testing.T, l *List) {\n\t\t\t\tuassert.Equal(t, 42, l.Get(2))\n\t\t\t\tuassert.Equal(t, 3, l.Size())\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tl := tt.setup()\n\t\t\terr := l.Set(tt.index, tt.value)\n\n\t\t\tif tt.expectedErr != nil {\n\t\t\t\tuassert.ErrorIs(t, err, tt.expectedErr)\n\t\t\t} else {\n\t\t\t\tuassert.Equal(t, nil, err)\n\t\t\t}\n\n\t\t\ttt.verify(t, l)\n\t\t})\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"ZJ5lfmuM9/5G9hytPO/tRY6ReBMC9WDIfdRH2R4vQFQ8OPI1AET8+hxM/DYRqm53JcFipmiR94ofe+D666Qd/A=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"message","path":"gno.land/p/jeronimoalbi/message/v0","files":[{"name":"broker.gno","body":"package message\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\n\t\"gno.land/p/moul/ulist/v0\"\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":"broker_filetest.gno","body":"package main\n\nimport (\n\t\"gno.land/p/jeronimoalbi/message/v0\"\n)\n\nfunc main() {\n\t// Create a message broker and a generic message callback\n\tbroker := message.NewBroker()\n\tcb := func(m message.Message) {\n\t\tprintln(\"topic triggered: \" + string(m.Topic))\n\t\tprintln(\"topic data: \" + m.Data.(string))\n\t}\n\n\t// Subscribe to a couple of events\n\t_, err := broker.Subscribe(\"eventA\", cb)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = broker.Subscribe(\"eventB\", cb)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Subscribe to an event and then unsubscribe from it\n\tid, err := broker.Subscribe(\"eventC\", cb)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = broker.Unsubscribe(\"eventC\", id)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Subscribe to all events\n\t_, err = broker.Subscribe(message.TopicAll, func(m message.Message) {\n\t\tprintln(\"catchall topic triggered: \" + string(m.Topic))\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// List broker topics\n\tprintln(\"topics:\")\n\tfor _, topic := range broker.Topics() {\n\t\tprintln(\"- \" + string(topic))\n\t}\n\n\t// Publish events\n\tprintln()\n\tif err = broker.Publish(\"eventA\", \"A\"); err != nil {\n\t\tpanic(err)\n\t}\n\n\tprintln()\n\tif err = broker.Publish(\"eventB\", \"B\"); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Output:\n// topics:\n// - eventA\n// - eventB\n// - eventC\n//\n// topic triggered: eventA\n// topic data: A\n// catchall topic triggered: eventA\n//\n// topic triggered: eventB\n// topic data: B\n// catchall topic triggered: eventB\n"},{"name":"broker_test.gno","body":"package message_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/jeronimoalbi/expect/v0\"\n\t\"gno.land/p/jeronimoalbi/message/v0\"\n)\n\nvar (\n\t_ message.Subscriber = (*message.Broker)(nil)\n\t_ message.Publisher  = (*message.Broker)(nil)\n)\n\nfunc TestBrokerTopics(t *testing.T) {\n\tbroker := message.NewBroker()\n\texpect.\n\t\tValue(t, len(broker.Topics())).\n\t\tAsInt().\n\t\tToEqual(0)\n\n\tcb := func(message.Message) {}\n\tbroker.Subscribe(\"foo\", cb)\n\tbroker.Subscribe(\"bar\", cb)\n\tbroker.Subscribe(\"baz\", cb)\n\tbroker.Subscribe(message.TopicAll, cb)\n\ttopics := broker.Topics()\n\n\texpect.\n\t\tValue(t, len(topics)).\n\t\tAsInt().\n\t\tToEqual(3)\n\texpect.\n\t\tValue(t, string(topics[0])).\n\t\tAsString().\n\t\tToEqual(\"bar\")\n\texpect.\n\t\tValue(t, string(topics[1])).\n\t\tAsString().\n\t\tToEqual(\"baz\")\n\texpect.\n\t\tValue(t, string(topics[2])).\n\t\tAsString().\n\t\tToEqual(\"foo\")\n}\n\nfunc TestBrokerPublish(t *testing.T) {\n\ttests := []struct {\n\t\tname               string\n\t\tsubscribe, publish message.Topic\n\t\tdata               any\n\t\tmessage            *message.Message\n\t\terr                error\n\t}{\n\t\t{\n\t\t\tname:      \"publishes subscribed topic\",\n\t\t\tsubscribe: \"foo\",\n\t\t\tpublish:   \"foo\",\n\t\t\tdata:      \"foo's data\",\n\t\t\tmessage: \u0026message.Message{\n\t\t\t\tTopic: \"foo\",\n\t\t\t\tData:  \"foo's data\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"publishes all topics\",\n\t\t\tsubscribe: message.TopicAll,\n\t\t\tpublish:   \"foo\",\n\t\t\tdata:      \"foo's data\",\n\t\t\tmessage: \u0026message.Message{\n\t\t\t\tTopic: \"foo\",\n\t\t\t\tData:  \"foo's data\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid topic\",\n\t\t\tsubscribe: \"foo\",\n\t\t\tpublish:   message.TopicAll,\n\t\t\terr:       message.ErrInvalidTopic,\n\t\t},\n\t\t{\n\t\t\tname:      \"no topic\",\n\t\t\tsubscribe: \"foo\",\n\t\t\tpublish:   \"\",\n\t\t\terr:       message.ErrRequiredTopic,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// Arrange\n\t\t\tvar msg *message.Message\n\t\t\tbroker := message.NewBroker()\n\t\t\tbroker.Subscribe(tt.subscribe, func(m message.Message) { msg = \u0026m })\n\n\t\t\t// Act\n\t\t\terr := broker.Publish(tt.publish, tt.data)\n\n\t\t\t// Assert\n\t\t\tif tt.err != nil {\n\t\t\t\texpect.\n\t\t\t\t\tFunc(t, func() error { return err }).\n\t\t\t\t\tWithFailPrefix(\"expect a publish error\").\n\t\t\t\t\tToFail().\n\t\t\t\t\tWithError(tt.err)\n\t\t\t\texpect.\n\t\t\t\t\tValue(t, istypednil(msg)).\n\t\t\t\t\tWithFailPrefix(\"expect callback not to be called\").\n\t\t\t\t\tAsBoolean().\n\t\t\t\t\tToBeTruthy()\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\texpect.\n\t\t\t\tValue(t, err).\n\t\t\t\tWithFailPrefix(\"expect no publish error\").\n\t\t\t\tToBeNil()\n\t\t\texpect.\n\t\t\t\tValue(t, msg).\n\t\t\t\tWithFailPrefix(\"expect callback to be called\").\n\t\t\t\tNot().ToBeNil()\n\t\t\texpect.\n\t\t\t\tValue(t, string(msg.Topic)).\n\t\t\t\tWithFailPrefix(\"expect message topic to match\").\n\t\t\t\tAsString().\n\t\t\t\tToEqual(string(tt.message.Topic))\n\t\t\texpect.\n\t\t\t\tValue(t, msg.Data).\n\t\t\t\tWithFailPrefix(\"expect message data to match\").\n\t\t\t\tAsString().\n\t\t\t\tToEqual(tt.message.Data.(string))\n\t\t})\n\t}\n}\n\nfunc TestBrokerSubscribe(t *testing.T) {\n\tcb := func(message.Message) {}\n\ttests := []struct {\n\t\tname     string\n\t\tsetup    func(*message.Broker)\n\t\ttopic    message.Topic\n\t\tid       int64\n\t\tcallback message.Callback\n\t\terr      error\n\t}{\n\t\t{\n\t\t\tname:     \"single subscription\",\n\t\t\ttopic:    \"foo\",\n\t\t\tid:       1,\n\t\t\tcallback: cb,\n\t\t},\n\t\t{\n\t\t\tname: \"existing subscriptions\",\n\t\t\tsetup: func(b *message.Broker) {\n\t\t\t\tb.Subscribe(\"foo\", cb)\n\t\t\t},\n\t\t\ttopic:    \"foo\",\n\t\t\tid:       2,\n\t\t\tcallback: cb,\n\t\t},\n\t\t{\n\t\t\tname: \"other subscription topics\",\n\t\t\tsetup: func(b *message.Broker) {\n\t\t\t\tb.Subscribe(\"foo\", cb)\n\t\t\t},\n\t\t\ttopic:    \"bar\",\n\t\t\tid:       1,\n\t\t\tcallback: cb,\n\t\t},\n\t\t{\n\t\t\tname:  \"no topic\",\n\t\t\ttopic: \"\",\n\t\t\terr:   message.ErrRequiredTopic,\n\t\t},\n\t\t{\n\t\t\tname:     \"no callback\",\n\t\t\ttopic:    \"foo\",\n\t\t\tcallback: nil,\n\t\t\terr:      message.ErrRequiredCallback,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// Arrange\n\t\t\tbroker := message.NewBroker()\n\t\t\tif tt.setup != nil {\n\t\t\t\ttt.setup(broker)\n\t\t\t}\n\n\t\t\t// Act\n\t\t\tid, err := broker.Subscribe(tt.topic, tt.callback)\n\n\t\t\t// Assert\n\t\t\tif tt.err != nil {\n\t\t\t\texpect.\n\t\t\t\t\tFunc(t, func() error { return err }).\n\t\t\t\t\tWithFailPrefix(\"expect a subscribe error\").\n\t\t\t\t\tToFail().\n\t\t\t\t\tWithError(tt.err)\n\t\t\t\texpect.\n\t\t\t\t\tValue(t, id).\n\t\t\t\t\tWithFailPrefix(\"expect zero ID\").\n\t\t\t\t\tAsInt().\n\t\t\t\t\tToEqual(0)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\texpect.\n\t\t\t\tValue(t, err).\n\t\t\t\tWithFailPrefix(\"expect no subscribe error\").\n\t\t\t\tToBeNil()\n\t\t\texpect.\n\t\t\t\tValue(t, id).\n\t\t\t\tWithFailPrefix(\"expect ID to match\").\n\t\t\t\tAsInt().\n\t\t\t\tToEqual(tt.id)\n\t\t})\n\t}\n}\n\nfunc TestBrokerUnsubscribe(t *testing.T) {\n\tcb := func(message.Message) {}\n\ttests := []struct {\n\t\tname   string\n\t\tsetup  func(*message.Broker)\n\t\ttopic  message.Topic\n\t\tid     int\n\t\terrMsg string\n\t}{\n\t\t{\n\t\t\tname: \"single subscription\",\n\t\t\tsetup: func(b *message.Broker) {\n\t\t\t\tb.Subscribe(\"foo\", cb)\n\t\t\t},\n\t\t\ttopic: \"foo\",\n\t\t\tid:    1,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple subscriptions\",\n\t\t\tsetup: func(b *message.Broker) {\n\t\t\t\tb.Subscribe(\"foo\", cb)\n\t\t\t\tb.Subscribe(\"foo\", cb)\n\t\t\t},\n\t\t\ttopic: \"foo\",\n\t\t\tid:    2,\n\t\t},\n\t\t{\n\t\t\tname: \"other subscription topics\",\n\t\t\tsetup: func(b *message.Broker) {\n\t\t\t\tb.Subscribe(\"foo\", cb)\n\t\t\t\tb.Subscribe(\"bar\", cb)\n\t\t\t},\n\t\t\ttopic: \"foo\",\n\t\t\tid:    1,\n\t\t},\n\t\t{\n\t\t\tname:   \"not found\",\n\t\t\ttopic:  \"foo\",\n\t\t\tid:     1,\n\t\t\terrMsg: \"message topic not found: foo\",\n\t\t},\n\t\t{\n\t\t\tname:   \"no topic\",\n\t\t\ttopic:  \"\",\n\t\t\terrMsg: message.ErrRequiredTopic.Error(),\n\t\t},\n\t\t{\n\t\t\tname:   \"no subscription ID\",\n\t\t\ttopic:  \"foo\",\n\t\t\tid:     0,\n\t\t\terrMsg: message.ErrRequiredSubscriptionID.Error(),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// Arrange\n\t\t\tbroker := message.NewBroker()\n\t\t\tif tt.setup != nil {\n\t\t\t\ttt.setup(broker)\n\t\t\t}\n\n\t\t\t// Act\n\t\t\tunsubscribed, err := broker.Unsubscribe(tt.topic, tt.id)\n\n\t\t\t// Assert\n\t\t\tif tt.errMsg != \"\" {\n\t\t\t\texpect.\n\t\t\t\t\tFunc(t, func() error { return err }).\n\t\t\t\t\tWithFailPrefix(\"expect a subscribe error\").\n\t\t\t\t\tToFail().\n\t\t\t\t\tWithMessage(tt.errMsg)\n\t\t\t\texpect.\n\t\t\t\t\tValue(t, unsubscribed).\n\t\t\t\t\tWithFailPrefix(\"expect unsubscribe to fail\").\n\t\t\t\t\tAsBoolean().\n\t\t\t\t\tToBeFalsy()\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\texpect.\n\t\t\t\tValue(t, err).\n\t\t\t\tWithFailPrefix(\"expect no unsubscribe error\").\n\t\t\t\tToBeNil()\n\t\t\texpect.\n\t\t\t\tValue(t, unsubscribed).\n\t\t\t\tWithFailPrefix(\"expect unsubscribe to succeed\").\n\t\t\t\tAsBoolean().\n\t\t\t\tToBeTruthy()\n\t\t})\n\t}\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/v0\"\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"1z9qv3ifA0ufR0ijFJt1dIXNcl6sCbwO65TdSjjrIn5AzPsB3ljhu8HOQLzPetFTNSXkLDHeLoILtotXSYNrtg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"pager","path":"gno.land/p/jeronimoalbi/pager/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/jeronimoalbi/pager/v0\"\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/v0\"\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"},{"name":"pager_test.gno","body":"package pager\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestPager(t *testing.T) {\n\tcases := []struct {\n\t\tname, uri, prevPath, nextPath, param string\n\t\toffset, pageSize, page, pageCount    int\n\t\thasPages                             bool\n\t\titems                                []int\n\t\terr                                  error\n\t}{\n\t\t{\n\t\t\tname:      \"page 1\",\n\t\t\turi:       \"gno.land/r/demo/test:foo/bar?page=1\u0026foo=bar\",\n\t\t\titems:     []int{1, 2, 3, 4, 5, 6},\n\t\t\thasPages:  true,\n\t\t\tnextPath:  \"?foo=bar\u0026page=2\",\n\t\t\tpageSize:  5,\n\t\t\tpage:      1,\n\t\t\tpageCount: 2,\n\t\t},\n\t\t{\n\t\t\tname:      \"page 2\",\n\t\t\turi:       \"gno.land/r/demo/test:foo/bar?page=2\u0026foo=bar\",\n\t\t\titems:     []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},\n\t\t\thasPages:  true,\n\t\t\tprevPath:  \"?foo=bar\u0026page=1\",\n\t\t\tnextPath:  \"\",\n\t\t\toffset:    5,\n\t\t\tpageSize:  5,\n\t\t\tpage:      2,\n\t\t\tpageCount: 2,\n\t\t},\n\t\t{\n\t\t\tname:      \"custom query param\",\n\t\t\turi:       \"gno.land/r/demo/test:foo/bar?current=2\u0026foo=bar\",\n\t\t\titems:     []int{1, 2, 3},\n\t\t\tparam:     \"current\",\n\t\t\thasPages:  true,\n\t\t\tprevPath:  \"?current=1\u0026foo=bar\",\n\t\t\tnextPath:  \"\",\n\t\t\toffset:    2,\n\t\t\tpageSize:  2,\n\t\t\tpage:      2,\n\t\t\tpageCount: 2,\n\t\t},\n\t\t{\n\t\t\tname: \"missing page\",\n\t\t\turi:  \"gno.land/r/demo/test:foo/bar?page=3\u0026foo=bar\",\n\t\t\terr:  ErrInvalidPageNumber,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid page zero\",\n\t\t\turi:  \"gno.land/r/demo/test:foo/bar?page=0\",\n\t\t\terr:  ErrInvalidPageNumber,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid page number\",\n\t\t\turi:  \"gno.land/r/demo/test:foo/bar?page=foo\",\n\t\t\terr:  ErrInvalidPageNumber,\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Act\n\t\t\tp, err := New(tc.uri, len(tc.items), WithPageSize(tc.pageSize), WithPageQueryParam(tc.param))\n\n\t\t\t// Assert\n\t\t\tif tc.err != nil {\n\t\t\t\turequire.ErrorIs(t, err, tc.err, \"expected an error\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.NoError(t, err, \"expect no error\")\n\t\t\tuassert.Equal(t, len(tc.items), p.TotalItems(), \"total items\")\n\t\t\tuassert.Equal(t, tc.page, p.Page(), \"page number\")\n\t\t\tuassert.Equal(t, tc.pageCount, p.PageCount(), \"number of pages\")\n\t\t\tuassert.Equal(t, tc.pageSize, p.PageSize(), \"page size\")\n\t\t\tuassert.Equal(t, tc.prevPath, p.PrevPageURI(), \"prev URL page\")\n\t\t\tuassert.Equal(t, tc.nextPath, p.NextPageURI(), \"next URL page\")\n\t\t\tuassert.Equal(t, tc.hasPages, p.HasPages(), \"has pages\")\n\t\t\tuassert.Equal(t, tc.offset, p.Offset(), \"item offset\")\n\t\t})\n\t}\n}\n\nfunc TestPagerIterate(t *testing.T) {\n\tcases := []struct {\n\t\tname, uri   string\n\t\titems, page []int\n\t\tstop        bool\n\t}{\n\t\t{\n\t\t\tname:  \"page 1\",\n\t\t\turi:   \"gno.land/r/demo/test:foo/bar?page=1\",\n\t\t\titems: []int{1, 2, 3, 4, 5, 6, 7},\n\t\t\tpage:  []int{1, 2, 3},\n\t\t},\n\t\t{\n\t\t\tname:  \"page 2\",\n\t\t\turi:   \"gno.land/r/demo/test:foo/bar?page=2\",\n\t\t\titems: []int{1, 2, 3, 4, 5, 6, 7},\n\t\t\tpage:  []int{4, 5, 6},\n\t\t},\n\t\t{\n\t\t\tname:  \"page 3\",\n\t\t\turi:   \"gno.land/r/demo/test:foo/bar?page=3\",\n\t\t\titems: []int{1, 2, 3, 4, 5, 6, 7},\n\t\t\tpage:  []int{7},\n\t\t},\n\t\t{\n\t\t\tname:  \"stop iteration\",\n\t\t\turi:   \"gno.land/r/demo/test:foo/bar?page=1\",\n\t\t\titems: []int{1, 2, 3},\n\t\t\tstop:  true,\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Arrange\n\t\t\tvar (\n\t\t\t\titems []int\n\t\t\t\tp     = MustNew(tc.uri, len(tc.items), WithPageSize(3))\n\t\t\t)\n\n\t\t\t// Act\n\t\t\tstopped := p.Iterate(func(i int) bool {\n\t\t\t\tif tc.stop {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\n\t\t\t\titems = append(items, tc.items[i])\n\t\t\t\treturn false\n\t\t\t})\n\n\t\t\t// Assert\n\t\t\tuassert.Equal(t, tc.stop, stopped)\n\t\t\turequire.Equal(t, len(tc.page), len(items), \"expect iteration of the right number of items\")\n\n\t\t\tfor i, v := range items {\n\t\t\t\turequire.Equal(t, tc.page[i], v, \"expect iterated items to match\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestPicker(t *testing.T) {\n\tpageSize := 3\n\tcases := []struct {\n\t\tname, uri, output string\n\t\ttotalItems        int\n\t}{\n\t\t{\n\t\t\tname:       \"one page\",\n\t\t\turi:        \"gno.land/r/demo/test:foo/bar?page=1\",\n\t\t\ttotalItems: 3,\n\t\t\toutput:     \"\",\n\t\t},\n\t\t{\n\t\t\tname:       \"two pages\",\n\t\t\turi:        \"gno.land/r/demo/test:foo/bar?page=1\",\n\t\t\ttotalItems: 4,\n\t\t\toutput:     \"\\\\- | page 1 of 2 | [»](?page=2)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"three pages\",\n\t\t\turi:        \"gno.land/r/demo/test:foo/bar?page=1\",\n\t\t\ttotalItems: 7,\n\t\t\toutput:     \"\\\\- | page 1 of 3 | [»](?page=2)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"three pages second page\",\n\t\t\turi:        \"gno.land/r/demo/test:foo/bar?page=2\",\n\t\t\ttotalItems: 7,\n\t\t\toutput:     \"[«](?page=1) | page 2 of 3 | [»](?page=3)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"three pages third page\",\n\t\t\turi:        \"gno.land/r/demo/test:foo/bar?page=3\",\n\t\t\ttotalItems: 7,\n\t\t\toutput:     \"[«](?page=2) | page 3 of 3 | \\\\-\",\n\t\t},\n\t}\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Arrange\n\t\t\tp := MustNew(tc.uri, tc.totalItems, WithPageSize(pageSize))\n\n\t\t\t// Act\n\t\t\toutput := Picker(p)\n\n\t\t\t// Assert\n\t\t\tuassert.Equal(t, tc.output, output)\n\t\t})\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"+AQyFv9Jqdoz20A4avzSZMPbmldjx3FgiKvOKcoRNMtl45OEAMM+FqsVIM62AW6CbTneyAzj+Ttid3EQOpc/xw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5","package":{"name":"coinsort","path":"gno.land/p/leon/coinsort/v0","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":"coinsort_test.gno","body":"package coinsort\n\nimport (\n\t\"chain\"\n\t\"testing\"\n)\n\nfunc TestSortByBalance(t *testing.T) {\n\tcoins := chain.Coins{\n\t\tchain.Coin{Denom: \"b\", Amount: 50},\n\t\tchain.Coin{Denom: \"c\", Amount: 10},\n\t\tchain.Coin{Denom: \"a\", Amount: 100},\n\t}\n\n\texpected := chain.Coins{\n\t\tchain.Coin{Denom: \"c\", Amount: 10},\n\t\tchain.Coin{Denom: \"b\", Amount: 50},\n\t\tchain.Coin{Denom: \"a\", Amount: 100},\n\t}\n\n\tSortByBalance(coins)\n\n\tfor i := range coins {\n\t\tif coins[i] != expected[i] {\n\t\t\tt.Errorf(\"SortByBalance failed at index %d: got %+v, want %+v\", i, coins[i], expected[i])\n\t\t}\n\t}\n}\n\nfunc TestSortByCustomDescendingAmount(t *testing.T) {\n\tcoins := chain.Coins{\n\t\tchain.Coin{Denom: \"a\", Amount: 2},\n\t\tchain.Coin{Denom: \"b\", Amount: 3},\n\t\tchain.Coin{Denom: \"c\", Amount: 1},\n\t}\n\n\texpected := chain.Coins{\n\t\tchain.Coin{Denom: \"b\", Amount: 3},\n\t\tchain.Coin{Denom: \"a\", Amount: 2},\n\t\tchain.Coin{Denom: \"c\", Amount: 1},\n\t}\n\n\tSortBy(coins, func(a, b chain.Coin) bool {\n\t\treturn a.Amount \u003e b.Amount // descending\n\t})\n\n\tfor i := range coins {\n\t\tif coins[i] != expected[i] {\n\t\t\tt.Errorf(\"SortBy custom descending failed at index %d: got %+v, want %+v\", i, coins[i], expected[i])\n\t\t}\n\t}\n}\n\nfunc TestSortByNilFunc(t *testing.T) {\n\tcoins := chain.Coins{\n\t\tchain.Coin{Denom: \"x\", Amount: 5},\n\t\tchain.Coin{Denom: \"z\", Amount: 20},\n\t\tchain.Coin{Denom: \"y\", Amount: 10},\n\t}\n\n\texpected := chain.Coins{\n\t\tchain.Coin{Denom: \"x\", Amount: 5},\n\t\tchain.Coin{Denom: \"z\", Amount: 20},\n\t\tchain.Coin{Denom: \"y\", Amount: 10},\n\t}\n\n\tSortBy(coins, nil)\n\n\t// should stay the same\n\tfor i := range coins {\n\t\tif coins[i] != expected[i] {\n\t\t\tt.Errorf(\"SortBy nil func failed at index %d: got %+v, want %+v\", i, coins[i], expected[i])\n\t\t}\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/leon/coinsort/v0\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"xUIvusMP+ffn6jwXyXzLkfUb1A9X9MU0RWNtJg3DYd1nLYMan3OSWsDLF219TigPojUTukYrilAJdx9nxdlqaQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5","package":{"name":"ctg","path":"gno.land/p/leon/ctg/v0","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":"converter_test.gno","body":"package ctg\n\nimport (\n\t\"testing\"\n)\n\nfunc TestConvertKnownAddress(t *testing.T) {\n\tconst (\n\t\tcosmosAddr = \"cosmos1jg8mtutu9khhfwc4nxmuhcpftf0pajdh6svrgs\"\n\t\tgnoAddr    = \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"\n\t)\n\tgot, err := ConvertCosmosToGno(cosmosAddr)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif got != gnoAddr {\n\t\tt.Fatalf(\"got %s, want %s\", got, gnoAddr)\n\t}\n}\n\nfunc TestConvertCosmosToGno(t *testing.T) {\n\tdecoded := []byte{\n\t\t0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,\n\t\t0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10,\n\t\t0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,\n\t\t0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x00,\n\t}\n\n\tcosmosAddr := mustEncode(\"cosmos\", decoded)\n\twantGno := mustEncode(\"g\", decoded)\n\n\tgot, err := ConvertCosmosToGno(cosmosAddr)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif string(got) != wantGno {\n\t\tt.Fatalf(\"got %s, want %s\", got, wantGno)\n\t}\n\n\t// invalid bech32\n\tif _, err := ConvertCosmosToGno(\"not-bech32\"); err == nil {\n\t\tt.Fatalf(\"expected error for invalid bech32\")\n\t}\n\n\t// wrong prefix\n\tgAddr := mustEncode(\"g\", decoded)\n\tif _, err := ConvertCosmosToGno(gAddr); err == nil {\n\t\tt.Fatalf(\"expected error for non-cosmos prefix\")\n\t}\n}\n\nfunc TestConvertAnyToGno(t *testing.T) {\n\tpayload := []byte{\n\t\t0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,\n\t\t0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10,\n\t\t0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,\n\t\t0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x00,\n\t}\n\n\ttests := []struct {\n\t\tname    string\n\t\tinput   string\n\t\twant    string\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:  \"cosmos→g\",\n\t\t\tinput: mustEncode(\"cosmos\", payload),\n\t\t\twant:  mustEncode(\"g\", payload),\n\t\t},\n\t\t{\n\t\t\tname:  \"osmo→g\",\n\t\t\tinput: mustEncode(\"osmo\", payload),\n\t\t\twant:  mustEncode(\"g\", payload),\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid bech32\",\n\t\t\tinput:   \"xyz123\",\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot, err := ConvertAnyToGno(tc.input)\n\t\t\tif tc.wantErr {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Fatalf(\"expected error, got nil\")\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t}\n\t\t\tif string(got) != tc.want {\n\t\t\t\tt.Fatalf(\"got %s, want %s\", got, tc.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestConvertGnoToAny(t *testing.T) {\n\tpayload := []byte{\n\t\t0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,\n\t\t0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10,\n\t\t0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,\n\t\t0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x00,\n\t}\n\n\tgno := address(mustEncode(\"g\", payload))\n\n\tt.Run(\"g→cosmos\", func(t *testing.T) {\n\t\tgot, err := ConvertGnoToAny(\"cosmos\", gno)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\t\tif got != mustEncode(\"cosmos\", payload) {\n\t\t\tt.Fatalf(\"conversion incorrect: %s\", got)\n\t\t}\n\t})\n\n\tt.Run(\"g→foobar\", func(t *testing.T) {\n\t\tgot, err := ConvertGnoToAny(\"foobar\", gno)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\t\tif got != mustEncode(\"foobar\", payload) {\n\t\t\tt.Fatalf(\"conversion incorrect: %s\", got)\n\t\t}\n\t})\n\n\tt.Run(\"g→osmo\", func(t *testing.T) {\n\t\tgot, err := ConvertGnoToAny(\"osmo\", gno)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\t\tif got != mustEncode(\"osmo\", payload) {\n\t\t\tt.Fatalf(\"conversion incorrect: %s\", got)\n\t\t}\n\t})\n\n\tt.Run(\"wrong source prefix\", func(t *testing.T) {\n\t\tcosmos := mustEncode(\"cosmos\", payload)\n\t\tif _, err := ConvertGnoToAny(\"g\", address(cosmos)); err == nil {\n\t\t\tt.Fatalf(\"expected error for non-g source prefix\")\n\t\t}\n\t})\n\n\tt.Run(\"invalid bech32\", func(t *testing.T) {\n\t\tif _, err := ConvertGnoToAny(\"cosmos\", address(\"nope\")); err == nil {\n\t\t\tt.Fatalf(\"expected error for invalid bech32\")\n\t\t}\n\t})\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/leon/ctg/v0\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"PM27/S1TkoZmDIwZ2581p+afJo4rtRPdsU5J1Elog9wwulQMEt22g4QRJpWMApfd4bEAXT4bg+xs5BNaiV2JvA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"svg","path":"gno.land/p/nt/svg/v0","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/nt/svg/v0\"\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/nt/svg/v0\"\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/svg/v0\"\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"},{"name":"z0_filetest.gno","body":"// PKGPATH: gno.land/r/test\npackage test\n\nimport \"gno.land/p/nt/svg/v0\"\n\nfunc main() {\n\tcanvas := svg.Canvas{Width: 500, Height: 500}\n\tcanvas.Append(\n\t\tsvg.Rectangle{X: 50, Y: 50, Width: 100, Height: 100, Fill: \"red\"},\n\t\tsvg.Circle{CX: 100, CY: 100, R: 50, Fill: \"blue\"},\n\t\tsvg.Text{X: 100, Y: 100, Text: \"hello world!\", Fill: \"magenta\"},\n\t)\n\tcanvas.Append(\n\t\tsvg.NewCircle(100, 100, 50, \"blue\").WithClass(\"toto\"),\n\t)\n\tprintln(canvas)\n}\n\n// Output:\n// \u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"500\" height=\"500\" viewBox=\"\"\u003e\u003crect x=\"50\" y=\"50\" width=\"100\" height=\"100\" rx=\"0\" ry=\"0\" fill=\"red\" /\u003e\u003ccircle cx=\"100\" cy=\"100\" r=\"50\" fill=\"blue\" /\u003e\u003ctext x=\"100\" y=\"100\" dx=\"0\" dy=\"0\" rotate=\"\" fill=\"magenta\" \u003ehello world!\u003c/text\u003e\u003ccircle cx=\"100\" cy=\"100\" r=\"50\" fill=\"blue\" class=\"toto\"/\u003e\u003c/svg\u003e\n"},{"name":"z1_filetest.gno","body":"// PKGPATH: gno.land/r/test\npackage test\n\nimport \"gno.land/p/nt/svg/v0\"\n\nfunc main() {\n\tcanvas := svg.Canvas{\n\t\tWidth: 500, Height: 500,\n\t\tElems: []svg.Elem{\n\t\t\tsvg.Rectangle{X: 50, Y: 50, Width: 100, Height: 100, Fill: \"red\"},\n\t\t\tsvg.Circle{CX: 50, CY: 50, R: 100, Fill: \"red\"},\n\t\t\tsvg.Text{X: 100, Y: 100, Text: \"hello world!\", Fill: \"magenta\"},\n\t\t},\n\t}\n\tprintln(canvas)\n}\n\n// Output:\n// \u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"500\" height=\"500\" viewBox=\"\"\u003e\u003crect x=\"50\" y=\"50\" width=\"100\" height=\"100\" rx=\"0\" ry=\"0\" fill=\"red\" /\u003e\u003ccircle cx=\"50\" cy=\"50\" r=\"100\" fill=\"red\" /\u003e\u003ctext x=\"100\" y=\"100\" dx=\"0\" dy=\"0\" rotate=\"\" fill=\"magenta\" \u003ehello world!\u003c/text\u003e\u003c/svg\u003e\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"epXDbRCPY9Bx2EW7IH5LVRrcBcSw3k27ODfQJIN4YbNc66+33Ed4X6kQgE+bbWv74g9qRUhQZOW2q9/poZ05Yw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5","package":{"name":"svgbtn","path":"gno.land/p/leon/svgbtn/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/leon/svgbtn/v0\"\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/nt/svg/v0\"\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"QLeBrXHQcF53FGHLiD9qO+Z3lRCgRczLXBmfaUucTNVMoWN+duP5DaJ7M9py1wk6tOIhNl5UuVtTStv51UGvRA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"md","path":"gno.land/p/mason/md/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/mason/md/v0\"\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"0VL+vWBEg2fNLIlwALadUX6MlYPRVeAmyKyfCa9H0C4ovyXpD0R1+JWSjKvhm+DWgh7LWqKrwi62qHoagtuBSA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","package":{"name":"once","path":"gno.land/p/moul/once/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/once/v0\"\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"},{"name":"once_test.gno","body":"package once\n\nimport (\n\t\"errors\"\n\t\"testing\"\n)\n\nfunc TestOnce_Do(t *testing.T) {\n\tcounter := 0\n\tonce := New()\n\n\tincrement := func() {\n\t\tcounter++\n\t}\n\n\t// First call should execute\n\tonce.Do(increment)\n\tif counter != 1 {\n\t\tt.Errorf(\"expected counter to be 1, got %d\", counter)\n\t}\n\n\t// Second call should not execute\n\tonce.Do(increment)\n\tif counter != 1 {\n\t\tt.Errorf(\"expected counter to still be 1, got %d\", counter)\n\t}\n}\n\nfunc TestOnce_DoErr(t *testing.T) {\n\tonce := New()\n\texpectedErr := errors.New(\"test error\")\n\n\tfn := func() error {\n\t\treturn expectedErr\n\t}\n\n\t// First call should return error\n\tif err := once.DoErr(fn); err != expectedErr {\n\t\tt.Errorf(\"expected error %v, got %v\", expectedErr, err)\n\t}\n\n\t// Second call should return same error\n\tif err := once.DoErr(fn); err != expectedErr {\n\t\tt.Errorf(\"expected error %v, got %v\", expectedErr, err)\n\t}\n}\n\nfunc TestOnce_DoOrPanic(t *testing.T) {\n\tonce := New()\n\texecuted := false\n\n\tfn := func() {\n\t\texecuted = true\n\t}\n\n\t// First call should execute\n\tonce.DoOrPanic(fn)\n\tif !executed {\n\t\tt.Error(\"function should have executed\")\n\t}\n\n\t// Second call should panic\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Error(\"expected panic on second execution\")\n\t\t}\n\t}()\n\tonce.DoOrPanic(fn)\n}\n\nfunc TestOnce_DoValue(t *testing.T) {\n\tonce := New()\n\texpected := \"test value\"\n\tcounter := 0\n\n\tfn := func() any {\n\t\tcounter++\n\t\treturn expected\n\t}\n\n\t// First call should return value\n\tif result := once.DoValue(fn); result != expected {\n\t\tt.Errorf(\"expected %v, got %v\", expected, result)\n\t}\n\n\t// Second call should return cached value\n\tif result := once.DoValue(fn); result != expected {\n\t\tt.Errorf(\"expected %v, got %v\", expected, result)\n\t}\n\n\tif counter != 1 {\n\t\tt.Errorf(\"function should have executed only once, got %d executions\", counter)\n\t}\n}\n\nfunc TestOnce_DoValueErr(t *testing.T) {\n\tonce := New()\n\texpectedVal := \"test value\"\n\texpectedErr := errors.New(\"test error\")\n\tcounter := 0\n\n\tfn := func() (any, error) {\n\t\tcounter++\n\t\treturn expectedVal, expectedErr\n\t}\n\n\t// First call should return value and error\n\tval, err := once.DoValueErr(fn)\n\tif val != expectedVal || err != expectedErr {\n\t\tt.Errorf(\"expected (%v, %v), got (%v, %v)\", expectedVal, expectedErr, val, err)\n\t}\n\n\t// Second call should return cached value and error\n\tval, err = once.DoValueErr(fn)\n\tif val != expectedVal || err != expectedErr {\n\t\tt.Errorf(\"expected (%v, %v), got (%v, %v)\", expectedVal, expectedErr, val, err)\n\t}\n\n\tif counter != 1 {\n\t\tt.Errorf(\"function should have executed only once, got %d executions\", counter)\n\t}\n}\n\nfunc TestOnce_Reset(t *testing.T) {\n\tonce := New()\n\tcounter := 0\n\n\tfn := func() {\n\t\tcounter++\n\t}\n\n\tonce.Do(fn)\n\tif counter != 1 {\n\t\tt.Errorf(\"expected counter to be 1, got %d\", counter)\n\t}\n\n\tonce.Reset()\n\tonce.Do(fn)\n\tif counter != 2 {\n\t\tt.Errorf(\"expected counter to be 2 after reset, got %d\", counter)\n\t}\n}\n\nfunc TestOnce_IsDone(t *testing.T) {\n\tonce := New()\n\n\tif once.IsDone() {\n\t\tt.Error(\"new Once instance should not be done\")\n\t}\n\n\tonce.Do(func() {})\n\n\tif !once.IsDone() {\n\t\tt.Error(\"Once instance should be done after execution\")\n\t}\n}\n\nfunc TestOnce_Error(t *testing.T) {\n\tonce := New()\n\texpectedErr := errors.New(\"test error\")\n\n\tif err := once.Error(); err != nil {\n\t\tt.Errorf(\"expected nil error, got %v\", err)\n\t}\n\n\tonce.DoErr(func() error {\n\t\treturn expectedErr\n\t})\n\n\tif err := once.Error(); err != expectedErr {\n\t\tt.Errorf(\"expected error %v, got %v\", expectedErr, err)\n\t}\n}\n\nfunc TestOnce_Value(t *testing.T) {\n\tonce := New()\n\n\t// Test unexecuted state\n\tval, err := once.Value()\n\tif err != ErrNotExecuted {\n\t\tt.Errorf(\"expected ErrNotExecuted, got %v\", err)\n\t}\n\tif val != nil {\n\t\tt.Errorf(\"expected nil value, got %v\", val)\n\t}\n\n\t// Test after execution\n\texpected := \"test value\"\n\tonce.DoValue(func() any {\n\t\treturn expected\n\t})\n\n\tval, err = once.Value()\n\tif err != nil {\n\t\tt.Errorf(\"expected nil error, got %v\", err)\n\t}\n\tif val != expected {\n\t\tt.Errorf(\"expected value %v, got %v\", expected, val)\n\t}\n}\n\nfunc TestOnce_DoValueErr_Panic_MarkedDone(t *testing.T) {\n\tonce := New()\n\tcount := 0\n\tfn := func() (any, error) {\n\t\tcount++\n\t\tpanic(\"panic\")\n\t}\n\tvar r any\n\tfunc() {\n\t\tdefer func() { r = recover() }()\n\t\tonce.DoValueErr(fn)\n\t}()\n\tif r == nil {\n\t\tt.Error(\"expected panic on first call\")\n\t}\n\tif !once.IsDone() {\n\t\tt.Error(\"expected once to be marked as done after panic\")\n\t}\n\tr = nil\n\tfunc() {\n\t\tdefer func() { r = recover() }()\n\t\tonce.DoValueErr(fn)\n\t}()\n\tif r != nil {\n\t\tt.Error(\"expected no panic on subsequent call\")\n\t}\n\tif count != 1 {\n\t\tt.Errorf(\"expected count to be 1, got %d\", count)\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"2gm32nWo47vYACoVjmCgO9rPZ5WUAyQ87fN6ihisI+5iAAUq77XnUZrB1PoR/VQUNjdDb+azcm6Opwocy10jaA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","package":{"name":"authz","path":"gno.land/p/moul/authz/v0","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: The contract at a given path is itself the authority\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: Makes the contract at a path its own authority\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/v0\"\n\t\"gno.land/p/moul/once/v0\"\n\t\"gno.land/p/nt/avl/rotree/v0\"\n\t\"gno.land/p/nt/avl/v0\"\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//   - Caller- and title-forgery on the RAW interface: Authorize takes\n//     `caller` and `title` as ARGUMENTS, not from the frame. A consumer\n//     that exposes a bare Authority (rather than the *Authorizer that\n//     wraps it) therefore lets any holder present any caller and any\n//     title. Caller-forgery is contained — the only authority-mutating\n//     closure lives inside Authorizer.Transfer, so a forged caller on a\n//     raw Authorize runs the caller's own inert closure and cannot\n//     transfer; TestForgedCallerCannotTransfer pins that boundary.\n//     Title-forgery is NOT contained: the title is passed straight to the\n//     contractHandler, so a handler that branches on it (routing, quotas,\n//     audit trails) must not treat it as trusted. Keep the Authority\n//     unexported and hand out only what callers need; see\n//     r/gnops/valopers/admin.gno, which exports a description string.\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.\n//\n// A non-canonical impl is wrapped as custom_authority[...] so that the\n// official \"dropped\" is distinguishable from a \"*custom*: dropped\"\n// (autoclaimed) one. Shared with ContractAuthority.String, which applies\n// the same rule to its nested proposer.\nfunc (a *Authorizer) String() string {\n\treturn canonicalAuthorityString(a.auth)\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 makes the contract at `path` its OWN authority:\n// the default proposer accepts only chain.PackageAddress(path), so a\n// privileged action proceeds only when driven from that contract's own\n// frame.\n//\n// NewRestrictedContractAuthority does NOT widen this — it REPLACES it.\n// See its godoc; with an explicit proposer, `path` is a label and\n// contractAddr is never consulted.\n//\n// The `caller` compared against that address is established upstream by\n// Authorizer.DoByCurrent / DoByPrevious / Transfer under rlm.IsCurrent(),\n// so an external realm cannot present the contract address.\n//\n// BREAKING: the default proposer used to be\n// NewAutoAcceptAuthority (\"anyone can propose\"). Consumers that relied on\n// arbitrary callers driving a plain ContractAuthority will start getting\n// \"unauthorized\"; that is the fix, not a regression. Pass an explicit\n// proposer to NewRestrictedContractAuthority to opt back in.\n//\n// SECURITY — the gate binds the contract's IDENTITY, not its intent.\n// Inside any crossing frame of the contract, rlm.Address() is\n// unconditionally PackageAddress(path), so `DoByCurrent` from within the\n// contract is a tautology; only DoByPrevious and Transfer gain a real\n// check. Consequently ANY exported function of the contract that returns\n// a crossing closure (or anything else carrying its frame) hands this\n// authority to its caller. Keep privileged closures unexported.\n//\n// So DO NOT write NewContractAuthority(ownPath) + DoByCurrent. It reads\n// like \"only I may do this\" and compiles to \"anyone may do this\". Point\n// `path` at the principal you actually want to assert — usually the\n// governance realm that will drive the action — and use DoByPrevious, so\n// the comparison is against whoever crossed in:\n//\n//\t// in r/gnops/valopers/init.gno\n//\tauth = authz.NewWithAuthority(\n//\t    authz.NewContractAuthority(\"gno.land/r/gov/dao\", handler),\n//\t)\n//\t// in the privileged write\n//\tauth.DoByPrevious(0, rlm, \"update-instructions\", action)\n//\n// Own-path is right only when something OTHER than the contract's own\n// frame supplies the caller — i.e. you drive it via DoByPrevious from a\n// realm you have deliberately let call in, or via Transfer.\n//\n// SECURITY — a gate is only as good as the principal it names. Pointing\n// `path` at a realm whose frame ANY caller can mint buys nothing. Check,\n// for whatever principal you choose, that nothing exported by that realm\n// hands out its frame identity:\n//\n//   - `gno.land/r/gov/dao` is safe to assert ONLY because\n//     SimpleExecutor.Execute rejects invocation from outside the proxy\n//     (r/gov/dao/types.gno). Before that gate existed, dao's exported\n//     NewSimpleExecutor + Execute let any realm mint a frame whose\n//     Previous() was r/gov/dao, so this whole pairing authenticated\n//     nothing. Do not assume a governance path is unforgeable; verify it.\n//   - Whatever the principal, an exported function of YOUR realm whose\n//     signature is assignable to a callback type the principal invokes\n//     (for r/gov/dao: `func(realm) error`) is itself the leak — the\n//     principal will happily call it for an attacker. Keep privileged\n//     entrypoints out of that shape, or take extra parameters.\n//\n// Neither shape defends against re-exporting the privileged closure: its\n// holder picks the Previous() they present by wrapping it in an executor\n// of their own. Unexported closures remain the load-bearing rule.\n//\n// A syntactically valid but WRONG `path` is a permanent brick, not an\n// error: the authority accepts nobody, and Transfer routes through the\n// same gate, so it cannot be rotated out either. Construction rejects\n// malformed paths, but it cannot tell a wrong path from a right one —\n// e.g. \"gno.land/r/gov/dao/impl/v0\" (the path r/gov/dao's own\n// allowedDAOs list holds) is well-formed and permanently dead, because\n// an executor's Previous() is the PROXY path, never the impl. Give the\n// consumer a governance-gated rotation entrypoint before deploying; see\n// r/gnops/valopers/admin.gno's NewAuthorityRotationProposalRequest.\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 enforces at-most-once invocation and NOTHING ELSE — in\n// particular it does not check the caller, and the handler may call it\n// however it likes (multiple times, never, out of order). Register only\n// trusted handler functions; treat handler registration as the trust\n// boundary.\n//\n// Panics if `path` is empty or malformed, or if `handler` is nil.\nfunc NewContractAuthority(path string, handler PrivilegedActionHandler) *ContractAuthority {\n\taddr := chain.PackageAddress(assertValidContractPath(path))\n\t// The default proposer is a real Authority, not an absent one. Encoding\n\t// the strictest policy as a nil field would mean the secure default is\n\t// what you get by leaving something OUT: a struct literal that skips\n\t// this constructor, a persisted value from an older build, or re-adding\n\t// `if proposer == nil { proposer = NewAutoAcceptAuthority() }` would all\n\t// silently be wide open again. A one-field struct costs the same as the\n\t// nil it replaces and cannot be reached by omission.\n\treturn newContractAuthority(path, addr, handler, \u0026contractIdentityAuthority{addr: addr})\n}\n\n// NewRestrictedContractAuthority creates a contract authority whose\n// proposer REPLACES the contract-identity gate.\n//\n// This is the escape hatch, not a second lock. `proposer` becomes the ONLY\n// authorization decision: contractAddr is not consulted on this path, so\n// `path` degrades to a label that appears in String() and asserts nothing.\n// NewRestrictedContractAuthority(p, h, NewAutoAcceptAuthority()) is exactly\n// the old default behaviour (\"anyone can propose\"), stated\n// explicitly rather than reached by default.\n//\n// In particular NewRestrictedContractAuthority(\"gno.land/r/gov/dao\", h,\n// NewMemberAuthority(alice)) does NOT mean \"GovDAO and alice\". It means\n// \"alice, and not GovDAO\".\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.\n//     String() renders a non-canonical proposer wrapped as\n//     custom_authority[...] so such a substitution is at least visible.\nfunc NewRestrictedContractAuthority(path string, handler PrivilegedActionHandler, proposer Authority) Authority {\n\tif proposer == nil {\n\t\tpanic(\"proposer cannot be nil\")\n\t}\n\taddr := chain.PackageAddress(assertValidContractPath(path))\n\treturn newContractAuthority(path, addr, handler, proposer)\n}\n\n// newContractAuthority is the single construction path: both exported\n// constructors validate, then land here, so a guard added once applies to\n// both. `handler` is rejected here rather than surfaced at Authorize time\n// because Authorize checks contractHandler == nil BEFORE consulting the\n// proposer, and Transfer routes through Authorize — so a nil-handler\n// authority could never be rotated out. It is a permanent brick with no\n// on-chain recovery for anyone: the same bricked-governance failure\n// mode, reached by accident.\nfunc newContractAuthority(path string, addr address, handler PrivilegedActionHandler, proposer Authority) *ContractAuthority {\n\tif handler == nil {\n\t\tpanic(\"contract handler cannot be nil\")\n\t}\n\treturn \u0026ContractAuthority{\n\t\tcontractPath:    path,\n\t\tcontractAddr:    addr,\n\t\tcontractHandler: handler,\n\t\tproposer:        proposer,\n\t}\n}\n\n// assertValidContractPath rejects paths that chain.PackageAddress would\n// hash happily but that no realm could ever present, which would brick the\n// authority permanently (nobody authorizes, and Transfer routes through the\n// same gate so nobody can rotate out). Returns `path` so it composes.\n//\n// The authoritative rule is gnolang.ReGnoUserPkgPath (\"all paths must be\n// lowercase ascii alphanumeric characters\", gnovm/pkg/gnolang/mempackage.go),\n// but no validator is exported to Gno code — chain's isValidSubpath /\n// assertValidSubpath are unexported, and there is no chain.IsValidPkgPath.\n// So this is a deliberately permissive superset: segment (\"/\" segment)*,\n// segment = [a-z0-9] ([a-z0-9_.-]* [a-z0-9])?. It accepts every real\n// package path and catches what actually gets typed wrong — trailing or\n// embedded whitespace, empty segments, uppercase, stray punctuation. It\n// CANNOT catch a well-formed path that is simply the wrong principal. See\n// NewContractAuthority's godoc on rotation.\nfunc assertValidContractPath(path string) string {\n\tif path == \"\" {\n\t\tpanic(\"contract path cannot be empty\")\n\t}\n\tif !strings.Contains(path, \"/\") {\n\t\tpanic(\"contract path must be a package path, e.g. gno.land/r/gov/dao\")\n\t}\n\tstart := 0\n\tfor i := 0; i \u003c= len(path); i++ {\n\t\tif i == len(path) || path[i] == '/' {\n\t\t\tif !isValidContractPathSegment(path[start:i]) {\n\t\t\t\tpanic(\"contract path must be '/'-separated segments of [a-z0-9] with '_.-' allowed inside a segment, e.g. gno.land/r/gov/dao\")\n\t\t\t}\n\t\t\tstart = i + 1\n\t\t}\n\t}\n\treturn path\n}\n\nfunc isValidContractPathSegment(seg string) bool {\n\tif seg == \"\" {\n\t\treturn false\n\t}\n\tfor i := 0; i \u003c len(seg); i++ {\n\t\tswitch c := seg[i]; {\n\t\tcase c \u003e= 'a' \u0026\u0026 c \u003c= 'z', c \u003e= '0' \u0026\u0026 c \u003c= '9':\n\t\tcase c == '_' || c == '.' || c == '-':\n\t\t\t// Allowed only strictly inside a segment.\n\t\t\tif i == 0 || i == len(seg)-1 {\n\t\t\t\treturn false\n\t\t\t}\n\t\tdefault:\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// contractIdentityAuthority is the default proposer installed by\n// NewContractAuthority: the contract at the bound path, and nobody else.\n// Unexported and unmutable by design — AddMember/RemoveMember are\n// meaningless for a fixed contract identity, which is why this is not a\n// MemberAuthority holding one address.\ntype contractIdentityAuthority struct {\n\taddr address\n}\n\nfunc (a *contractIdentityAuthority) Authorize(caller address, _ string, action PrivilegedAction, _ ...any) error {\n\tif caller != a.addr {\n\t\treturn errors.New(\"unauthorized\")\n\t}\n\treturn action()\n}\n\nfunc (a *contractIdentityAuthority) String() string { return \"contract-identity\" }\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\thandle := 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}\n\n\t// The proposer IS the authorization decision. For an authority built by\n\t// NewContractAuthority it is a contractIdentityAuthority bound to\n\t// contractAddr — the contract itself and nobody else; for one built by\n\t// NewRestrictedContractAuthority it is whatever the consumer installed,\n\t// and contractAddr is deliberately not consulted (see that constructor's\n\t// godoc). `caller` is established upstream by Authorizer.DoByCurrent /\n\t// DoByPrevious / Transfer under rlm.IsCurrent(), so an external realm\n\t// cannot present an arbitrary principal here.\n\t//\n\t// A nil proposer is not a policy, it is a malformed value: both\n\t// constructors always install one, so nil means the struct was built by\n\t// a literal that bypassed them. Fail closed rather than dereference.\n\tif a.proposer == nil {\n\t\treturn errors.New(\"proposer is not set\")\n\t}\n\treturn a.proposer.Authorize(caller, title+\"_proposal\", handle, args...)\n}\n\n// String renders the contract path AND the proposer.\n//\n// The proposer half is security-relevant, not cosmetic: it is the only\n// thing distinguishing a gated authority from a wide-open one. Rendering\n// the path alone made\n//\n//\tNewContractAuthority(path, handler)                              // gated\n//\tNewRestrictedContractAuthority(path, handler, AutoAccept{})      // open to all\n//\n// byte-identical, so any consumer test asserting on this string — and\n// any on-chain reader inspecting it — was blind to the difference. A\n// consumer realm could have its authority swapped for a fully permissive\n// one and its assertions would stay green.\n//\n// \"contract-identity\" names the default installed by NewContractAuthority:\n// the contract at contractPath, and nobody else, may drive this authority.\n//\n// The proposer is rendered through canonicalAuthorityString, NOT by calling\n// a.proposer.String() directly. Authority is an open interface, so a foreign\n// impl can return any text it likes — including \"contract-identity\". That\n// made a fully permissive authority byte-identical to the gated default\n// again, defeating the very assertions this rendering exists to support.\n// Non-canonical impls are wrapped as custom_authority[...], mirroring what\n// Authorizer.String has always done at the outer level.\n//\n// Read the `contract=` half with care: it is load-bearing only for the\n// contract-identity default. With any other proposer it is a label —\n// see NewRestrictedContractAuthority.\nfunc (a *ContractAuthority) String() string {\n\treturn ufmt.Sprintf(\n\t\t\"contract_authority[contract=%s,proposer=%s]\",\n\t\ta.contractPath,\n\t\tcanonicalAuthorityString(a.proposer),\n\t)\n}\n\n// canonicalAuthorityString renders an Authority, wrapping any\n// implementation that is not one of this package's own as\n// custom_authority[...] so a foreign impl cannot impersonate a canonical\n// one by choosing its String() text. Shared by ContractAuthority.String\n// (for the nested proposer) and Authorizer.String (for the installed\n// authority).\nfunc canonicalAuthorityString(auth Authority) string {\n\tif auth == nil {\n\t\t// Only reachable via a struct literal that bypassed the\n\t\t// constructors; Authorize fails closed on the same condition.\n\t\treturn \"\u003cunset\u003e\"\n\t}\n\tswitch auth.(type) {\n\tcase *MemberAuthority, *ContractAuthority, *AutoAcceptAuthority,\n\t\t*droppedAuthority, *contractIdentityAuthority:\n\t\treturn auth.String()\n\tdefault:\n\t\treturn ufmt.Sprintf(\"custom_authority[%s]\", auth.String())\n\t}\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":"authz_test.gno","body":"package authz\n\nimport (\n\t\"chain\"\n\t\"errors\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nfunc TestNewWithCurrent(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\n\tauth := NewWithMembers(cur.Address())\n\n\t// Check that the current authority is a MemberAuthority\n\tmemberAuth, ok := auth.Authority().(*MemberAuthority)\n\tuassert.True(t, ok, \"expected MemberAuthority\")\n\n\t// Check that the caller is a member\n\tuassert.True(t, memberAuth.Has(alice), \"caller should be a member\")\n\n\t// Check string representation\n\tuassert.True(t, strings.Contains(auth.String(), alice.String()))\n}\n\nfunc TestNewWithAuthority(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tmemberAuth := NewMemberAuthority(alice)\n\n\tauth := NewWithAuthority(memberAuth)\n\n\t// Check that the current authority is the one we provided\n\tuassert.True(t, auth.Authority() == memberAuth, \"expected provided authority\")\n}\n\nfunc TestAuthorizerAuthorize(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\n\tauth := NewWithMembers(cur.Address())\n\n\t// Test successful action with args\n\texecuted := false\n\targs := []any{\"test_arg\", 123}\n\terr := auth.DoByCurrent(0, cur, \"test_action\", func() error {\n\t\texecuted = true\n\t\treturn nil\n\t}, args...)\n\n\tuassert.True(t, err == nil, \"expected no error\")\n\tuassert.True(t, executed, \"action should have been executed\")\n\n\t// Test unauthorized action with args\n\ttesting.SetRealm(testing.NewUserRealm(testutils.TestAddress(\"bob\")))\n\n\texecuted = false\n\terr = auth.DoByCurrent(0, cur, \"test_action\", func() error {\n\t\texecuted = true\n\t\treturn nil\n\t}, \"unauthorized_arg\")\n\n\tuassert.True(t, err != nil, \"expected error\")\n\tuassert.False(t, executed, \"action should not have been executed\")\n\n\t// Test action returning error\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\texpectedErr := errors.New(\"test error\")\n\n\terr = auth.DoByCurrent(0, cur, \"test_action\", func() error {\n\t\treturn expectedErr\n\t})\n\n\tuassert.True(t, err == expectedErr, \"expected specific error\")\n}\n\nfunc TestAuthorizerTransfer(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\n\tauth := NewWithMembers(cur.Address())\n\n\t// Test transfer to new member authority\n\tbob := testutils.TestAddress(\"bob\")\n\tnewAuth := NewMemberAuthority(bob)\n\n\tvar err error\n\tfunc(cur realm) { err = auth.Transfer(0, cur, newAuth) }(cross(cur))\n\tuassert.True(t, err == nil, \"expected no error\")\n\tuassert.True(t, auth.Authority() == newAuth, \"expected new authority\")\n\n\t// Test unauthorized transfer: principal is not a member of newAuth.\n\tcarol := testutils.TestAddress(\"carol\")\n\ttesting.SetRealm(testing.NewUserRealm(carol))\n\n\tfunc(cur realm) { err = auth.Transfer(0, cur, NewMemberAuthority(alice)) }(cross(cur))\n\tuassert.True(t, err != nil, \"expected error\")\n\n\t// Test transfer to contract authority — bob is the current authority.\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\tcontractAuth := NewContractAuthority(\"gno.land/r/test\", func(title string, action PrivilegedAction) error {\n\t\treturn action()\n\t})\n\n\tfunc(cur realm) { err = auth.Transfer(0, cur, contractAuth) }(cross(cur))\n\tuassert.True(t, err == nil, \"expected no error\")\n\tuassert.True(t, auth.Authority() == contractAuth, \"expected contract authority\")\n}\n\nfunc TestAuthorizerTransferChain(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\n\t// Create a chain of transfers\n\tauth := NewWithMembers(cur.Address())\n\n\t// First transfer to a new member authority\n\tbob := testutils.TestAddress(\"bob\")\n\tmemberAuth := NewMemberAuthority(bob)\n\n\tvar err error\n\tfunc(cur realm) { err = auth.Transfer(0, cur, memberAuth) }(cross(cur))\n\tuassert.True(t, err == nil, \"unexpected error in first transfer\")\n\n\t// Then transfer to a contract authority — bob is now the authority.\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\tcontractAuth := NewContractAuthority(\"gno.land/r/test\", func(title string, action PrivilegedAction) error {\n\t\treturn action()\n\t})\n\tfunc(cur realm) { err = auth.Transfer(0, cur, contractAuth) }(cross(cur))\n\tuassert.True(t, err == nil, \"unexpected error in second transfer\")\n\n\t// Finally transfer to an auto-accept authority. The authority is now\n\t// the contract authority, whose default proposer is the contract\n\t// itself, so the transfer's caller —\n\t// cur.Previous() — must be the contract. Set the outer realm to the\n\t// contract, then cross into a closure so cur.Previous() resolves to\n\t// it.\n\tautoAuth := NewAutoAcceptAuthority()\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/test\"))\n\tfunc(cur realm) { err = auth.Transfer(0, cur, autoAuth) }(cross(cur))\n\tuassert.True(t, err == nil, \"unexpected error in final transfer\")\n\tuassert.True(t, auth.Authority() == autoAuth, \"expected auto-accept authority\")\n}\n\nfunc TestAuthorizerTransferUnauthorizedRejected(cur realm, t *testing.T) {\n\t// Regression for the address-parameter forgery: previously Transfer\n\t// took a caller-supplied `caller address` that an attacker realm could\n\t// set to the real owner. After the IsCurrent + rlm.Previous() fix, the\n\t// principal is the captured cur's previous and cannot be forged.\n\tadmin := testutils.TestAddress(\"admin\")\n\tattacker := testutils.TestAddress(\"attacker\")\n\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\tauth := NewWithMembers(cur.Address()) // admin is the initial authority\n\n\tinitialAuth, ok := auth.Authority().(*MemberAuthority)\n\tuassert.True(t, ok)\n\tuassert.True(t, initialAuth.Has(admin))\n\tuassert.False(t, initialAuth.Has(attacker))\n\n\t// Attacker context: cur.Previous() inside the closure will be attacker.\n\ttesting.SetRealm(testing.NewUserRealm(attacker))\n\tattackerAuth := NewMemberAuthority(attacker)\n\tvar err error\n\tfunc(cur realm) { err = auth.Transfer(0, cur, attackerAuth) }(cross(cur))\n\n\tuassert.True(t, err != nil, \"attacker transfer must be rejected\")\n\n\t// Authority unchanged: still the initial admin-only MemberAuthority.\n\tfinalAuth, ok := auth.Authority().(*MemberAuthority)\n\tuassert.True(t, ok)\n\tuassert.True(t, finalAuth == initialAuth, \"authority must not have changed\")\n\tuassert.True(t, finalAuth.Has(admin))\n\tuassert.False(t, finalAuth.Has(attacker))\n}\n\nfunc TestAuthorizerWithDroppedAuthority(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\n\tauth := NewWithMembers(cur.Address())\n\n\t// Transfer to dropped authority\n\tvar err error\n\tfunc(cur realm) { err = auth.Transfer(0, cur, NewDroppedAuthority()) }(cross(cur))\n\tuassert.True(t, err == nil, \"expected no error\")\n\n\t// Try to execute action\n\terr = auth.DoByCurrent(0, cur, \"test_action\", func() error {\n\t\treturn nil\n\t})\n\tuassert.True(t, err != nil, \"expected error from dropped authority\")\n\n\t// Try to transfer again\n\tfunc(cur realm) { err = auth.Transfer(0, cur, NewMemberAuthority(alice)) }(cross(cur))\n\tuassert.True(t, err != nil, \"expected error when transferring from dropped authority\")\n}\n\nfunc TestContractAuthorityHandlerExecutionOnce(cur realm, t *testing.T) {\n\tattempts := 0\n\texecuted := 0\n\n\tcontractAuth := NewContractAuthority(\"gno.land/r/test\", func(title string, action PrivilegedAction) error {\n\t\t// Try to execute the action twice in the same handler\n\t\tif err := action(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tattempts++\n\n\t\t// Second execution should fail\n\t\tif err := action(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tattempts++\n\t\treturn nil\n\t})\n\n\t// Set caller to contract address\n\tcodeRealm := testing.NewCodeRealm(\"gno.land/r/test\")\n\ttesting.SetRealm(codeRealm)\n\tcode := codeRealm.Address()\n\n\ttestArgs := []any{\"proposal_id\", 42, \"metadata\", map[string]string{\"key\": \"value\"}}\n\terr := contractAuth.Authorize(code, \"test_action\", func() error {\n\t\texecuted++\n\t\treturn nil\n\t}, testArgs...)\n\n\tuassert.True(t, err == nil, \"handler execution should succeed\")\n\tuassert.True(t, attempts == 2, \"handler should have attempted execution twice\")\n\tuassert.True(t, executed == 1, \"handler should have executed once\")\n}\n\nfunc TestContractAuthorityExecutionTwice(cur realm, t *testing.T) {\n\texecuted := 0\n\n\tcontractAuth := NewContractAuthority(\"gno.land/r/test\", func(title string, action PrivilegedAction) error {\n\t\treturn action()\n\t})\n\n\t// Set caller to contract address\n\tcodeRealm := testing.NewCodeRealm(\"gno.land/r/test\")\n\ttesting.SetRealm(codeRealm)\n\tcode := codeRealm.Address()\n\ttestArgs := []any{\"proposal_id\", 42, \"metadata\", map[string]string{\"key\": \"value\"}}\n\n\terr := contractAuth.Authorize(code, \"test_action\", func() error {\n\t\texecuted++\n\t\treturn nil\n\t}, testArgs...)\n\n\tuassert.True(t, err == nil, \"handler execution should succeed\")\n\tuassert.True(t, executed == 1, \"handler should have executed once\")\n\n\t// A new action, even with the same title, should be executed\n\terr = contractAuth.Authorize(code, \"test_action\", func() error {\n\t\texecuted++\n\t\treturn nil\n\t}, testArgs...)\n\n\tuassert.True(t, err == nil, \"handler execution should succeed\")\n\tuassert.True(t, executed == 2, \"handler should have executed twice\")\n}\n\nfunc TestContractAuthorityWithProposer(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tmemberAuth := NewMemberAuthority(alice)\n\n\thandlerCalled := false\n\tactionExecuted := false\n\n\tcontractAuth := NewRestrictedContractAuthority(\"gno.land/r/test\", func(title string, action PrivilegedAction) error {\n\t\thandlerCalled = true\n\t\t// Set caller to contract address before executing action\n\t\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/test\"))\n\t\treturn action()\n\t}, memberAuth)\n\n\t// Test authorized member\n\ttestArgs := []any{\"proposal_metadata\", \"test value\"}\n\terr := contractAuth.Authorize(alice, \"test_action\", func() error {\n\t\tactionExecuted = true\n\t\treturn nil\n\t}, testArgs...)\n\n\tuassert.True(t, err == nil, \"authorized member should be able to propose\")\n\tuassert.True(t, handlerCalled, \"contract handler should be called\")\n\tuassert.True(t, actionExecuted, \"action should be executed\")\n\n\t// Reset flags for unauthorized test\n\thandlerCalled = false\n\tactionExecuted = false\n\n\t// Test unauthorized proposer\n\tbob := testutils.TestAddress(\"bob\")\n\terr = contractAuth.Authorize(bob, \"test_action\", func() error {\n\t\tactionExecuted = true\n\t\treturn nil\n\t}, testArgs...)\n\n\tuassert.True(t, err != nil, \"unauthorized member should not be able to propose\")\n\tuassert.False(t, handlerCalled, \"contract handler should not be called for unauthorized proposer\")\n\tuassert.False(t, actionExecuted, \"action should not be executed for unauthorized proposer\")\n}\n\nfunc TestAutoAcceptAuthority(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tauth := NewAutoAcceptAuthority()\n\n\t// Test that any action is authorized\n\texecuted := false\n\terr := auth.Authorize(alice, \"test_action\", func() error {\n\t\texecuted = true\n\t\treturn nil\n\t})\n\n\tuassert.True(t, err == nil, \"auto-accept should not return error\")\n\tuassert.True(t, executed, \"action should have been executed\")\n\n\t// Test with different caller\n\trandom := testutils.TestAddress(\"random\")\n\texecuted = false\n\terr = auth.Authorize(random, \"test_action\", func() error {\n\t\texecuted = true\n\t\treturn nil\n\t})\n\n\tuassert.True(t, err == nil, \"auto-accept should not care about caller\")\n\tuassert.True(t, executed, \"action should have been executed\")\n}\n\nfunc TestAutoAcceptAuthorityWithArgs(cur realm, t *testing.T) {\n\tauth := NewAutoAcceptAuthority()\n\tanyuser := testutils.TestAddress(\"anyuser\")\n\n\t// Test that any action is authorized with args\n\texecuted := false\n\ttestArgs := []any{\"arg1\", 42, \"arg3\"}\n\terr := auth.Authorize(anyuser, \"test_action\", func() error {\n\t\texecuted = true\n\t\treturn nil\n\t}, testArgs...)\n\n\tuassert.True(t, err == nil, \"auto-accept should not return error\")\n\tuassert.True(t, executed, \"action should have been executed\")\n}\n\nfunc TestMemberAuthorityMultipleMembers(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\tcarol := testutils.TestAddress(\"carol\")\n\n\t// Create authority with multiple members\n\tauth := NewMemberAuthority(alice, bob)\n\n\t// Test that both members can execute actions\n\tfor _, member := range []address{alice, bob} {\n\t\terr := auth.Authorize(member, \"test_action\", func() error {\n\t\t\treturn nil\n\t\t})\n\t\tuassert.True(t, err == nil, \"member should be authorized\")\n\t}\n\n\t// Test that non-member cannot execute\n\terr := auth.Authorize(carol, \"test_action\", func() error {\n\t\treturn nil\n\t})\n\tuassert.True(t, err != nil, \"non-member should not be authorized\")\n\n\t// Test Tree() functionality\n\ttree := auth.Tree()\n\tuassert.True(t, tree.Size() == 2, \"tree should have 2 members\")\n\n\t// Verify both members are in the tree\n\tfound := make(map[address]bool)\n\ttree.Iterate(\"\", \"\", func(key string, _ any) bool {\n\t\tfound[address(key)] = true\n\t\treturn false\n\t})\n\tuassert.True(t, found[alice], \"alice should be in the tree\")\n\tuassert.True(t, found[bob], \"bob should be in the tree\")\n\tuassert.False(t, found[carol], \"carol should not be in the tree\")\n\n\t// Test read-only nature of the tree\n\tdefer func() {\n\t\tr := recover()\n\t\tuassert.True(t, r != nil, \"modifying read-only tree should panic\")\n\t}()\n\ttree.Set(string(carol), nil) // This should panic\n}\n\nfunc TestAuthorizerCurrentNeverNil(cur realm, t *testing.T) {\n\tauth := NewWithMembers(cur.Address())\n\n\t// Authority should never be nil after initialization\n\tuassert.True(t, auth.Authority() != nil, \"current authority should not be nil\")\n\n\t// Authority should not be nil after transfer\n\tvar err error\n\tfunc(cur realm) { err = auth.Transfer(0, cur, NewAutoAcceptAuthority()) }(cross(cur))\n\tuassert.True(t, err == nil, \"transfer should succeed\")\n\tuassert.True(t, auth.Authority() != nil, \"current authority should not be nil after transfer\")\n}\n\nfunc TestContractAuthorityValidation(cur realm, t *testing.T) {\n\thandler := func(title string, action PrivilegedAction) error {\n\t\treturn nil\n\t}\n\n\t// Empty path panics (consistent with NewRestrictedContractAuthority).\n\tuassert.PanicsWithMessage(t, cur, \"contract path cannot be empty\", func() {\n\t\tNewContractAuthority(\"\", handler)\n\t})\n\n\t// A nil handler panics at CONSTRUCTION rather than being tolerated and\n\t// surfaced at Authorize time. Tolerating it produced a permanent brick:\n\t// see TestNilHandlerWouldBeUnrotatable below.\n\tuassert.PanicsWithMessage(t, cur, \"contract handler cannot be nil\", func() {\n\t\tNewContractAuthority(\"gno.land/r/test\", nil)\n\t})\n\n\t// The Authorize-time guard stays for a zero-value ContractAuthority,\n\t// which the constructors cannot produce but a struct literal can.\n\tcode := testing.NewCodeRealm(\"gno.land/r/test\").Address()\n\tzero := \u0026ContractAuthority{contractPath: \"gno.land/r/test\", contractAddr: code}\n\terr := zero.Authorize(code, \"test\", func() error {\n\t\treturn nil\n\t})\n\tuassert.True(t, err != nil, \"nil handler authority should fail to authorize\")\n\n\t// Test valid configuration\n\tcontractAuth := NewContractAuthority(\"gno.land/r/test\", handler)\n\terr = contractAuth.Authorize(code, \"test\", func() error {\n\t\treturn nil\n\t})\n\tuassert.True(t, err == nil, \"valid contract authority should authorize successfully\")\n}\n\nfunc TestAuthorizerString(cur realm, t *testing.T) {\n\tauth := NewWithMembers(cur.Address())\n\taddr := cur.Address()\n\n\t// Test initial string representation\n\tstr := auth.String()\n\tuassert.Equal(t, str, \"member_authority[\"+string(addr)+\"]\")\n\n\t// Test string after transfer — caller is the current member (cur).\n\tautoAuth := NewAutoAcceptAuthority()\n\tvar err error\n\tfunc(cur realm) { err = auth.Transfer(0, cur, autoAuth) }(cross(cur))\n\tuassert.True(t, err == nil, \"transfer should succeed\")\n\tstr = auth.String()\n\tuassert.Equal(t, str, \"auto_accept_authority\")\n\n\t// Test custom authority — auto-accept lets anyone transfer.\n\tcustomAuth := \u0026mockAuthority{}\n\tfunc(cur realm) { err = auth.Transfer(0, cur, customAuth) }(cross(cur))\n\tuassert.True(t, err == nil, \"transfer should succeed\")\n\tstr = auth.String()\n\tuassert.Equal(t, str, \"custom_authority[mock]\")\n}\n\ntype mockAuthority struct{}\n\nfunc (c mockAuthority) String() string { return \"mock\" }\nfunc (a mockAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {\n\t// autoaccept\n\treturn action()\n}\n\nfunc TestAuthorityString(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\n\t// MemberAuthority\n\tmemberAuth := NewMemberAuthority(alice)\n\tmemberStr := memberAuth.String()\n\texpectedMemberStr := \"member_authority[g1v9kxjcm9ta047h6lta047h6lta047h6lzd40gh]\"\n\tuassert.Equal(t, memberStr, expectedMemberStr)\n\n\t// ContractAuthority — the proposer is rendered, not just the path.\n\t// Without it the two constructors below are indistinguishable, which\n\t// is what made consumer-level assertions on this string blind to an\n\t// authority being swapped for a wide-open one.\n\tcontractAuth := NewContractAuthority(\"gno.land/r/test\", func(title string, action PrivilegedAction) error { return nil })\n\tcontractStr := contractAuth.String()\n\texpectedContractStr := \"contract_authority[contract=gno.land/r/test,proposer=contract-identity]\"\n\tuassert.Equal(t, contractStr, expectedContractStr)\n\n\t// Same path, same handler, explicit open proposer — must render\n\t// differently from the gated default above.\n\topenAuth := NewRestrictedContractAuthority(\n\t\t\"gno.land/r/test\",\n\t\tfunc(title string, action PrivilegedAction) error { return nil },\n\t\tNewAutoAcceptAuthority(),\n\t)\n\tuassert.Equal(t,\n\t\t\"contract_authority[contract=gno.land/r/test,proposer=auto_accept_authority]\",\n\t\topenAuth.String())\n\tuassert.NotEqual(t, contractStr, openAuth.String())\n\n\t// AutoAcceptAuthority\n\tautoAuth := NewAutoAcceptAuthority()\n\tautoStr := autoAuth.String()\n\texpectedAutoStr := \"auto_accept_authority\"\n\tuassert.Equal(t, autoStr, expectedAutoStr)\n\n\t// DroppedAuthority\n\tdroppedAuth := NewDroppedAuthority()\n\tdroppedStr := droppedAuth.String()\n\texpectedDroppedStr := \"dropped_authority\"\n\tuassert.Equal(t, droppedStr, expectedDroppedStr)\n}\n\n// TestContractAuthorityUnauthorizedCaller verifies the\n// fix: a ContractAuthority's default proposer is the contract itself, so\n// any other caller is rejected UPSTREAM (at the proposer) and the handler\n// and privileged action never run. Previously the default proposer was\n// AutoAcceptAuthority, and the only guard was a handler-side\n// `unsafe.CurrentRealm() == contractAddr` check — which was bypassable\n// and which many consumers (e.g. r/gnops/valopers) omitted entirely,\n// leaving no caller check at all.\nfunc TestContractAuthorityUnauthorizedCaller(cur realm, t *testing.T) {\n\tcontractPath := \"gno.land/r/testcontract\"\n\tcontractAddr := chain.PackageAddress(contractPath)\n\tunauthorizedAddr := testutils.TestAddress(\"unauthorized\")\n\n\t// A permissive handler that simply runs the action — the realistic\n\t// shape a consumer registers. The package, not the handler, must be\n\t// the one that rejects an unauthorized caller.\n\thandlerExecuted := false\n\tcontractHandler := func(title string, action PrivilegedAction) error {\n\t\thandlerExecuted = true\n\t\treturn action()\n\t}\n\tcontractAuth := NewContractAuthority(contractPath, contractHandler)\n\n\tactionExecuted := false\n\tprivilegedAction := func() error {\n\t\tactionExecuted = true\n\t\treturn nil\n\t}\n\n\t// 1. Unauthorized caller: rejected by the contract-identity proposer\n\t// before the handler or the action can run.\n\terr := contractAuth.Authorize(unauthorizedAddr, \"test_action\", privilegedAction)\n\tuassert.Error(t, err, \"unauthorized caller must be rejected\")\n\tuassert.ErrorContains(t, err, \"unauthorized\", \"rejection must come from the contract-identity proposer\")\n\tuassert.False(t, handlerExecuted, \"handler must not run for an unauthorized caller\")\n\tuassert.False(t, actionExecuted, \"privileged action must not run for an unauthorized caller\")\n\n\t// 2. The contract itself is authorized.\n\terr = contractAuth.Authorize(contractAddr, \"test_action\", privilegedAction)\n\tuassert.NoError(t, err, \"the contract itself must be authorized\")\n\tuassert.True(t, handlerExecuted, \"handler must run when the contract is the caller\")\n\tuassert.True(t, actionExecuted, \"privileged action must run when the contract is the caller\")\n}\n\n// TestAuthorizerDoByPrevious verifies the \"calling realm authorizes\"\n// pattern: a function (the inner crossing closure) invokes\n// DoByPrevious so the authority check sees cur.Previous() — the realm\n// that crossed into it — not the function's own realm.\n//\n// Each scenario crosses into the inner closure via cross(cur) after\n// SetRealm — inside the closure, cur is the fresh live cur and\n// cur.Previous() is the SetRealm'd outer realm. This is the only way\n// to exercise DoByPrevious correctly under the IsCurrent guard, which\n// rejects synthetic realm values (testing.MakeRealm) and stored\n// stale captures.\nfunc TestAuthorizerDoByPrevious(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\n\tauth := NewWithMembers(alice)\n\n\t// alice (member) crosses in: cur.Previous() == alice inside the inner closure.\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\texecuted := false\n\targs := []any{\"test_arg\", 123}\n\tfunc(cur realm) {\n\t\terr := auth.DoByPrevious(0, cur, \"test_action\", func() error {\n\t\t\texecuted = true\n\t\t\treturn nil\n\t\t}, args...)\n\t\tuassert.NoError(t, err, \"expected no error\")\n\t\tuassert.True(t, executed, \"action should have been executed\")\n\t}(cross(cur))\n\n\texpectedErr := errors.New(\"test error\")\n\tfunc(cur realm) {\n\t\terr := auth.DoByPrevious(0, cur, \"test_action\", func() error {\n\t\t\treturn expectedErr\n\t\t})\n\t\tuassert.ErrorContains(t, err, expectedErr.Error(), \"expected error\")\n\t}(cross(cur))\n\n\t// bob (not a member) crosses in: Authorize must reject.\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\texecuted = false\n\tfunc(cur realm) {\n\t\terr := auth.DoByPrevious(0, cur, \"test_action\", func() error {\n\t\t\texecuted = true\n\t\t\treturn nil\n\t\t}, \"unauthorized_arg\")\n\t\tuassert.ErrorContains(t, err, \"unauthorized\", \"expected error\")\n\t\tuassert.False(t, executed, \"action should not have been executed\")\n\t}(cross(cur))\n}\n\n// ---------------------------------------------------------------------------\n// Contract-identity gate: regression suite.\n//\n// A plain NewContractAuthority now defaults its proposer to the contract\n// ITSELF (MemberAuthority of the contract's own package address) instead of\n// AutoAcceptAuthority. These tests pin the security contract: only the\n// contract may drive privileged actions, an external caller can neither\n// drive nor Transfer the authority, the contract can still rotate its own\n// authority, and the escape hatch for a genuinely-open proposer still\n// exists.\n// ---------------------------------------------------------------------------\n\n// The default proposer accepts the contract itself and rejects everyone\n// else, before the handler or action can run.\nfunc TestDefaultProposerIsContractOnly(cur realm, t *testing.T) {\n\tconst path = \"gno.land/r/defcontract\"\n\tcontractAddr := chain.PackageAddress(path)\n\n\tran := 0\n\tca := NewContractAuthority(path, func(_ string, action PrivilegedAction) error {\n\t\treturn action()\n\t})\n\n\t// The contract itself: authorized.\n\terr := ca.Authorize(contractAddr, \"action\", func() error { ran++; return nil })\n\tuassert.NoError(t, err, \"the contract itself must be authorized\")\n\n\t// Anyone else: rejected at the proposer, action never runs.\n\toutsider := testutils.TestAddress(\"outsider\")\n\terr = ca.Authorize(outsider, \"action\", func() error { ran++; return nil })\n\tuassert.Error(t, err, \"a non-contract caller must be rejected\")\n\tuassert.ErrorContains(t, err, \"unauthorized\", \"rejection must come from the contract-identity proposer\")\n\n\tuassert.Equal(t, 1, ran, \"only the contract's action must have run\")\n}\n\n// An external realm that holds another realm's Authorizer cannot Transfer\n// it. The caller derived under IsCurrent is that external realm, not the\n// contract, so the rotation is rejected and the authority is left intact.\nfunc TestExternalTransferRejected(cur realm, t *testing.T) {\n\tauthorizer := NewWithAuthority(\n\t\tNewContractAuthority(\"gno.land/r/victim\", func(_ string, action PrivilegedAction) error {\n\t\t\treturn action()\n\t\t}),\n\t)\n\n\tattacker := testutils.TestAddress(\"attacker\")\n\ttesting.SetRealm(testing.NewUserRealm(attacker))\n\n\tvar err error\n\tfunc(cur realm) { err = authorizer.Transfer(0, cur, NewDroppedAuthority()) }(cross(cur))\n\n\tuassert.Error(t, err, \"external Transfer must be rejected\")\n\tuassert.ErrorContains(t, err, \"unauthorized\", \"rejection must come from the contract-identity proposer\")\n\n\t_, ok := authorizer.Authority().(*ContractAuthority)\n\tuassert.True(t, ok, \"authority must be unchanged after a rejected rotation\")\n}\n\n// Same shape via DoByPrevious (a privileged action instead of a\n// Transfer): an external caller is rejected and the action never runs.\nfunc TestExternalDoByPreviousRejected(cur realm, t *testing.T) {\n\tauthorizer := NewWithAuthority(\n\t\tNewContractAuthority(\"gno.land/r/victim2\", func(_ string, action PrivilegedAction) error {\n\t\t\treturn action()\n\t\t}),\n\t)\n\n\tran := false\n\ttesting.SetRealm(testing.NewUserRealm(testutils.TestAddress(\"randomuser\")))\n\tvar err error\n\tfunc(cur realm) {\n\t\terr = authorizer.DoByPrevious(0, cur, \"privileged\", func() error { ran = true; return nil })\n\t}(cross(cur))\n\n\tuassert.Error(t, err, \"external DoByPrevious must be rejected\")\n\tuassert.False(t, ran, \"privileged action must not run for an external caller\")\n}\n\n// The contract-identity model does NOT lock out legitimate governance: the\n// contract can still rotate (Transfer) its own authority when the transfer\n// is driven with the contract as the previous realm (caller == contractAddr).\nfunc TestContractCanRotateItsOwnAuthority(cur realm, t *testing.T) {\n\tconst path = \"gno.land/r/selfgov\"\n\tauthorizer := NewWithAuthority(\n\t\tNewContractAuthority(path, func(_ string, action PrivilegedAction) error {\n\t\t\treturn action()\n\t\t}),\n\t)\n\n\t// Drive the transfer with the contract as the caller: set the outer\n\t// realm to the contract, then cross into a closure so cur.Previous()\n\t// resolves to the contract.\n\ttesting.SetRealm(testing.NewCodeRealm(path))\n\tvar err error\n\tfunc(cur realm) { err = authorizer.Transfer(0, cur, NewAutoAcceptAuthority()) }(cross(cur))\n\n\tuassert.NoError(t, err, \"the contract itself must be able to rotate its authority\")\n\t_, ok := authorizer.Authority().(*AutoAcceptAuthority)\n\tuassert.True(t, ok, \"authority should have rotated to auto-accept\")\n}\n\n// Boundary test: the raw Authority.Authorize takes `caller` as a plain\n// parameter, so a holder of the raw Authority can forge caller ==\n// contractAddr. This proves that even so, a forged-caller raw Authorize\n// only runs the action the caller itself supplies — it CANNOT mutate an\n// Authorizer's installed authority, because the Transfer mutation lives\n// only inside Authorizer.Transfer (which derives caller non-forgeably).\n// The lesson encoded here: consumers must reach a ContractAuthority through\n// the Authorizer wrapper and must not hand out the raw Authority.\n//\n// WHAT THIS TEST DOES AND DOES NOT FREEZE. Only the final assertion is a\n// security property. That raw Authorize ACCEPTS a forged caller is\n// current behaviour, not a guarantee — it is the consequence of `caller`\n// being a parameter on an exported interface method. If someone later\n// hardens that path so raw Authorize stops accepting a forged caller,\n// this test going red means the test is stale, NOT that the change is\n// wrong: delete the two intermediate assertions and keep the last one.\n// (Recorded because asserting NoError here reads like the forgeability\n// is wanted. It is tolerated, and only because of what follows.)\nfunc TestForgedCallerCannotTransfer(cur realm, t *testing.T) {\n\tconst path = \"gno.land/r/boundary\"\n\tcontractAddr := chain.PackageAddress(path)\n\n\tauthorizer := NewWithAuthority(\n\t\tNewContractAuthority(path, func(_ string, action PrivilegedAction) error {\n\t\t\treturn action()\n\t\t}),\n\t)\n\n\t// Attacker obtains the raw Authority and forges the contract as caller.\n\tattackerRan := false\n\terr := authorizer.Authority().Authorize(contractAddr, \"transfer_authority\", func() error {\n\t\tattackerRan = true\n\t\treturn nil\n\t})\n\n\t// Current behaviour, documented above — not a property to preserve.\n\tuassert.NoError(t, err, \"raw Authorize currently accepts a forged caller (see comment)\")\n\tuassert.True(t, attackerRan, \"only the caller's own inert closure runs\")\n\n\t// THE property: the installed authority is UNCHANGED. Raw Authorize\n\t// cannot perform a Transfer — the mutation closure is private to\n\t// Authorizer.Transfer, which derives caller non-forgeably.\n\t_, ok := authorizer.Authority().(*ContractAuthority)\n\tuassert.True(t, ok, \"raw Authorize must not be able to transfer the authority\")\n}\n\n// Two contract authorities on different paths never cross-authorize.\nfunc TestContractPathIsolation(cur realm, t *testing.T) {\n\tcaB := NewContractAuthority(\"gno.land/r/pathb\", func(_ string, action PrivilegedAction) error {\n\t\treturn action()\n\t})\n\taddrA := chain.PackageAddress(\"gno.land/r/patha\")\n\n\tran := false\n\terr := caB.Authorize(addrA, \"action\", func() error { ran = true; return nil })\n\tuassert.Error(t, err, \"contract A must not authorize contract B's authority\")\n\tuassert.False(t, ran, \"action must not run across contract identities\")\n}\n\n// Escape hatch: a consumer that genuinely wants open proposals can still\n// opt in via NewRestrictedContractAuthority with an AutoAcceptAuthority\n// proposer — restoring the pre-fix \"anyone can propose\" behavior explicitly.\nfunc TestRestrictedAutoAcceptEscapeHatch(cur realm, t *testing.T) {\n\tca := NewRestrictedContractAuthority(\n\t\t\"gno.land/r/open\",\n\t\tfunc(_ string, action PrivilegedAction) error { return action() },\n\t\tNewAutoAcceptAuthority(),\n\t)\n\n\tran := false\n\terr := ca.Authorize(testutils.TestAddress(\"anyone\"), \"action\", func() error { ran = true; return nil })\n\tuassert.NoError(t, err, \"an explicit AutoAccept proposer restores open proposals\")\n\tuassert.True(t, ran, \"action must run under an explicit AutoAccept proposer\")\n}\n\n// Why the nil handler is now rejected at construction rather than\n// tolerated: Authorize checks contractHandler == nil BEFORE consulting the\n// proposer, and Transfer routes through Authorize, so there is no rotation\n// path out. It is a permanent brick reachable by an ordinary deployer\n// typo -- the same bricked-governance failure mode, by accident.\n//\n// The zero value is used because the constructor no longer allows it.\nfunc TestNilHandlerWouldBeUnrotatable(cur realm, t *testing.T) {\n\tconst path = \"gno.land/r/nilbrick\"\n\taddr := chain.PackageAddress(path)\n\tauthorizer := NewWithAuthority(\u0026ContractAuthority{\n\t\tcontractPath: path,\n\t\tcontractAddr: addr,\n\t\t// Both handler and proposer are nil: a struct literal is the only\n\t\t// way to reach this, and Authorize fails closed on either.\n\t})\n\n\t// Drive the rotation as the contract itself -- the only principal the\n\t// default proposer accepts. If this cannot escape, nobody can.\n\ttesting.SetRealm(testing.NewCodeRealm(path))\n\tvar err error\n\tfunc(cur realm) { err = authorizer.Transfer(0, cur, NewAutoAcceptAuthority()) }(cross(cur))\n\n\tuassert.Error(t, err, \"a nil-handler authority must not be silently rotatable\")\n\t_, stuck := authorizer.Authority().(*ContractAuthority)\n\tuassert.True(t, stuck, \"nil-handler authority is unrotatable -- hence rejected at construction\")\n}\n\n// A crossing closure declared HERE carries this package's identity, not\n// the contract path's -- testing.SetRealm does not change that, because\n// the VM mints a crossing frame's realm from the callee's declaring\n// package. So the DoByCurrent shape that Example_contractAuthority teaches\n// cannot be exercised from inside this package at all; it needs a real\n// realm. It is covered by filetests/z_contract_authority_shape_filetest.gno.\n//\n// The same property is why an exported crossing closure leaks a realm's\n// authority to any caller -- it is one mechanism seen from two\n// sides. This test pins the half that is observable here: an unrelated\n// package's frame is rejected.\nfunc TestForeignFrameCannotDriveContractAuthority(cur realm, t *testing.T) {\n\tauth := NewWithAuthority(NewContractAuthority(\"gno.land/r/demo/dao\", mockDAOHandler))\n\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/example\"))\n\tran := false\n\tvar err error\n\tfunc(cur realm) {\n\t\terr = auth.DoByCurrent(0, cur, \"update_params\", func() error {\n\t\t\tran = true\n\t\t\treturn nil\n\t\t})\n\t}(cross(cur))\n\n\tuassert.Error(t, err, \"a frame from another package must not drive this authority\")\n\tuassert.False(t, ran, \"the action must not run\")\n}\n\n// A malformed contract path binds the authority to an address no realm can\n// ever present. Because Transfer routes through the same gate, such an\n// authority is also unrotatable: a permanent brick, and the same failure\n// mode the nil-handler panic exists to prevent. Rejected at construction.\n//\n// Pre-fix these all constructed happily; the AutoAccept default made the\n// typo harmless (and insecure), so nothing complained.\nfunc TestContractPathValidationRejectsMalformed(cur realm, t *testing.T) {\n\thandler := func(_ string, action PrivilegedAction) error { return action() }\n\n\tfor _, path := range []string{\n\t\t\"gno.land/r/gov/dao \",  // trailing space\n\t\t\" gno.land/r/gov/dao\",  // leading space\n\t\t\"gno.land/r/gov/dao\\n\", // trailing newline\n\t\t\"gno.land/r/gov dao\",   // embedded space\n\t\t\" \",\n\t\t\"not a path\",\n\t\t\"GNO.LAND/R/GOV/DAO\", // uppercase: not a legal gno pkgpath\n\t\t\"gno.land//r/gov/dao\",\n\t\t\"gno.land/r/gov/dao/\",\n\t\t\"singlesegment\",\n\t} {\n\t\tuassert.PanicsContains(t, cur, \"contract path\", func() {\n\t\t\tNewContractAuthority(path, handler)\n\t\t}, \"malformed path must be rejected: \"+path)\n\t\tuassert.PanicsContains(t, cur, \"contract path\", func() {\n\t\t\tNewRestrictedContractAuthority(path, handler, NewAutoAcceptAuthority())\n\t\t}, \"malformed path must be rejected by the restricted ctor too: \"+path)\n\t}\n\n\t// Real paths still construct, including dashes, digits, underscores and\n\t// version suffixes.\n\tfor _, path := range []string{\n\t\t\"gno.land/r/gov/dao\",\n\t\t\"gno.land/r/gnops/valopers\",\n\t\t\"gno.land/p/moul/authz/v0\",\n\t\t\"gno.land/r/sys/validators/v0\",\n\t\t\"gno.land/r/some-user/my_pkg2\",\n\t} {\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tNewContractAuthority(path, handler)\n\t\t}, \"legitimate path must construct: \"+path)\n\t}\n}\n\n// spoofProposer is a wide-open Authority that LIES in String(), returning\n// the same text the canonical contract-identity default renders.\ntype spoofProposer struct{}\n\nfunc (spoofProposer) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {\n\treturn action()\n}\n\nfunc (spoofProposer) String() string { return \"contract-identity\" }\n\n// A foreign proposer must not be able to impersonate the gated default in\n// the rendered description. ContractAuthority.String() is promoted by this\n// package as the one surface distinguishing a gated authority from a\n// wide-open one, and consumers assert on it (r/gnops/valopers.Auth()). An\n// impl that chooses its own String() text defeated that: a fully permissive\n// authority rendered byte-identical to the default, so every string-based\n// configuration pin stayed green while the gate was gone.\nfunc TestSpoofedProposerCannotImpersonateContractIdentity(cur realm, t *testing.T) {\n\tconst path = \"gno.land/r/spoofprobe\"\n\thandler := func(_ string, action PrivilegedAction) error { return action() }\n\toutsider := chain.PackageAddress(\"gno.land/r/outsider\")\n\n\tgated := NewContractAuthority(path, handler)\n\tspoof := NewRestrictedContractAuthority(path, handler, spoofProposer{})\n\n\t// The behavioural difference the strings must reflect.\n\tuassert.Error(t, gated.Authorize(outsider, \"t\", func() error { return nil }),\n\t\t\"the gated default must reject an outsider\")\n\tuassert.NoError(t, spoof.Authorize(outsider, \"t\", func() error { return nil }),\n\t\t\"the spoofing proposer is wide open -- that is the point of the test\")\n\n\t// ...and they do.\n\tuassert.NotEqual(t, gated.String(), spoof.String(),\n\t\t\"a wide-open authority must not render identically to the gated default\")\n\tuassert.Equal(t,\n\t\t\"contract_authority[contract=\"+path+\",proposer=contract-identity]\",\n\t\tgated.String())\n\tuassert.Equal(t,\n\t\t\"contract_authority[contract=\"+path+\",proposer=custom_authority[contract-identity]]\",\n\t\tspoof.String(),\n\t\t\"a non-canonical proposer must be wrapped, not trusted to name itself\")\n\n\t// Same guarantee through the Authorizer wrapper, which is the shape a\n\t// consumer realm actually exposes.\n\tuassert.NotEqual(t,\n\t\tNewWithAuthority(gated).String(),\n\t\tNewWithAuthority(spoof).String())\n}\n\n// An explicit proposer REPLACES the contract-identity gate; it does not add\n// to it. NewRestrictedContractAuthority(govdaoPath, h, member(alice)) means\n// \"alice, and not GovDAO\" -- contractAddr is never consulted. Pinned because\n// the rendered string reads like a conjunction and the old godoc said\n// \"widen\", so a consumer could reasonably have expected \"both\".\nfunc TestExplicitProposerReplacesIdentityGate(cur realm, t *testing.T) {\n\tconst path = \"gno.land/r/gov/dao\"\n\thandler := func(_ string, action PrivilegedAction) error { return action() }\n\tgovdao := chain.PackageAddress(path)\n\talice := chain.PackageAddress(\"gno.land/r/alice\")\n\n\tgated := NewContractAuthority(path, handler)\n\treplaced := NewRestrictedContractAuthority(path, handler, NewMemberAuthority(alice))\n\n\tuassert.NoError(t, gated.Authorize(govdao, \"x\", func() error { return nil }),\n\t\t\"the default gate accepts the bound contract\")\n\tuassert.Error(t, gated.Authorize(alice, \"x\", func() error { return nil }),\n\t\t\"the default gate rejects everyone else\")\n\n\tuassert.Error(t, replaced.Authorize(govdao, \"x\", func() error { return nil }),\n\t\t\"an explicit proposer REPLACES the identity gate: govdao is no longer accepted\")\n\tuassert.NoError(t, replaced.Authorize(alice, \"x\", func() error { return nil }),\n\t\t\"only the explicit proposer's principal is accepted\")\n}\n"},{"name":"example_test.gno","body":"package authz\n\n// Example_basic demonstrates initializing and using a basic member authority\nfunc Example_basic(cur realm) {\n\t// Initialize from the EOA caller (e.g. in init(cur realm) of a realm\n\t// being deployed): caller passes the EOA address; the realm itself\n\t// is responsible for verifying the caller is an EOA when needed.\n\tauth := NewWithMembers(cur.Previous().Address())\n\n\t// Authorize with DoByPrevious, NOT DoByCurrent: the member set holds\n\t// cur.Previous().Address(), and DoByCurrent would present\n\t// cur.Address() -- this realm -- which is never in that set, so the\n\t// action would silently never run. Seeding and authorizing must read\n\t// the SAME side of the frame. Matches the package Quick Start.\n\tif err := auth.DoByPrevious(0, cur, \"update_config\", func() error {\n\t\t// config = newValue\n\t\treturn nil\n\t}); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Example_addingMembers demonstrates how to add new members to a member authority\nfunc Example_addingMembers(cur realm) {\n\t// Seed with Previous(), not Address(): MemberAuthority.AddMember\n\t// derives its principal as rlm.Previous().Address(), so a set holding\n\t// only cur.Address() authorizes nobody and the AddMember below fails.\n\tauth := NewWithMembers(cur.Previous().Address())\n\n\t// Add a new member to the authority\n\tmemberAuth := auth.Authority().(*MemberAuthority)\n\tif err := memberAuth.AddMember(0, cur, address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Example_contractAuthority demonstrates a contract-based authority.\n//\n// A plain NewContractAuthority accepts exactly one principal:\n// chain.PackageAddress(path). Point `path` at the principal you want to\n// ASSERT -- normally the governance realm that will drive the action --\n// and authorize with DoByPrevious, so the comparison is against whoever\n// crossed in. See NewContractAuthority's godoc.\n//\n// Do NOT pair a plain ContractAuthority on your OWN path with\n// DoByCurrent. rlm.Address() inside any crossing frame of a realm is\n// unconditionally PackageAddress(ownPath), so that gate compares the\n// realm to itself, can never reject, and any exported crossing\n// entrypoint next to it becomes an unauthenticated privileged write --\n// the self-comparing-gate shape. Own-path is right only when something\n// OTHER than your own frame supplies the caller, i.e. DoByPrevious from\n// a realm you deliberately let call in, or Transfer.\n//\n// Nothing here catches a wrong pairing: Example_* functions that take\n// any parameter are never executed by `gno test` (see isExampleFunc in\n// gnovm/pkg/test/test.go -- it rejects on method receiver, parameters or\n// results; \"// Output:\" plays no part in the predicate, so adding one\n// does NOT make the body run), so a sentinel panic in here still reports\n// ok. filetests/z_contract_authority_shape_filetest.gno executes the\n// gate from a real realm instead.\n//\n// Corollary of the identity gate: do not export anything that returns a\n// crossing closure of this realm, or callers inherit the authority.\nfunc Example_contractAuthority(cur realm) {\n\t// This realm asserts \"the governance realm governs me\".\n\tauth := NewWithAuthority(\n\t\tNewContractAuthority(\n\t\t\t\"gno.land/r/gov/dao\", // the principal asserted, not this realm\n\t\t\tmockDAOHandler,       // defined elsewhere for example\n\t\t),\n\t)\n\n\t// Authorized only when gno.land/r/gov/dao is the realm that crossed\n\t// into this function -- in practice, when a passed proposal executes.\n\t// Every other caller presents its own address here and is refused,\n\t// which is the whole point of the gate. Never discard this error.\n\tif err := auth.DoByPrevious(0, cur, \"update_params\", func() error {\n\t\treturn nil\n\t}); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Example_restrictedContractAuthority demonstrates a contract authority with member-only proposals\nfunc Example_restrictedContractAuthority(cur realm) {\n\t// Initialize member authority for proposers\n\tproposerAuth := NewMemberAuthority(\n\t\taddress(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"), // admin1\n\t\taddress(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\"), // admin2\n\t)\n\n\t// Create contract authority with restricted proposers\n\tauth := NewWithAuthority(\n\t\tNewRestrictedContractAuthority(\n\t\t\t\"gno.land/r/demo/dao\",\n\t\t\tmockDAOHandler,\n\t\t\tproposerAuth,\n\t\t),\n\t)\n\n\t// Only members can propose, and contract must approve.\n\t//\n\t// DoByPrevious, not DoByCurrent: the proposer set holds EOA\n\t// addresses, and DoByCurrent would present this realm's own address,\n\t// which is in no member set -- the proposer would reject and the\n\t// action would silently never run.\n\tif err := auth.DoByPrevious(0, cur, \"update_params\", func() error {\n\t\t// Executes after:\n\t\t// 1. Proposer initiates\n\t\t// 2. DAO approves\n\t\treturn nil\n\t}); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Example_switchingAuthority demonstrates switching from member to contract\n// authority.\n//\n// Note what a plain ContractAuthority on a FOREIGN path means after the\n// transfer: this realm loses the ability to take the authority back, and\n// DoByCurrent against it is dead. It is NOT a one-way door in the\n// stronger sense an earlier draft implied — the foreign realm can still\n// drive and rotate it via Previous() once it crosses in, which is the\n// intended async-DAO shape. What is given up is the original owner's\n// claim, not the authority's mutability. (The ADR for this change states\n// the same thing; keep the two in step.) To keep the authority here\n// while letting others propose, transfer to\n// NewRestrictedContractAuthority(ownPath, handler, proposerAuth)\n// instead.\nfunc Example_switchingAuthority(cur realm) {\n\t// Start with member authority.\n\t//\n\t// Seed the member set with Previous(), not Address(): Transfer\n\t// derives its principal as rlm.Previous().Address(), so a set\n\t// containing only cur.Address() authorizes nobody but a\n\t// same-package cross-call, and the Transfer below would abort for\n\t// every external caller — including a realm that copies this into\n\t// init(cur realm), which then fails its deploy tx. Matches\n\t// Example_basic and NewWithMembers' godoc.\n\tauth := NewWithMembers(cur.Previous().Address())\n\n\t// Create and switch to contract authority — control moves to\n\t// gno.land/r/demo/dao.\n\tdaoAuthority := NewContractAuthority(\n\t\t\"gno.land/r/demo/dao\",\n\t\tmockDAOHandler,\n\t)\n\tif err := auth.Transfer(0, cur, daoAuthority); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Mock handler for examples\nfunc mockDAOHandler(title string, action PrivilegedAction) error {\n\treturn action()\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/authz/v0\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"},{"name":"z_contract_authority_shape_filetest.gno","body":"// PKGPATH: gno.land/r/example\n//\n// Executes the contract-identity gate from a real realm.\n//\n// Two reasons this cannot be a plain unit test in p/moul/authz:\n//\n//  1. Example_* functions that take parameters and have no \"// Output:\"\n//     are never run by `gno test` — a sentinel panic inside one still\n//     reports ok. So the package's headline example, the shape readers\n//     copy, is invisible to CI unless something else runs it.\n//  2. The gate cannot be simulated from inside the package. The VM mints\n//     a crossing frame's realm from the CALLEE's declaring package, so a\n//     closure declared in p/moul/authz always presents that package's\n//     address no matter what testing.SetRealm says. Only a realm can\n//     present its own.\n//\n// (2) is the same mechanism as the capability leak, seen from the other\n// side: it is what makes `rlm.Address()` inside a crossing frame\n// unconditionally the realm's own address — and therefore what makes\n// `NewContractAuthority(ownPath)` + `DoByCurrent` a comparison of the\n// realm to itself that can never reject.\n//\n// WHAT THIS FILE PINS. Both halves go red if the identity gate is\n// reverted to its pre-fix `proposer: NewAutoAcceptAuthority()` default:\n//\n//   - a principal that is NOT the contract is refused;\n//   - the contract itself is accepted, so the gate does not lock out the\n//     legitimate driver.\n//\n// It deliberately does NOT demonstrate `DoByCurrent` on an own-path\n// authority. That pairing is the anti-pattern NewContractAuthority's\n// godoc forbids: it succeeds here and it would equally succeed for a\n// hostile caller, so asserting it green would document the hole as the\n// recommended shape. A filetest is confined to one realm and so cannot\n// exhibit the foreign caller itself; the package-level negatives\n// (TestForeignFrameCannotDriveContractAuthority,\n// TestContractPathIsolation) and r/gnops/valopers' filetests cover that.\npackage example\n\nimport (\n\t\"chain\"\n\n\t\"gno.land/p/moul/authz/v0\"\n)\n\nvar auth = authz.NewWithAuthority(\n\tauthz.NewContractAuthority(\n\t\t\"gno.land/r/example\", // this realm's own path\n\t\tfunc(_ string, action authz.PrivilegedAction) error { return action() },\n\t),\n)\n\nvar updated string\n\n// UpdateParams is the exported crossing entrypoint of the realm.\n//\n// DoByPrevious, not DoByCurrent: the principal is whoever crossed in, so\n// an unrelated realm calling this presents its own address and is\n// refused. Under DoByCurrent the principal would be this realm's own\n// address for every caller, and this entrypoint would be an\n// unauthenticated privileged write.\n//\n// Note what must NOT be added next to it: a function returning this\n// closure, or any other value carrying this frame. That is the leak.\nfunc UpdateParams(cur realm, v string) error {\n\treturn auth.DoByPrevious(0, cur, \"update_params\", func() error {\n\t\tupdated = v\n\t\treturn nil\n\t})\n}\n\nfunc main(cur realm) {\n\t// A principal that is not the contract is refused, and the action\n\t// does not run. This is the assertion the pre-fix AutoAccept default\n\t// could not make.\n\toutsider := chain.PackageAddress(\"gno.land/r/outsider\")\n\terr := auth.Authority().Authorize(outsider, \"update_params\", func() error {\n\t\tupdated = \"SHOULD-NOT-LAND\"\n\t\treturn nil\n\t})\n\tprintln(\"outsider err:\", err)\n\tprintln(\"updated:\", updated)\n\n\t// The contract itself is accepted: main and UpdateParams are both in\n\t// gno.land/r/example, so cur.Previous() is this realm.\n\tprintln(\"contract err:\", UpdateParams(cross(cur), \"governed-value\"))\n\tprintln(\"updated:\", updated)\n}\n\n// Output:\n// outsider err: unauthorized\n// updated:\n// contract err: undefined\n// updated: governed-value\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"oMDO7469zr06Ix+KTa5pKK3ujS2cxbi+2zkjAULb1Ndi3tYTVQstwHvHPOryARVfi4jY5ZX1QDh8SZFtWOHogg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","package":{"name":"dynreplacer","path":"gno.land/p/moul/dynreplacer/v0","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":"dynreplacer_test.gno","body":"package dynreplacer\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nfunc TestNew(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\tpairs []Pair\n\t}{\n\t\t{\n\t\t\tname:  \"empty constructor\",\n\t\t\tpairs: []Pair{},\n\t\t},\n\t\t{\n\t\t\tname: \"single pair\",\n\t\t\tpairs: []Pair{\n\t\t\t\t{\":name:\", func() string { return \"World\" }},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple pairs\",\n\t\t\tpairs: []Pair{\n\t\t\t\t{\":greeting:\", func() string { return \"Hello\" }},\n\t\t\t\t{\":name:\", func() string { return \"World\" }},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tr := New(tt.pairs...)\n\t\t\tuassert.True(t, r.callbacks != nil, \"callbacks map should be initialized\")\n\t\t\tuassert.True(t, r.cachedValues != nil, \"cachedValues map should be initialized\")\n\n\t\t\t// Verify all callbacks were registered\n\t\t\tfor _, pair := range tt.pairs {\n\t\t\t\t_, exists := r.callbacks[pair.Placeholder]\n\t\t\t\tuassert.True(t, exists, \"callback should be registered for \"+pair.Placeholder)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestReplace(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tlayout   string\n\t\tsetup    func(*Replacer)\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"empty layout\",\n\t\t\tlayout:   \"\",\n\t\t\tsetup:    func(r *Replacer) {},\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:   \"single replacement\",\n\t\t\tlayout: \"Hello :name:!\",\n\t\t\tsetup: func(r *Replacer) {\n\t\t\t\tr.RegisterCallback(\":name:\", func() string { return \"World\" })\n\t\t\t},\n\t\t\texpected: \"Hello World!\",\n\t\t},\n\t\t{\n\t\t\tname:   \"multiple replacements\",\n\t\t\tlayout: \":greeting: :name:!\",\n\t\t\tsetup: func(r *Replacer) {\n\t\t\t\tr.RegisterCallback(\":greeting:\", func() string { return \"Hello\" })\n\t\t\t\tr.RegisterCallback(\":name:\", func() string { return \"World\" })\n\t\t\t},\n\t\t\texpected: \"Hello World!\",\n\t\t},\n\t\t{\n\t\t\tname:   \"no recursive replacement\",\n\t\t\tlayout: \":outer:\",\n\t\t\tsetup: func(r *Replacer) {\n\t\t\t\tr.RegisterCallback(\":outer:\", func() string { return \":inner:\" })\n\t\t\t\tr.RegisterCallback(\":inner:\", func() string { return \"content\" })\n\t\t\t},\n\t\t\texpected: \":inner:\",\n\t\t},\n\t\t{\n\t\t\tname:   \"unused callbacks\",\n\t\t\tlayout: \"Hello :name:!\",\n\t\t\tsetup: func(r *Replacer) {\n\t\t\t\tr.RegisterCallback(\":name:\", func() string { return \"World\" })\n\t\t\t\tr.RegisterCallback(\":unused:\", func() string { return \"Never Called\" })\n\t\t\t},\n\t\t\texpected: \"Hello World!\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tr := New()\n\t\t\ttt.setup(r)\n\t\t\tresult := r.Replace(tt.layout)\n\t\t\tuassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestCaching(t *testing.T) {\n\tr := New()\n\tcallCount := 0\n\tr.RegisterCallback(\":expensive:\", func() string {\n\t\tcallCount++\n\t\treturn \"computed\"\n\t})\n\n\tlayout := \"Value: :expensive:\"\n\n\t// First call should compute\n\tresult1 := r.Replace(layout)\n\tuassert.Equal(t, \"Value: computed\", result1)\n\tuassert.Equal(t, 1, callCount)\n\n\t// Second call should use cache\n\tresult2 := r.Replace(layout)\n\tuassert.Equal(t, \"Value: computed\", result2)\n\tuassert.Equal(t, 1, callCount)\n\n\t// After clearing cache, should recompute\n\tr.ClearCache()\n\tresult3 := r.Replace(layout)\n\tuassert.Equal(t, \"Value: computed\", result3)\n\tuassert.Equal(t, 2, callCount)\n}\n\nfunc TestComplexExample(t *testing.T) {\n\tlayout := `\n\t\t# Welcome to gno.land\n\n\t\t## Blog\n\t\t:latest-blogposts:\n\n\t\t## Events\n\t\t:next-events:\n\n\t\t## Awesome Gno\n\t\t:awesome-gno:\n\t`\n\n\tr := New(\n\t\tPair{\":latest-blogposts:\", func() string { return \"Latest blog posts content here\" }},\n\t\tPair{\":next-events:\", func() string { return \"Upcoming events listed here\" }},\n\t\tPair{\":awesome-gno:\", func() string { return \":latest-blogposts: (This should NOT be replaced again)\" }},\n\t)\n\n\tresult := r.Replace(layout)\n\n\t// Check that original placeholders are replaced\n\tuassert.True(t, !strings.Contains(result, \":latest-blogposts:\\n\"), \"':latest-blogposts:' placeholder should be replaced\")\n\tuassert.True(t, !strings.Contains(result, \":next-events:\\n\"), \"':next-events:' placeholder should be replaced\")\n\tuassert.True(t, !strings.Contains(result, \":awesome-gno:\\n\"), \"':awesome-gno:' placeholder should be replaced\")\n\n\t// Check that the replacement content is present\n\tuassert.True(t, strings.Contains(result, \"Latest blog posts content here\"), \"Blog posts content should be present\")\n\tuassert.True(t, strings.Contains(result, \"Upcoming events listed here\"), \"Events content should be present\")\n\tuassert.True(t, strings.Contains(result, \":latest-blogposts: (This should NOT be replaced again)\"),\n\t\t\"Nested placeholder should not be replaced\")\n}\n\nfunc TestEdgeCases(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tlayout   string\n\t\tsetup    func(*Replacer)\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:   \"empty string placeholder\",\n\t\t\tlayout: \"Hello :\",\n\t\t\tsetup: func(r *Replacer) {\n\t\t\t\tr.RegisterCallback(\"\", func() string { return \"World\" })\n\t\t\t},\n\t\t\texpected: \"WorldHWorldeWorldlWorldlWorldoWorld World:World\",\n\t\t},\n\t\t{\n\t\t\tname:   \"overlapping placeholders\",\n\t\t\tlayout: \"Hello :name::greeting:\",\n\t\t\tsetup: func(r *Replacer) {\n\t\t\t\tr.RegisterCallback(\":name:\", func() string { return \"World\" })\n\t\t\t\tr.RegisterCallback(\":greeting:\", func() string { return \"Hi\" })\n\t\t\t\tr.RegisterCallback(\":name::greeting:\", func() string { return \"Should not match\" })\n\t\t\t},\n\t\t\texpected: \"Hello WorldHi\",\n\t\t},\n\t\t{\n\t\t\tname:   \"replacement order\",\n\t\t\tlayout: \":a::b::c:\",\n\t\t\tsetup: func(r *Replacer) {\n\t\t\t\tr.RegisterCallback(\":c:\", func() string { return \"3\" })\n\t\t\t\tr.RegisterCallback(\":b:\", func() string { return \"2\" })\n\t\t\t\tr.RegisterCallback(\":a:\", func() string { return \"1\" })\n\t\t\t},\n\t\t\texpected: \"123\",\n\t\t},\n\t\t{\n\t\t\tname:   \"special characters in placeholders\",\n\t\t\tlayout: \"Hello :$name#123:!\",\n\t\t\tsetup: func(r *Replacer) {\n\t\t\t\tr.RegisterCallback(\":$name#123:\", func() string { return \"World\" })\n\t\t\t},\n\t\t\texpected: \"Hello World!\",\n\t\t},\n\t\t{\n\t\t\tname:   \"multiple occurrences of same placeholder\",\n\t\t\tlayout: \":name: and :name: again\",\n\t\t\tsetup: func(r *Replacer) {\n\t\t\t\tcallCount := 0\n\t\t\t\tr.RegisterCallback(\":name:\", func() string {\n\t\t\t\t\tcallCount++\n\t\t\t\t\treturn \"World\"\n\t\t\t\t})\n\t\t\t},\n\t\t\texpected: \"World and World again\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tr := New()\n\t\t\ttt.setup(r)\n\t\t\tresult := r.Replace(tt.layout)\n\t\t\tuassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/dynreplacer/v0\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"uPXLfXutbjRtulLX3KDml7quOnDJlBuvRP1IwaZCnLstzZMr7z2BeFiFwSu8eNG/5iRZh13L3ZY7NdJUQndJvA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","package":{"name":"fifo","path":"gno.land/p/moul/fifo/v0","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":"fifo_test.gno","body":"package fifo\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nfunc TestNew(t *testing.T) {\n\tl := New(5)\n\tuassert.Equal(t, 5, l.MaxSize())\n\tuassert.Equal(t, 0, l.Size())\n}\n\nfunc TestAppend(t *testing.T) {\n\tl := New(3)\n\n\t// Test adding within capacity\n\tl.Append(1)\n\tl.Append(2)\n\tuassert.Equal(t, 2, l.Size())\n\tuassert.Equal(t, 1, l.Get(0))\n\tuassert.Equal(t, 2, l.Get(1))\n\n\t// Test overflow behavior\n\tl.Append(3)\n\tl.Append(4)\n\tuassert.Equal(t, 3, l.Size())\n\tuassert.Equal(t, 2, l.Get(0))\n\tuassert.Equal(t, 3, l.Get(1))\n\tuassert.Equal(t, 4, l.Get(2))\n}\n\nfunc TestPrepend(t *testing.T) {\n\tl := New(3)\n\n\t// Test adding within capacity\n\tl.Prepend(1)\n\tl.Prepend(2)\n\tuassert.Equal(t, 2, l.Size())\n\tuassert.Equal(t, 2, l.Get(0))\n\tuassert.Equal(t, 1, l.Get(1))\n\n\t// Test overflow behavior\n\tl.Prepend(3)\n\tl.Prepend(4)\n\tuassert.Equal(t, 3, l.Size())\n\tuassert.Equal(t, 4, l.Get(0))\n\tuassert.Equal(t, 3, l.Get(1))\n\tuassert.Equal(t, 2, l.Get(2))\n}\n\nfunc TestGet(t *testing.T) {\n\tl := New(3)\n\tl.Append(1)\n\tl.Append(2)\n\tl.Append(3)\n\n\t// Test valid indices\n\tuassert.Equal(t, 1, l.Get(0))\n\tuassert.Equal(t, 2, l.Get(1))\n\tuassert.Equal(t, 3, l.Get(2))\n\n\t// Test invalid indices\n\tuassert.True(t, l.Get(-1) == nil)\n\tuassert.True(t, l.Get(3) == nil)\n}\n\nfunc TestEntries(t *testing.T) {\n\tl := New(3)\n\tl.Append(1)\n\tl.Append(2)\n\tl.Append(3)\n\n\tentries := l.Entries()\n\tuassert.Equal(t, 3, len(entries))\n\tuassert.Equal(t, 1, entries[0])\n\tuassert.Equal(t, 2, entries[1])\n\tuassert.Equal(t, 3, entries[2])\n}\n\nfunc TestLatest(t *testing.T) {\n\tl := New(5)\n\n\t// Test empty list\n\tuassert.True(t, l.Latest() == nil)\n\n\t// Test single entry\n\tl.Append(1)\n\tuassert.Equal(t, 1, l.Latest())\n\n\t// Test multiple entries\n\tl.Append(2)\n\tl.Append(3)\n\tuassert.Equal(t, 3, l.Latest())\n\n\t// Test after overflow\n\tl.Append(4)\n\tl.Append(5)\n\tl.Append(6)\n\tuassert.Equal(t, 6, l.Latest())\n}\n\nfunc TestIterator(t *testing.T) {\n\tl := New(3)\n\tl.Append(1)\n\tl.Append(2)\n\tl.Append(3)\n\n\titer := l.Iterator()\n\tuassert.Equal(t, 1, iter())\n\tuassert.Equal(t, 2, iter())\n\tuassert.Equal(t, 3, iter())\n\tuassert.True(t, iter() == nil)\n}\n\nfunc TestMixedOperations(t *testing.T) {\n\tl := New(3)\n\n\t// Mix of append and prepend operations\n\tl.Append(1)  // [1]\n\tl.Prepend(2) // [2,1]\n\tl.Append(3)  // [2,1,3]\n\tl.Prepend(4) // [4,2,1]\n\n\tentries := l.Entries()\n\tuassert.Equal(t, 3, len(entries))\n\tuassert.Equal(t, 4, entries[0])\n\tuassert.Equal(t, 2, entries[1])\n\tuassert.Equal(t, 1, entries[2])\n}\n\nfunc TestEmptyList(t *testing.T) {\n\tl := New(3)\n\n\t// Test operations on empty list\n\tuassert.Equal(t, 0, l.Size())\n\tuassert.True(t, l.Get(0) == nil)\n\tuassert.Equal(t, 0, len(l.Entries()))\n\tuassert.True(t, l.Latest() == nil)\n\n\titer := l.Iterator()\n\tuassert.True(t, iter() == nil)\n}\n\nfunc TestEdgeCases(t *testing.T) {\n\t// Test zero-size list\n\tl := New(0)\n\tuassert.Equal(t, 0, l.MaxSize())\n\tl.Append(1) // Should be no-op\n\tuassert.Equal(t, 0, l.Size())\n\n\t// Test single-element list\n\tl = New(1)\n\tl.Append(1)\n\tl.Append(2) // Should replace 1\n\tuassert.Equal(t, 1, l.Size())\n\tuassert.Equal(t, 2, l.Latest())\n\n\t// Test rapid append/prepend alternation\n\tl = New(3)\n\tl.Append(1)  // [1]\n\tl.Prepend(2) // [2,1]\n\tl.Append(3)  // [2,1,3]\n\tl.Prepend(4) // [4,2,1]\n\tl.Append(5)  // [2,1,5]\n\tuassert.Equal(t, 3, l.Size())\n\tentries := l.Entries()\n\tuassert.Equal(t, 2, entries[0])\n\tuassert.Equal(t, 1, entries[1])\n\tuassert.Equal(t, 5, entries[2])\n\n\t// Test nil values\n\tl = New(2)\n\tl.Append(nil)\n\tl.Prepend(nil)\n\tuassert.Equal(t, 2, l.Size())\n\tuassert.True(t, l.Get(0) == nil)\n\tuassert.True(t, l.Get(1) == nil)\n\n\t// Test index bounds\n\tl = New(3)\n\tl.Append(1)\n\tuassert.True(t, l.Get(-1) == nil)\n\tuassert.True(t, l.Get(1) == nil)\n\n\t// Test iterator exhaustion\n\tl = New(2)\n\tl.Append(1)\n\tl.Append(2)\n\titer := l.Iterator()\n\tuassert.Equal(t, 1, iter())\n\tuassert.Equal(t, 2, iter())\n\tuassert.True(t, iter() == nil)\n\tuassert.True(t, iter() == nil)\n\n\t// Test prepend on full list\n\tl = New(2)\n\tl.Append(1)\n\tl.Append(2)  // [1,2]\n\tl.Prepend(3) // [3,1]\n\tuassert.Equal(t, 2, l.Size())\n\tentries = l.Entries()\n\tuassert.Equal(t, 3, entries[0])\n\tuassert.Equal(t, 1, entries[1])\n}\n\nfunc TestSetMaxSize(t *testing.T) {\n\tl := New(5)\n\n\t// Fill the list\n\tl.Append(1)\n\tl.Append(2)\n\tl.Append(3)\n\tl.Append(4)\n\tl.Append(5)\n\n\t// Test increasing maxSize\n\tl.SetMaxSize(7)\n\tuassert.Equal(t, 7, l.MaxSize())\n\tuassert.Equal(t, 5, l.Size())\n\n\t// Test reducing maxSize\n\tl.SetMaxSize(3)\n\tuassert.Equal(t, 3, l.Size())\n\tentries := l.Entries()\n\tuassert.Equal(t, 3, entries[0])\n\tuassert.Equal(t, 4, entries[1])\n\tuassert.Equal(t, 5, entries[2])\n\n\t// Test setting to zero\n\tl.SetMaxSize(0)\n\tuassert.Equal(t, 0, l.Size())\n\tuassert.True(t, l.head == nil)\n\tuassert.True(t, l.tail == nil)\n\n\t// Test negative maxSize\n\tl.SetMaxSize(-1)\n\tuassert.Equal(t, 0, l.MaxSize())\n\n\t// Test setting back to positive\n\tl.SetMaxSize(2)\n\tl.Append(1)\n\tl.Append(2)\n\tl.Append(3)\n\tuassert.Equal(t, 2, l.Size())\n\tentries = l.Entries()\n\tuassert.Equal(t, 2, entries[0])\n\tuassert.Equal(t, 3, entries[1])\n}\n\nfunc TestDelete(t *testing.T) {\n\tl := New(5)\n\n\t// Test delete on empty list\n\tuassert.False(t, l.Delete(0))\n\tuassert.False(t, l.Delete(-1))\n\n\t// Fill list\n\tl.Append(1)\n\tl.Append(2)\n\tl.Append(3)\n\tl.Append(4)\n\n\t// Test invalid indices\n\tuassert.False(t, l.Delete(-1))\n\tuassert.False(t, l.Delete(4))\n\n\t// Test deleting from middle\n\tuassert.True(t, l.Delete(1))\n\tuassert.Equal(t, 3, l.Size())\n\tentries := l.Entries()\n\tuassert.Equal(t, 1, entries[0])\n\tuassert.Equal(t, 3, entries[1])\n\tuassert.Equal(t, 4, entries[2])\n\n\t// Test deleting from head\n\tuassert.True(t, l.Delete(0))\n\tuassert.Equal(t, 2, l.Size())\n\tentries = l.Entries()\n\tuassert.Equal(t, 3, entries[0])\n\tuassert.Equal(t, 4, entries[1])\n\n\t// Test deleting from tail\n\tuassert.True(t, l.Delete(1))\n\tuassert.Equal(t, 1, l.Size())\n\tuassert.Equal(t, 3, l.Latest())\n\n\t// Test deleting last element\n\tuassert.True(t, l.Delete(0))\n\tuassert.Equal(t, 0, l.Size())\n\tuassert.True(t, l.head == nil)\n\tuassert.True(t, l.tail == nil)\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/fifo/v0\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"cjO2G2eZtKD8sK0cswehNjicUjgwgwLd70F9xKPhno9ljVx9swtJQEH5J6UelsmupKSBd8sJtlOAe2OK5qQ6cw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","package":{"name":"txlink","path":"gno.land/p/moul/txlink/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/txlink/v0\"\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"},{"name":"txlink_test.gno","body":"package txlink\n\nimport (\n\t\"chain/runtime\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestCall(t *testing.T) {\n\tcd := runtime.ChainDomain()\n\n\ttests := []struct {\n\t\tfn        string\n\t\targs      []string\n\t\twant      string\n\t\trealm_XXX Realm\n\t}{\n\t\t{\"foo\", []string{\"bar\", \"1\", \"baz\", \"2\"}, \"/p/moul/txlink/v0$help\u0026func=foo\u0026bar=1\u0026baz=2\", \"\"},\n\t\t{\"testFunc\", []string{\"key\", \"value\"}, \"/p/moul/txlink/v0$help\u0026func=testFunc\u0026key=value\", \"\"},\n\t\t{\"noArgsFunc\", []string{}, \"/p/moul/txlink/v0$help\u0026func=noArgsFunc\", \"\"},\n\t\t{\"oddArgsFunc\", []string{\"key\"}, \"/p/moul/txlink/v0$help\u0026func=oddArgsFunc\u0026error=odd+number+of+arguments\", \"\"},\n\t\t{\"foo\", []string{\"bar\", \"1\", \"baz\", \"2\"}, \"/r/lorem/ipsum$help\u0026func=foo\u0026bar=1\u0026baz=2\", Realm(cd + \"/r/lorem/ipsum\")},\n\t\t{\"testFunc\", []string{\"key\", \"value\"}, \"/r/lorem/ipsum$help\u0026func=testFunc\u0026key=value\", Realm(cd + \"/r/lorem/ipsum\")},\n\t\t{\"noArgsFunc\", []string{}, \"/r/lorem/ipsum$help\u0026func=noArgsFunc\", Realm(cd + \"/r/lorem/ipsum\")},\n\t\t{\"oddArgsFunc\", []string{\"key\"}, \"/r/lorem/ipsum$help\u0026func=oddArgsFunc\u0026error=odd+number+of+arguments\", Realm(cd + \"/r/lorem/ipsum\")},\n\t\t{\"foo\", []string{\"bar\", \"1\", \"baz\", \"2\"}, \"https://gno.world/r/lorem/ipsum$help\u0026func=foo\u0026bar=1\u0026baz=2\", \"gno.world/r/lorem/ipsum\"},\n\t\t{\"testFunc\", []string{\"key\", \"value\"}, \"https://gno.world/r/lorem/ipsum$help\u0026func=testFunc\u0026key=value\", \"gno.world/r/lorem/ipsum\"},\n\t\t{\"noArgsFunc\", []string{}, \"https://gno.world/r/lorem/ipsum$help\u0026func=noArgsFunc\", \"gno.world/r/lorem/ipsum\"},\n\t\t{\"oddArgsFunc\", []string{\"key\"}, \"https://gno.world/r/lorem/ipsum$help\u0026func=oddArgsFunc\u0026error=odd+number+of+arguments\", \"gno.world/r/lorem/ipsum\"},\n\t\t{\"test\", []string{\"key\", \"hello world\"}, \"/p/moul/txlink/v0$help\u0026func=test\u0026key=hello+world\", \"\"},\n\t\t{\"test\", []string{\"key\", \"a\u0026b=c\"}, \"/p/moul/txlink/v0$help\u0026func=test\u0026key=a%26b%3Dc\", \"\"},\n\t\t{\"test\", []string{\"key\", \"\"}, \"/p/moul/txlink/v0$help\u0026func=test\u0026key=\", \"\"},\n\t\t{\"testSend\", []string{\"key\", \"hello world\", \".send\", \"1000000ugnot\"}, \"/p/moul/txlink/v0$help\u0026func=testSend\u0026.send=1000000ugnot\u0026key=hello+world\", \"\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\ttitle := string(tt.realm_XXX) + \"_\" + tt.fn\n\t\tt.Run(title, func(t *testing.T) {\n\t\t\tif tt.fn == \"oddArgsFunc\" {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t\tif r != \"odd number of arguments\" {\n\t\t\t\t\t\t\tt.Errorf(\"expected panic with message 'odd number of arguments', got: %v\", r)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tt.Error(\"expected panic for odd number of arguments, but did not panic\")\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t}\n\t\t\tgot := tt.realm_XXX.Call(tt.fn, tt.args...)\n\t\t\turequire.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestBuilder(t *testing.T) {\n\tcases := []struct {\n\t\tname     string\n\t\tbuild    func() string\n\t\texpected string\n\t}{\n\t\t// Basic functionality tests\n\t\t{\n\t\t\tname: \"empty_function\",\n\t\t\tbuild: func() string {\n\t\t\t\treturn NewLink(\"\").URL()\n\t\t\t},\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"function_without_args\",\n\t\t\tbuild: func() string {\n\t\t\t\treturn NewLink(\"MyFunc\").URL()\n\t\t\t},\n\t\t\texpected: \"/p/moul/txlink/v0$help\u0026func=MyFunc\",\n\t\t},\n\n\t\t// Realm tests\n\t\t{\n\t\t\tname: \"gnoland_realm\",\n\t\t\tbuild: func() string {\n\t\t\t\treturn Realm(\"gno.land/r/demo\").\n\t\t\t\t\tNewLink(\"MyFunc\").\n\t\t\t\t\tAddArgs(\"key\", \"value\").\n\t\t\t\t\tURL()\n\t\t\t},\n\t\t\texpected: \"/r/demo$help\u0026func=MyFunc\u0026key=value\",\n\t\t},\n\t\t{\n\t\t\tname: \"external_realm\",\n\t\t\tbuild: func() string {\n\t\t\t\treturn Realm(\"gno.world/r/demo\").\n\t\t\t\t\tNewLink(\"MyFunc\").\n\t\t\t\t\tAddArgs(\"key\", \"value\").\n\t\t\t\t\tURL()\n\t\t\t},\n\t\t\texpected: \"https://gno.world/r/demo$help\u0026func=MyFunc\u0026key=value\",\n\t\t},\n\t\t{\n\t\t\tname: \"empty_realm\",\n\t\t\tbuild: func() string {\n\t\t\t\treturn Realm(\"\").\n\t\t\t\t\tNewLink(\"func\").\n\t\t\t\t\tAddArgs(\"key\", \"value\").\n\t\t\t\t\tURL()\n\t\t\t},\n\t\t\texpected: \"/p/moul/txlink/v0$help\u0026func=func\u0026key=value\",\n\t\t},\n\n\t\t// URL encoding tests\n\t\t{\n\t\t\tname: \"url_encoding_with_spaces\",\n\t\t\tbuild: func() string {\n\t\t\t\treturn NewLink(\"test\").\n\t\t\t\t\tAddArgs(\"key\", \"hello world\").\n\t\t\t\t\tURL()\n\t\t\t},\n\t\t\texpected: \"/p/moul/txlink/v0$help\u0026func=test\u0026key=hello+world\",\n\t\t},\n\t\t{\n\t\t\tname: \"url_encoding_with_special_chars\",\n\t\t\tbuild: func() string {\n\t\t\t\treturn NewLink(\"test\").\n\t\t\t\t\tAddArgs(\"key\", \"a\u0026b=c\").\n\t\t\t\t\tURL()\n\t\t\t},\n\t\t\texpected: \"/p/moul/txlink/v0$help\u0026func=test\u0026key=a%26b%3Dc\",\n\t\t},\n\t\t{\n\t\t\tname: \"url_encoding_with_unicode\",\n\t\t\tbuild: func() string {\n\t\t\t\treturn NewLink(\"func\").\n\t\t\t\t\tAddArgs(\"key\", \"🌟\").\n\t\t\t\t\tURL()\n\t\t\t},\n\t\t\texpected: \"/p/moul/txlink/v0$help\u0026func=func\u0026key=%F0%9F%8C%9F\",\n\t\t},\n\t\t{\n\t\t\tname: \"url_encoding_with_special_chars_in_key\",\n\t\t\tbuild: func() string {\n\t\t\t\treturn NewLink(\"func\").\n\t\t\t\t\tAddArgs(\"my/key\", \"value\").\n\t\t\t\t\tURL()\n\t\t\t},\n\t\t\texpected: \"/p/moul/txlink/v0$help\u0026func=func\u0026my%2Fkey=value\",\n\t\t},\n\n\t\t// AddArgs tests\n\t\t{\n\t\t\tname: \"addargs_with_multiple_pairs\",\n\t\t\tbuild: func() string {\n\t\t\t\treturn NewLink(\"MyFunc\").\n\t\t\t\t\tAddArgs(\"key1\", \"value1\", \"key2\", \"value2\").\n\t\t\t\t\tURL()\n\t\t\t},\n\t\t\texpected: \"/p/moul/txlink/v0$help\u0026func=MyFunc\u0026key1=value1\u0026key2=value2\",\n\t\t},\n\t\t{\n\t\t\tname: \"addargs_with_odd_number_of_args\",\n\t\t\tbuild: func() string {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t\tif r != \"odd number of arguments\" {\n\t\t\t\t\t\t\tt.Errorf(\"expected panic with message 'odd number of arguments', got: %v\", r)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tt.Error(\"expected panic for odd number of arguments, but did not panic\")\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t\treturn NewLink(\"MyFunc\").\n\t\t\t\t\tAddArgs(\"key1\", \"value1\", \"orphan\").\n\t\t\t\t\tURL()\n\t\t\t},\n\t\t\texpected: \"\",\n\t\t},\n\n\t\t// Empty values tests\n\t\t{\n\t\t\tname: \"empty_key_should_be_ignored\",\n\t\t\tbuild: func() string {\n\t\t\t\treturn NewLink(\"func\").\n\t\t\t\t\tAddArgs(\"\", \"value\").\n\t\t\t\t\tURL()\n\t\t\t},\n\t\t\texpected: \"/p/moul/txlink/v0$help\u0026func=func\",\n\t\t},\n\t\t{\n\t\t\tname: \"empty_value_should_be_kept\",\n\t\t\tbuild: func() string {\n\t\t\t\treturn NewLink(\"func\").\n\t\t\t\t\tAddArgs(\"key\", \"\").\n\t\t\t\t\tURL()\n\t\t\t},\n\t\t\texpected: \"/p/moul/txlink/v0$help\u0026func=func\u0026key=\",\n\t\t},\n\n\t\t// Send tests\n\t\t{\n\t\t\tname: \"send_via_addsend_method\",\n\t\t\tbuild: func() string {\n\t\t\t\treturn NewLink(\"MyFunc\").\n\t\t\t\t\tAddArgs(\"key\", \"value\").\n\t\t\t\t\tSetSend(\"1000000ugnot\").\n\t\t\t\t\tURL()\n\t\t\t},\n\t\t\texpected: \"/p/moul/txlink/v0$help\u0026func=MyFunc\u0026.send=1000000ugnot\u0026key=value\",\n\t\t},\n\t\t{\n\t\t\tname: \"send_via_addarg_method_panic\",\n\t\t\tbuild: func() string {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t\tif r != \"invalid key\" {\n\t\t\t\t\t\t\tt.Errorf(\"expected panic with message 'invalid key', got: %v\", r)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tt.Errorf(\"expected panic for .send key, but did not panic\")\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t\tNewLink(\"MyFunc\").AddArgs(\".send\", \"1000000ugnot\")\n\t\t\t\treturn \"no panic occurred\"\n\t\t\t},\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"addsend_should_override_previous_addsend\",\n\t\t\tbuild: func() string {\n\t\t\t\treturn NewLink(\"MyFunc\").\n\t\t\t\t\tSetSend(\"1000000ugnot\").\n\t\t\t\t\tSetSend(\"2000000ugnot\").\n\t\t\t\t\tURL()\n\t\t\t},\n\t\t\texpected: \"/p/moul/txlink/v0$help\u0026func=MyFunc\u0026.send=2000000ugnot\",\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot := tc.build()\n\t\t\turequire.Equal(t, tc.expected, got)\n\t\t})\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"4SpClOz5Xalc9iO/eiWTb4cHUxoFFDpeYGGHn7qj4URretqIN41Ege6yRnxHBgJFD7OzBq470LVcO6snNXD4KA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","package":{"name":"helplink","path":"gno.land/p/moul/helplink/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/helplink/v0\"\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/v0\"\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"},{"name":"helplink_test.gno","body":"package helplink\n\nimport (\n\t\"chain/runtime\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestFunc(t *testing.T) {\n\tcd := runtime.ChainDomain()\n\ttests := []struct {\n\t\ttitle     string\n\t\tfn        string\n\t\targs      []string\n\t\twant      string\n\t\trealm_XXX Realm\n\t}{\n\t\t{\"Example\", \"foo\", []string{\"bar\", \"1\", \"baz\", \"2\"}, \"[Example](/p/moul/helplink/v0$help\u0026func=foo\u0026bar=1\u0026baz=2)\", \"\"},\n\t\t{\"Realm Example\", \"foo\", []string{\"bar\", \"1\", \"baz\", \"2\"}, \"[Realm Example](/r/lorem/ipsum$help\u0026func=foo\u0026bar=1\u0026baz=2)\", Realm(cd + \"/r/lorem/ipsum\")},\n\t\t{\"Single Arg\", \"testFunc\", []string{\"key\", \"value\"}, \"[Single Arg](/p/moul/helplink/v0$help\u0026func=testFunc\u0026key=value)\", \"\"},\n\t\t{\"No Args\", \"noArgsFunc\", []string{}, \"[No Args](/p/moul/helplink/v0$help\u0026func=noArgsFunc)\", \"\"},\n\t\t{\"Odd Args\", \"oddArgsFunc\", []string{\"key\"}, \"[Odd Args](/p/moul/helplink/v0$help\u0026func=oddArgsFunc\u0026error=odd+number+of+arguments)\", \"\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.title, func(t *testing.T) {\n\t\t\tif tt.fn == \"oddArgsFunc\" {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t\tif r != \"odd number of arguments\" {\n\t\t\t\t\t\t\tt.Errorf(\"expected panic with message 'odd number of arguments', got: %v\", r)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tt.Error(\"expected panic for odd number of arguments, but did not panic\")\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t}\n\t\t\tgot := tt.realm_XXX.Func(tt.title, tt.fn, tt.args...)\n\t\t\turequire.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestFuncURL(t *testing.T) {\n\tcd := runtime.ChainDomain()\n\ttests := []struct {\n\t\tfn        string\n\t\targs      []string\n\t\twant      string\n\t\trealm_XXX Realm\n\t}{\n\t\t{\"foo\", []string{\"bar\", \"1\", \"baz\", \"2\"}, \"/p/moul/helplink/v0$help\u0026func=foo\u0026bar=1\u0026baz=2\", \"\"},\n\t\t{\"testFunc\", []string{\"key\", \"value\"}, \"/p/moul/helplink/v0$help\u0026func=testFunc\u0026key=value\", \"\"},\n\t\t{\"noArgsFunc\", []string{}, \"/p/moul/helplink/v0$help\u0026func=noArgsFunc\", \"\"},\n\t\t{\"oddArgsFunc\", []string{\"key\"}, \"/p/moul/helplink/v0$help\u0026func=oddArgsFunc\u0026error=odd+number+of+arguments\", \"\"},\n\t\t{\"foo\", []string{\"bar\", \"1\", \"baz\", \"2\"}, \"/r/lorem/ipsum$help\u0026func=foo\u0026bar=1\u0026baz=2\", Realm(cd + \"/r/lorem/ipsum\")},\n\t\t{\"testFunc\", []string{\"key\", \"value\"}, \"/r/lorem/ipsum$help\u0026func=testFunc\u0026key=value\", Realm(cd + \"/r/lorem/ipsum\")},\n\t\t{\"noArgsFunc\", []string{}, \"/r/lorem/ipsum$help\u0026func=noArgsFunc\", Realm(cd + \"/r/lorem/ipsum\")},\n\t\t{\"oddArgsFunc\", []string{\"key\"}, \"/r/lorem/ipsum$help\u0026func=oddArgsFunc\u0026error=odd+number+of+arguments\", Realm(cd + \"/r/lorem/ipsum\")},\n\t\t{\"foo\", []string{\"bar\", \"1\", \"baz\", \"2\"}, \"https://gno.world/r/lorem/ipsum$help\u0026func=foo\u0026bar=1\u0026baz=2\", \"gno.world/r/lorem/ipsum\"},\n\t\t{\"testFunc\", []string{\"key\", \"value\"}, \"https://gno.world/r/lorem/ipsum$help\u0026func=testFunc\u0026key=value\", \"gno.world/r/lorem/ipsum\"},\n\t\t{\"noArgsFunc\", []string{}, \"https://gno.world/r/lorem/ipsum$help\u0026func=noArgsFunc\", \"gno.world/r/lorem/ipsum\"},\n\t\t{\"oddArgsFunc\", []string{\"key\"}, \"https://gno.world/r/lorem/ipsum$help\u0026func=oddArgsFunc\u0026error=odd+number+of+arguments\", \"gno.world/r/lorem/ipsum\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\ttitle := tt.fn\n\t\tt.Run(title, func(t *testing.T) {\n\t\t\tif tt.fn == \"oddArgsFunc\" {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t\tif r != \"odd number of arguments\" {\n\t\t\t\t\t\t\tt.Errorf(\"expected panic with message 'odd number of arguments', got: %v\", r)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tt.Error(\"expected panic for odd number of arguments, but did not panic\")\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t}\n\t\t\tgot := tt.realm_XXX.FuncURL(tt.fn, tt.args...)\n\t\t\turequire.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestHome(t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/test/helplink\"))\n\tcd := runtime.ChainDomain()\n\ttests := []struct {\n\t\trealm_XXX Realm\n\t\twant      string\n\t}{\n\t\t{\"\", \"/r/test/helplink$help\"},\n\t\t{Realm(cd + \"/r/lorem/ipsum\"), \"/r/lorem/ipsum$help\"},\n\t\t{\"gno.world/r/lorem/ipsum\", \"https://gno.world/r/lorem/ipsum$help\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(string(tt.realm_XXX), func(t *testing.T) {\n\t\t\tgot := tt.realm_XXX.Home()\n\t\t\turequire.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"XlB9QOPWfvG9q5KTcrYgEM1gNv103OjT6bojLOmUCU0c1mTUvI0m1YJIiGWde/GtXC87yRqpIRLGX5n4BaxUNw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","package":{"name":"mdtable","path":"gno.land/p/moul/mdtable/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/mdtable/v0\"\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/v0\"\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"},{"name":"mdtable_test.gno","body":"package mdtable_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/moul/mdtable/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\n// XXX: switch to `func Example() {}` when supported.\nfunc TestExample(t *testing.T) {\n\ttable := mdtable.Table{\n\t\tHeaders: []string{\"ID\", \"Title\", \"Status\"},\n\t\tRows: [][]string{\n\t\t\t{\"#1\", \"Add a new validator\", \"succeed\"},\n\t\t\t{\"#2\", \"Change parameter\", \"timed out\"},\n\t\t\t{\"#3\", \"Fill pool\", \"active\"},\n\t\t},\n\t}\n\n\tgot := table.String()\n\texpected := `| ID | Title | Status |\n| --- | --- | --- |\n| #1 | Add a new validator | succeed |\n| #2 | Change parameter | timed out |\n| #3 | Fill pool | active |\n`\n\n\turequire.Equal(t, got, expected)\n}\n\nfunc TestTableString(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\ttable    mdtable.Table\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"With Headers and Rows\",\n\t\t\ttable: mdtable.Table{\n\t\t\t\tHeaders: []string{\"ID\", \"Title\", \"Status\", \"Date\"},\n\t\t\t\tRows: [][]string{\n\t\t\t\t\t{\"#1\", \"Add a new validator\", \"succeed\", \"2024-01-01\"},\n\t\t\t\t\t{\"#2\", \"Change parameter\", \"timed out\", \"2024-01-02\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: `| ID | Title | Status | Date |\n| --- | --- | --- | --- |\n| #1 | Add a new validator | succeed | 2024-01-01 |\n| #2 | Change parameter | timed out | 2024-01-02 |\n`,\n\t\t},\n\t\t{\n\t\t\tname: \"Without Headers\",\n\t\t\ttable: mdtable.Table{\n\t\t\t\tRows: [][]string{\n\t\t\t\t\t{\"#1\", \"Add a new validator\", \"succeed\", \"2024-01-01\"},\n\t\t\t\t\t{\"#2\", \"Change parameter\", \"timed out\", \"2024-01-02\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: `|  |  |  |  |\n| --- | --- | --- | --- |\n| #1 | Add a new validator | succeed | 2024-01-01 |\n| #2 | Change parameter | timed out | 2024-01-02 |\n`,\n\t\t},\n\t\t{\n\t\t\tname: \"Without Rows\",\n\t\t\ttable: mdtable.Table{\n\t\t\t\tHeaders: []string{\"ID\", \"Title\", \"Status\", \"Date\"},\n\t\t\t},\n\t\t\texpected: `| ID | Title | Status | Date |\n| --- | --- | --- | --- |\n`,\n\t\t},\n\t\t{\n\t\t\tname: \"With Pipe Character in Content\",\n\t\t\ttable: mdtable.Table{\n\t\t\t\tHeaders: []string{\"ID\", \"Title\", \"Status\", \"Date\"},\n\t\t\t\tRows: [][]string{\n\t\t\t\t\t{\"#1\", \"Add a new | validator\", \"succeed\", \"2024-01-01\"},\n\t\t\t\t\t{\"#2\", \"Change parameter\", \"timed out\", \"2024-01-02\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: `| ID | Title | Status | Date |\n| --- | --- | --- | --- |\n| #1 | Add a new \u0026#124; validator | succeed | 2024-01-01 |\n| #2 | Change parameter | timed out | 2024-01-02 |\n`,\n\t\t},\n\t\t{\n\t\t\tname: \"With Varying Row Sizes\", // XXX: should we have a different behavior?\n\t\t\ttable: mdtable.Table{\n\t\t\t\tHeaders: []string{\"ID\", \"Title\"},\n\t\t\t\tRows: [][]string{\n\t\t\t\t\t{\"#1\", \"Add a new validator\"},\n\t\t\t\t\t{\"#2\", \"Change parameter\", \"Extra Column\"},\n\t\t\t\t\t{\"#3\", \"Fill pool\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: `| ID | Title |\n| --- | --- |\n| #1 | Add a new validator |\n| #2 | Change parameter | Extra Column |\n| #3 | Fill pool |\n`,\n\t\t},\n\t\t{\n\t\t\tname: \"With UTF-8 Characters\",\n\t\t\ttable: mdtable.Table{\n\t\t\t\tHeaders: []string{\"ID\", \"Title\", \"Status\", \"Date\"},\n\t\t\t\tRows: [][]string{\n\t\t\t\t\t{\"#1\", \"Café\", \"succeed\", \"2024-01-01\"},\n\t\t\t\t\t{\"#2\", \"München\", \"timed out\", \"2024-01-02\"},\n\t\t\t\t\t{\"#3\", \"São Paulo\", \"active\", \"2024-01-03\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: `| ID | Title | Status | Date |\n| --- | --- | --- | --- |\n| #1 | Café | succeed | 2024-01-01 |\n| #2 | München | timed out | 2024-01-02 |\n| #3 | São Paulo | active | 2024-01-03 |\n`,\n\t\t},\n\t\t{\n\t\t\tname:     \"With no Headers and no Rows\",\n\t\t\ttable:    mdtable.Table{},\n\t\t\texpected: ``,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := tt.table.String()\n\t\t\turequire.Equal(t, got, tt.expected)\n\t\t})\n\t}\n}\n\nfunc TestTableAppend(t *testing.T) {\n\ttable := mdtable.Table{\n\t\tHeaders: []string{\"ID\", \"Title\", \"Status\", \"Date\"},\n\t}\n\n\t// Use the Append method to add rows to the table\n\ttable.Append([]string{\"#1\", \"Add a new validator\", \"succeed\", \"2024-01-01\"})\n\ttable.Append([]string{\"#2\", \"Change parameter\", \"timed out\", \"2024-01-02\"})\n\ttable.Append([]string{\"#3\", \"Fill pool\", \"active\", \"2024-01-03\"})\n\tgot := table.String()\n\n\texpected := `| ID | Title | Status | Date |\n| --- | --- | --- | --- |\n| #1 | Add a new validator | succeed | 2024-01-01 |\n| #2 | Change parameter | timed out | 2024-01-02 |\n| #3 | Fill pool | active | 2024-01-03 |\n`\n\turequire.Equal(t, got, expected)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"iMOh/4gspAwUHUzJ/GeQefkQQ31KyXEO56u3mxMtyYJ252pZaam1mpESEZL7ntencEqsDHkMUcQ8f3YWIdwQ3g=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","package":{"name":"realmpath","path":"gno.land/p/moul/realmpath/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/realmpath/v0\"\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/v0\"\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"},{"name":"realmpath_test.gno","body":"package realmpath_test\n\nimport (\n\t\"chain/runtime\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"gno.land/p/moul/realmpath/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestExample(t *testing.T) {\n\tcd := runtime.ChainDomain()\n\ttesting.SetRealm(testing.NewCodeRealm(cd + \"/r/lorem/ipsum\"))\n\n\t// initial parsing\n\tpath := \"hello/world?foo=bar\u0026baz=foobar\"\n\treq := realmpath.Parse(path)\n\turequire.False(t, req == nil, \"req should not be nil\")\n\tuassert.Equal(t, req.Path, \"hello/world\")\n\tuassert.Equal(t, req.Query.Get(\"foo\"), \"bar\")\n\tuassert.Equal(t, req.Query.Get(\"baz\"), \"foobar\")\n\tuassert.Equal(t, req.String(), \"/r/lorem/ipsum:hello/world?baz=foobar\u0026foo=bar\")\n\n\t// alter query\n\treq.Query.Set(\"hey\", \"salut\")\n\tuassert.Equal(t, req.String(), \"/r/lorem/ipsum:hello/world?baz=foobar\u0026foo=bar\u0026hey=salut\")\n\n\t// alter path\n\treq.Path = \"bye/ciao\"\n\tuassert.Equal(t, req.String(), \"/r/lorem/ipsum:bye/ciao?baz=foobar\u0026foo=bar\u0026hey=salut\")\n}\n\nfunc TestParse(t *testing.T) {\n\tcd := runtime.ChainDomain()\n\ttesting.SetRealm(testing.NewCodeRealm(cd + \"/r/lorem/ipsum\"))\n\n\ttests := []struct {\n\t\trawPath        string\n\t\trealm_XXX      string // optional\n\t\texpectedPath   string\n\t\texpectedQuery  url.Values\n\t\texpectedString string\n\t}{\n\t\t{\n\t\t\trawPath:      \"hello/world?foo=bar\u0026baz=foobar\",\n\t\t\texpectedPath: \"hello/world\",\n\t\t\texpectedQuery: url.Values{\n\t\t\t\t\"foo\": []string{\"bar\"},\n\t\t\t\t\"baz\": []string{\"foobar\"},\n\t\t\t},\n\t\t\texpectedString: \"/r/lorem/ipsum:hello/world?baz=foobar\u0026foo=bar\",\n\t\t},\n\t\t{\n\t\t\trawPath:      \"api/v1/resource?search=test\u0026limit=10\",\n\t\t\texpectedPath: \"api/v1/resource\",\n\t\t\texpectedQuery: url.Values{\n\t\t\t\t\"search\": []string{\"test\"},\n\t\t\t\t\"limit\":  []string{\"10\"},\n\t\t\t},\n\t\t\texpectedString: \"/r/lorem/ipsum:api/v1/resource?limit=10\u0026search=test\",\n\t\t},\n\t\t{\n\t\t\trawPath:        \"singlepath\",\n\t\t\texpectedPath:   \"singlepath\",\n\t\t\texpectedQuery:  url.Values{},\n\t\t\texpectedString: \"/r/lorem/ipsum:singlepath\",\n\t\t},\n\t\t{\n\t\t\trawPath:        \"path/with/trailing/slash/\",\n\t\t\texpectedPath:   \"path/with/trailing/slash/\",\n\t\t\texpectedQuery:  url.Values{},\n\t\t\texpectedString: \"/r/lorem/ipsum:path/with/trailing/slash\",\n\t\t},\n\t\t{\n\t\t\trawPath:        \"emptyquery?\",\n\t\t\texpectedPath:   \"emptyquery\",\n\t\t\texpectedQuery:  url.Values{},\n\t\t\texpectedString: \"/r/lorem/ipsum:emptyquery\",\n\t\t},\n\t\t{\n\t\t\trawPath:      \"path/with/special/characters/?key=val%20ue\u0026anotherKey=with%21special%23chars\",\n\t\t\texpectedPath: \"path/with/special/characters/\",\n\t\t\texpectedQuery: url.Values{\n\t\t\t\t\"key\":        []string{\"val ue\"},\n\t\t\t\t\"anotherKey\": []string{\"with!special#chars\"},\n\t\t\t},\n\t\t\texpectedString: \"/r/lorem/ipsum:path/with/special/characters?anotherKey=with%21special%23chars\u0026key=val+ue\",\n\t\t},\n\t\t{\n\t\t\trawPath:      \"path/with/empty/key?keyEmpty\u0026=valueEmpty\",\n\t\t\texpectedPath: \"path/with/empty/key\",\n\t\t\texpectedQuery: url.Values{\n\t\t\t\t\"keyEmpty\": []string{\"\"},\n\t\t\t\t\"\":         []string{\"valueEmpty\"},\n\t\t\t},\n\t\t\texpectedString: \"/r/lorem/ipsum:path/with/empty/key?=valueEmpty\u0026keyEmpty=\",\n\t\t},\n\t\t{\n\t\t\trawPath:      \"path/with/multiple/empty/keys?=empty1\u0026=empty2\",\n\t\t\texpectedPath: \"path/with/multiple/empty/keys\",\n\t\t\texpectedQuery: url.Values{\n\t\t\t\t\"\": []string{\"empty1\", \"empty2\"},\n\t\t\t},\n\t\t\texpectedString: \"/r/lorem/ipsum:path/with/multiple/empty/keys?=empty1\u0026=empty2\",\n\t\t},\n\t\t{\n\t\t\trawPath:      \"path/with/percent-encoded/%20space?query=hello%20world\",\n\t\t\texpectedPath: \"path/with/percent-encoded/%20space\", // XXX: should we decode?\n\t\t\texpectedQuery: url.Values{\n\t\t\t\t\"query\": []string{\"hello world\"},\n\t\t\t},\n\t\t\texpectedString: \"/r/lorem/ipsum:path/with/percent-encoded/%20space?query=hello+world\",\n\t\t},\n\t\t{\n\t\t\trawPath:      \"path/with/very/long/query?key1=value1\u0026key2=value2\u0026key3=value3\u0026key4=value4\u0026key5=value5\u0026key6=value6\",\n\t\t\texpectedPath: \"path/with/very/long/query\",\n\t\t\texpectedQuery: url.Values{\n\t\t\t\t\"key1\": []string{\"value1\"},\n\t\t\t\t\"key2\": []string{\"value2\"},\n\t\t\t\t\"key3\": []string{\"value3\"},\n\t\t\t\t\"key4\": []string{\"value4\"},\n\t\t\t\t\"key5\": []string{\"value5\"},\n\t\t\t\t\"key6\": []string{\"value6\"},\n\t\t\t},\n\t\t\texpectedString: \"/r/lorem/ipsum:path/with/very/long/query?key1=value1\u0026key2=value2\u0026key3=value3\u0026key4=value4\u0026key5=value5\u0026key6=value6\",\n\t\t},\n\t\t{\n\t\t\trawPath:      \"custom/realm?foo=bar\u0026baz=foobar\",\n\t\t\trealm_XXX:    cd + \"/r/foo/bar\",\n\t\t\texpectedPath: \"custom/realm\",\n\t\t\texpectedQuery: url.Values{\n\t\t\t\t\"foo\": []string{\"bar\"},\n\t\t\t\t\"baz\": []string{\"foobar\"},\n\t\t\t},\n\t\t\texpectedString: \"/r/foo/bar:custom/realm?baz=foobar\u0026foo=bar\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.rawPath, func(t *testing.T) {\n\t\t\treq := realmpath.Parse(tt.rawPath)\n\t\t\treq.Realm = tt.realm_XXX // set optional realm\n\t\t\turequire.False(t, req == nil, \"req should not be nil\")\n\t\t\tuassert.Equal(t, req.Path, tt.expectedPath)\n\t\t\turequire.Equal(t, len(req.Query), len(tt.expectedQuery))\n\t\t\tuassert.Equal(t, req.Query.Encode(), tt.expectedQuery.Encode())\n\t\t\t// XXX: uassert.Equal(t, req.Query, tt.expectedQuery)\n\t\t\tuassert.Equal(t, req.String(), tt.expectedString)\n\t\t})\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"Ryw/9ph6HJ97MSOwmxNlLPoxcg978+8E9F+wS7HdwqFjWIGvTIOhc3SxIQOQXAfnH+6AU3fxj3C39ZtO6Xx2yA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g15ge0ae9077eh40erwrn2eq0xw6wupwqthpv34l","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/v0 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":"addrset_test.gno","body":"package addrset\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nconst (\n\taddr1 = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\taddr2 = address(\"g1manfred47kzduec920z88wfr64ylksmdcedlf5\")\n\taddr3 = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\")\n)\n\nfunc TestSetBasics(t *testing.T) {\n\tvar set Set // the zero value is a usable empty set\n\n\tuassert.Equal(t, 0, set.Size())\n\tuassert.False(t, set.Has(addr1))\n\tuassert.False(t, set.Remove(addr1))\n\n\tuassert.True(t, set.Add(addr1), \"expect newly added\")\n\tuassert.False(t, set.Add(addr1), \"expect duplicate add to report existing\")\n\tuassert.True(t, set.Has(addr1))\n\tuassert.Equal(t, 1, set.Size())\n\n\tuassert.True(t, set.Add(addr2))\n\tuassert.True(t, set.Add(addr3))\n\tuassert.Equal(t, 3, set.Size())\n\n\tuassert.True(t, set.Remove(addr2), \"expect removal of present address\")\n\tuassert.False(t, set.Remove(addr2), \"expect second removal to report absent\")\n\tuassert.False(t, set.Has(addr2))\n\tuassert.Equal(t, 2, set.Size())\n}\n\nfunc TestSetIteration(t *testing.T) {\n\tvar set Set\n\tset.Add(addr1)\n\tset.Add(addr2)\n\tset.Add(addr3)\n\n\t// Sorted (bech32-string) order: addr1 \u003c addr2 \u003c addr3.\n\tvar got []address\n\tset.IterateByOffset(0, set.Size(), func(a address) bool {\n\t\tgot = append(got, a)\n\t\treturn false\n\t})\n\turequire.Equal(t, 3, len(got))\n\tuassert.True(t, got[0] == addr1 \u0026\u0026 got[1] == addr2 \u0026\u0026 got[2] == addr3, \"expect sorted order\")\n\n\t// Offset and count window.\n\tgot = nil\n\tset.IterateByOffset(1, 1, func(a address) bool {\n\t\tgot = append(got, a)\n\t\treturn false\n\t})\n\turequire.Equal(t, 1, len(got))\n\tuassert.True(t, got[0] == addr2, \"expect the middle element\")\n\n\t// Early stop.\n\tcount := 0\n\tset.IterateByOffset(0, set.Size(), func(a address) bool {\n\t\tcount++\n\t\treturn true\n\t})\n\tuassert.Equal(t, 1, count)\n\n\t// Reverse order.\n\tgot = nil\n\tset.ReverseIterateByOffset(0, set.Size(), func(a address) bool {\n\t\tgot = append(got, a)\n\t\treturn false\n\t})\n\turequire.Equal(t, 3, len(got))\n\tuassert.True(t, got[0] == addr3 \u0026\u0026 got[2] == addr1, \"expect reverse order\")\n}\n\nfunc TestSetIterationLimits(t *testing.T) {\n\tvar set Set\n\tset.Add(addr1)\n\tset.Add(addr2)\n\tset.Add(addr3)\n\n\t// Offset at/beyond the size visits nothing, in both directions.\n\tvisits := 0\n\tcount := func(a address) bool { visits++; return false }\n\tset.IterateByOffset(3, 10, count)\n\tset.IterateByOffset(99, 10, count)\n\tset.ReverseIterateByOffset(3, 10, count)\n\tuassert.Equal(t, 0, visits)\n\n\t// A count larger than the remainder visits just the remainder.\n\tset.IterateByOffset(1, 10, count)\n\tuassert.Equal(t, 2, visits)\n\n\t// The reverse offset counts from the END: offset 1, count 1 is the\n\t// second-to-last address in sorted order (addr2: addr1 \u003c addr2 \u003c addr3).\n\tvar got []address\n\tset.ReverseIterateByOffset(1, 1, func(a address) bool {\n\t\tgot = append(got, a)\n\t\treturn false\n\t})\n\turequire.Equal(t, 1, len(got))\n\tuassert.True(t, got[0] == addr2, \"expect reverse offset anchored at the end\")\n}\n\n// TestSetAcrossLeafSplits exercises the wrapper over a multi-leaf tree\n// (the backing splits past fanout 32): ordering, offset windows in both\n// directions, and removal back down through merges.\nfunc TestSetAcrossLeafSplits(t *testing.T) {\n\tconst n = 100\n\n\tvar (\n\t\tset   Set\n\t\taddrs []address\n\t)\n\tfor i := 0; i \u003c n; i++ {\n\t\t// Zero-padded so generation order == lexicographic order.\n\t\ts := strconv.Itoa(i)\n\t\taddrs = append(addrs, address(\"g1\"+strings.Repeat(\"0\", 38-len(s))+s))\n\t}\n\tfor _, a := range addrs {\n\t\turequire.True(t, set.Add(a), \"expect newly added\")\n\t}\n\turequire.Equal(t, n, set.Size())\n\n\t// Full forward iteration is complete and sorted (keys were generated\n\t// in sorted order).\n\ti := 0\n\tset.IterateByOffset(0, n, func(a address) bool {\n\t\turequire.True(t, a == addrs[i], \"expect sorted order at \"+strconv.Itoa(i))\n\t\ti++\n\t\treturn false\n\t})\n\turequire.Equal(t, n, i)\n\n\t// An offset window that crosses leaf boundaries.\n\tvar got []address\n\tset.IterateByOffset(30, 10, func(a address) bool {\n\t\tgot = append(got, a)\n\t\treturn false\n\t})\n\turequire.Equal(t, 10, len(got))\n\tuassert.True(t, got[0] == addrs[30] \u0026\u0026 got[9] == addrs[39], \"expect window [30,40)\")\n\n\t// Reverse window anchored from the end.\n\tgot = nil\n\tset.ReverseIterateByOffset(10, 5, func(a address) bool {\n\t\tgot = append(got, a)\n\t\treturn false\n\t})\n\turequire.Equal(t, 5, len(got))\n\tuassert.True(t, got[0] == addrs[n-11] \u0026\u0026 got[4] == addrs[n-15], \"expect reverse window from the end\")\n\n\t// Remove every other address (drives leaf merges), then verify.\n\tfor i := 0; i \u003c n; i += 2 {\n\t\turequire.True(t, set.Remove(addrs[i]), \"expect removal\")\n\t}\n\turequire.Equal(t, n/2, set.Size())\n\tfor i := 0; i \u003c n; i++ {\n\t\tuassert.Equal(t, i%2 == 1, set.Has(addrs[i]))\n\t}\n}\n\nfunc TestReadonlySet(t *testing.T) {\n\tvar set Set\n\tset.Add(addr1)\n\n\tview := set.Readonly()\n\tuassert.True(t, view.Has(addr1))\n\tuassert.Equal(t, 1, view.Size())\n\n\t// The view is a live handle, not a snapshot.\n\tset.Add(addr2)\n\tuassert.Equal(t, 2, view.Size())\n\tuassert.True(t, view.Has(addr2))\n\n\t// Iteration mirrors the set and reports early stop.\n\tstopped := view.IterateByOffset(0, view.Size(), func(a address) bool { return false })\n\tuassert.False(t, stopped)\n\tstopped = view.IterateByOffset(0, view.Size(), func(a address) bool { return true })\n\tuassert.True(t, stopped)\n\n\tstopped = view.ReverseIterateByOffset(0, view.Size(), func(a address) bool { return true })\n\tuassert.True(t, stopped)\n\n\t// An empty set's view iterates nothing and reports not-stopped.\n\tempty := NewReadonlySet(\u0026Set{})\n\tuassert.Equal(t, 0, empty.Size())\n\tstopped = empty.IterateByOffset(0, 10, func(a address) bool { return true })\n\tuassert.False(t, stopped)\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"7H6BgT8bCuLJhyOLPHWXeuXoOwqhBLzhOLrj7YZeHjRaoq7YeY5pNchc62DgUifo53A/2J/ySaBjTn0SqXiwiA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"rotree","path":"gno.land/p/nt/bptree/rotree/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/bptree/rotree/v0\"\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"},{"name":"rotree_test.gno","body":"package rotree\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/bptree/v0\"\n)\n\nfunc TestExample(t *testing.T) {\n\t// User represents our internal data structure\n\ttype User struct {\n\t\tID       string\n\t\tName     string\n\t\tBalance  int\n\t\tInternal string // sensitive internal data\n\t}\n\n\t// Create and populate the original tree with user pointers\n\ttree := bptree.NewBPTree32()\n\ttree.Set(\"alice\", \u0026User{\n\t\tID:       \"1\",\n\t\tName:     \"Alice\",\n\t\tBalance:  100,\n\t\tInternal: \"sensitive_data_1\",\n\t})\n\ttree.Set(\"bob\", \u0026User{\n\t\tID:       \"2\",\n\t\tName:     \"Bob\",\n\t\tBalance:  200,\n\t\tInternal: \"sensitive_data_2\",\n\t})\n\n\t// Define a makeEntrySafeFn that:\n\t// 1. Creates a defensive copy of the User struct\n\t// 2. Omits sensitive internal data\n\tmakeEntrySafeFn := func(v any) any {\n\t\toriginalUser := v.(*User)\n\t\treturn \u0026User{\n\t\t\tID:       originalUser.ID,\n\t\t\tName:     originalUser.Name,\n\t\t\tBalance:  originalUser.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\troTree := Wrap(tree, makeEntrySafeFn)\n\n\t// Test retrieving and verifying a user\n\tt.Run(\"Get User\", func(t *testing.T) {\n\t\t// Get user from read-only tree\n\t\tvalue := roTree.Get(\"alice\")\n\t\tif value == nil {\n\t\t\tt.Fatal(\"User 'alice' not found\")\n\t\t}\n\n\t\tuser := value.(*User)\n\n\t\t// Verify user data is correct\n\t\tif user.Name != \"Alice\" || user.Balance != 100 {\n\t\t\tt.Errorf(\"Unexpected user data: got name=%s balance=%d\", user.Name, user.Balance)\n\t\t}\n\n\t\t// Verify sensitive data is not exposed\n\t\tif user.Internal != \"\" {\n\t\t\tt.Error(\"Sensitive data should not be exposed\")\n\t\t}\n\n\t\t// Verify it's a different instance than the original\n\t\toriginalValue := tree.Get(\"alice\")\n\t\toriginalUser := originalValue.(*User)\n\t\tif user == originalUser {\n\t\t\tt.Error(\"Read-only tree should return a copy, not the original pointer\")\n\t\t}\n\t})\n\n\t// Test iterating over users\n\tt.Run(\"Iterate Users\", func(t *testing.T) {\n\t\tcount := 0\n\t\troTree.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\t\tuser := value.(*User)\n\t\t\t// Verify each user has empty Internal field\n\t\t\tif user.Internal != \"\" {\n\t\t\t\tt.Error(\"Sensitive data exposed during iteration\")\n\t\t\t}\n\t\t\tcount++\n\t\t\treturn false\n\t\t})\n\n\t\tif count != 2 {\n\t\t\tt.Errorf(\"Expected 2 users, got %d\", count)\n\t\t}\n\t})\n\n\t// Verify that modifications to the returned user don't affect the original\n\tt.Run(\"Modification Safety\", func(t *testing.T) {\n\t\tvalue := roTree.Get(\"alice\")\n\t\tuser := value.(*User)\n\n\t\t// Try to modify the returned user\n\t\tuser.Balance = 999\n\t\tuser.Internal = \"hacked\"\n\n\t\t// Verify original is unchanged\n\t\toriginalValue := tree.Get(\"alice\")\n\t\toriginalUser := originalValue.(*User)\n\t\tif originalUser.Balance != 100 || originalUser.Internal != \"sensitive_data_1\" {\n\t\t\tt.Error(\"Original user data was modified\")\n\t\t}\n\t})\n}\n\nfunc TestReadOnlyTree(t *testing.T) {\n\t// Example of a makeEntrySafeFn that appends \"_readonly\" to demonstrate transformation\n\tmakeEntrySafeFn := func(value any) any {\n\t\treturn value.(string) + \"_readonly\"\n\t}\n\n\ttree := bptree.NewBPTree32()\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\ttree.Set(\"key3\", \"value3\")\n\n\troTree := Wrap(tree, makeEntrySafeFn)\n\n\ttests := []struct {\n\t\tname     string\n\t\tkey      string\n\t\texpected any\n\t}{\n\t\t{\"ExistingKey1\", \"key1\", \"value1_readonly\"},\n\t\t{\"ExistingKey2\", \"key2\", \"value2_readonly\"},\n\t\t{\"NonExistingKey\", \"key4\", nil},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvalue := roTree.Get(tt.key)\n\t\t\tif value != tt.expected {\n\t\t\t\tt.Errorf(\"For key %s, expected %v, got %v\", tt.key, tt.expected, value)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Add example tests showing different makeEntrySafeFn implementations\nfunc TestMakeEntrySafeFnVariants(t *testing.T) {\n\ttree := bptree.NewBPTree32()\n\ttree.Set(\"slice\", []int{1, 2, 3})\n\ttree.Set(\"map\", map[string]int{\"a\": 1})\n\n\ttests := []struct {\n\t\tname            string\n\t\tmakeEntrySafeFn func(any) any\n\t\tkey             string\n\t\tvalidate        func(t *testing.T, value any)\n\t}{\n\t\t{\n\t\t\tname: \"Defensive Copy Slice\",\n\t\t\tmakeEntrySafeFn: func(v any) any {\n\t\t\t\toriginal := v.([]int)\n\t\t\t\treturn append([]int{}, original...)\n\t\t\t},\n\t\t\tkey: \"slice\",\n\t\t\tvalidate: func(t *testing.T, value any) {\n\t\t\t\tslice := value.([]int)\n\t\t\t\t// Modify the returned slice\n\t\t\t\tslice[0] = 999\n\t\t\t\t// Verify original is unchanged\n\t\t\t\toriginalValue := tree.Get(\"slice\")\n\t\t\t\toriginal := originalValue.([]int)\n\t\t\t\tif original[0] != 1 {\n\t\t\t\t\tt.Error(\"Original slice was modified\")\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t// Add more test cases for different makeEntrySafeFn implementations\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\troTree := Wrap(tree, tt.makeEntrySafeFn)\n\t\t\tvalue := roTree.Get(tt.key)\n\t\t\tif value == nil {\n\t\t\t\tt.Fatal(\"Key not found\")\n\t\t\t}\n\t\t\ttt.validate(t, value)\n\t\t})\n\t}\n}\n\nfunc TestNilMakeEntrySafeFn(t *testing.T) {\n\t// Create a tree with some test data\n\ttree := bptree.NewBPTree32()\n\toriginalValue := []int{1, 2, 3}\n\ttree.Set(\"test\", originalValue)\n\n\t// Create a ReadOnlyTree with nil makeEntrySafeFn\n\troTree := Wrap(tree, nil)\n\n\t// Test that we get back the original value\n\tvalue := roTree.Get(\"test\")\n\tif value == nil {\n\t\tt.Fatal(\"Key not found\")\n\t}\n\n\t// Verify it's the exact same slice (not a copy)\n\tretrievedSlice := value.([]int)\n\tif \u0026retrievedSlice[0] != \u0026originalValue[0] {\n\t\tt.Error(\"Expected to get back the original slice reference\")\n\t}\n\n\t// Test through iteration as well\n\troTree.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\tretrievedSlice := value.([]int)\n\t\tif \u0026retrievedSlice[0] != \u0026originalValue[0] {\n\t\t\tt.Error(\"Expected to get back the original slice reference in iteration\")\n\t\t}\n\t\treturn false\n\t})\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"F1vKls5xvZWTaLTuYgkngD208WvKGa0vrY8HLC3HJs0iVyDfIi/7HLRXGyoZz37a7R1PwEFwkYAqjEdJfPzIRA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"list","path":"gno.land/p/nt/bptree/list/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/bptree/list/v0\"\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/rotree/v0\"\n\t\"gno.land/p/nt/bptree/v0\"\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"},{"name":"list_test.gno","body":"package list\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nfunc TestList_Basic(t *testing.T) {\n\tvar l List\n\n\t// Test empty list\n\tif l.Len() != 0 {\n\t\tt.Errorf(\"new list should be empty, got len %d\", l.Len())\n\t}\n\n\t// Test append and length\n\tl.Append(1, 2, 3)\n\tif l.Len() != 3 {\n\t\tt.Errorf(\"expected len 3, got %d\", l.Len())\n\t}\n\n\t// Test get\n\tif v, _ := l.Get(0); v != 1 {\n\t\tt.Errorf(\"expected 1 at index 0, got %v\", v)\n\t}\n\tif v, _ := l.Get(1); v != 2 {\n\t\tt.Errorf(\"expected 2 at index 1, got %v\", v)\n\t}\n\tif v, _ := l.Get(2); v != 3 {\n\t\tt.Errorf(\"expected 3 at index 2, got %v\", v)\n\t}\n\n\t// Test out of bounds\n\tif _, ok := l.Get(-1); ok {\n\t\tt.Error(\"expected ok=false for negative index\")\n\t}\n\tif _, ok := l.Get(3); ok {\n\t\tt.Error(\"expected ok=false for out of bounds index\")\n\t}\n}\n\nfunc TestList_Set(t *testing.T) {\n\tvar l List\n\tl.Append(1, 2, 3)\n\n\t// Test valid set within bounds\n\tif ok := l.Set(1, 42); !ok {\n\t\tt.Error(\"Set should return true for valid index\")\n\t}\n\tif v, _ := l.Get(1); v != 42 {\n\t\tt.Errorf(\"expected 42 after Set, got %v\", v)\n\t}\n\n\t// Test set at size (append)\n\tif ok := l.Set(3, 4); !ok {\n\t\tt.Error(\"Set should return true when appending at size\")\n\t}\n\tif v, _ := l.Get(3); v != 4 {\n\t\tt.Errorf(\"expected 4 after Set at size, got %v\", v)\n\t}\n\n\t// Test invalid sets\n\tif ok := l.Set(-1, 10); ok {\n\t\tt.Error(\"Set should return false for negative index\")\n\t}\n\tif ok := l.Set(5, 10); ok {\n\t\tt.Error(\"Set should return false for index \u003e size\")\n\t}\n\n\t// Verify list state hasn't changed after invalid operations\n\texpected := []any{1, 42, 3, 4}\n\tfor i, want := range expected {\n\t\tif got, _ := l.Get(i); got != want {\n\t\t\tt.Errorf(\"index %d = %v; want %v\", i, got, want)\n\t\t}\n\t}\n}\n\nfunc TestList_Delete(t *testing.T) {\n\tvar l List\n\tl.Append(1, 2, 3)\n\n\t// Test valid delete\n\tif v, ok := l.Delete(1); !ok || v != 2 {\n\t\tt.Errorf(\"Delete(1) = %v, %v; want 2, true\", v, ok)\n\t}\n\tif l.Len() != 2 {\n\t\tt.Errorf(\"expected len 2 after delete, got %d\", l.Len())\n\t}\n\tif v, _ := l.Get(1); v != 3 {\n\t\tt.Errorf(\"expected 3 at index 1 after delete, got %v\", v)\n\t}\n\n\t// Test invalid delete\n\tif v, ok := l.Delete(-1); ok || v != nil {\n\t\tt.Errorf(\"Delete(-1) = %v, %v; want nil, false\", v, ok)\n\t}\n\tif v, ok := l.Delete(2); ok || v != nil {\n\t\tt.Errorf(\"Delete(2) = %v, %v; want nil, false\", v, ok)\n\t}\n}\n\nfunc TestList_Slice(t *testing.T) {\n\tvar l List\n\tl.Append(1, 2, 3, 4, 5)\n\n\t// Test valid ranges\n\tvalues := l.Slice(1, 4)\n\texpected := []any{2, 3, 4}\n\tif !sliceEqual(values, expected) {\n\t\tt.Errorf(\"Slice(1,4) = %v; want %v\", values, expected)\n\t}\n\n\t// Test edge cases\n\tif values := l.Slice(-1, 2); !sliceEqual(values, []any{1, 2}) {\n\t\tt.Errorf(\"Slice(-1,2) = %v; want [1 2]\", values)\n\t}\n\tif values := l.Slice(3, 10); !sliceEqual(values, []any{4, 5}) {\n\t\tt.Errorf(\"Slice(3,10) = %v; want [4 5]\", values)\n\t}\n\tif values := l.Slice(3, 2); values != nil {\n\t\tt.Errorf(\"Slice(3,2) = %v; want nil\", values)\n\t}\n}\n\nfunc TestList_ForEach(t *testing.T) {\n\tvar l List\n\tl.Append(1, 2, 3)\n\n\tsum := 0\n\tl.ForEach(func(index int, value any) bool {\n\t\tsum += value.(int)\n\t\treturn false\n\t})\n\n\tif sum != 6 {\n\t\tt.Errorf(\"ForEach sum = %d; want 6\", sum)\n\t}\n\n\t// Test early termination\n\tcount := 0\n\tl.ForEach(func(index int, value any) bool {\n\t\tcount++\n\t\treturn true // stop after first item\n\t})\n\n\tif count != 1 {\n\t\tt.Errorf(\"ForEach early termination count = %d; want 1\", count)\n\t}\n}\n\nfunc TestList_Clone(t *testing.T) {\n\tvar l List\n\tl.Append(1, 2, 3)\n\n\tclone := l.Clone()\n\n\t// Test same length\n\tif clone.Len() != l.Len() {\n\t\tt.Errorf(\"clone.Len() = %d; want %d\", clone.Len(), l.Len())\n\t}\n\n\t// Test same values\n\tfor i := 0; i \u003c l.Len(); i++ {\n\t\tcv, _ := clone.Get(i)\n\t\tlv, _ := l.Get(i)\n\t\tif cv != lv {\n\t\t\tt.Errorf(\"clone.Get(%d) = %v; want %v\", i, cv, lv)\n\t\t}\n\t}\n\n\t// Test independence\n\tl.Set(0, 42)\n\tcv, _ := clone.Get(0)\n\tlv, _ := l.Get(0)\n\tif cv == lv {\n\t\tt.Error(\"clone should be independent of original\")\n\t}\n\n\t// Test that appending to both after clone works independently.\n\tvar l2 List\n\tl2.Append(\"a\", \"b\", \"c\")\n\tclone2 := l2.Clone()\n\n\tl2.Append(\"d\", \"e\")\n\tclone2.Append(\"x\", \"y\")\n\n\tif l2.Len() != 5 {\n\t\tt.Errorf(\"original after append: Len() = %d; want 5\", l2.Len())\n\t}\n\tif clone2.Len() != 5 {\n\t\tt.Errorf(\"clone after append: Len() = %d; want 5\", clone2.Len())\n\t}\n\torigExpected := []any{\"a\", \"b\", \"c\", \"d\", \"e\"}\n\tcloneExpected := []any{\"a\", \"b\", \"c\", \"x\", \"y\"}\n\tfor i := 0; i \u003c 5; i++ {\n\t\tov, _ := l2.Get(i)\n\t\tif ov != origExpected[i] {\n\t\t\tt.Errorf(\"original.Get(%d) = %v; want %v\", i, ov, origExpected[i])\n\t\t}\n\t\tcv, _ := clone2.Get(i)\n\t\tif cv != cloneExpected[i] {\n\t\t\tt.Errorf(\"clone.Get(%d) = %v; want %v\", i, cv, cloneExpected[i])\n\t\t}\n\t}\n}\n\nfunc TestList_DeleteRange(t *testing.T) {\n\tvar l List\n\tl.Append(1, 2, 3, 4, 5)\n\n\t// Test valid range delete\n\tdeleted := l.DeleteRange(1, 4)\n\tif deleted != 3 {\n\t\tt.Errorf(\"DeleteRange(1,4) deleted %d elements; want 3\", deleted)\n\t}\n\tif l.Len() != 2 {\n\t\tt.Errorf(\"after DeleteRange(1,4) len = %d; want 2\", l.Len())\n\t}\n\texpected := []any{1, 5}\n\tfor i, want := range expected {\n\t\tif got, _ := l.Get(i); got != want {\n\t\t\tt.Errorf(\"after DeleteRange(1,4) index %d = %v; want %v\", i, got, want)\n\t\t}\n\t}\n\n\t// Test edge cases\n\tl = List{}\n\tl.Append(1, 2, 3)\n\n\t// Delete with negative start\n\tif deleted := l.DeleteRange(-1, 2); deleted != 2 {\n\t\tt.Errorf(\"DeleteRange(-1,2) deleted %d elements; want 2\", deleted)\n\t}\n\n\t// Delete with end \u003e length\n\tl = List{}\n\tl.Append(1, 2, 3)\n\tif deleted := l.DeleteRange(1, 5); deleted != 2 {\n\t\tt.Errorf(\"DeleteRange(1,5) deleted %d elements; want 2\", deleted)\n\t}\n\n\t// Delete invalid range\n\tif deleted := l.DeleteRange(2, 1); deleted != 0 {\n\t\tt.Errorf(\"DeleteRange(2,1) deleted %d elements; want 0\", deleted)\n\t}\n\n\t// Delete empty range\n\tif deleted := l.DeleteRange(1, 1); deleted != 0 {\n\t\tt.Errorf(\"DeleteRange(1,1) deleted %d elements; want 0\", deleted)\n\t}\n}\n\nfunc TestList_Tree(t *testing.T) {\n\tvar l List\n\tl.Append(1, 2, 3)\n\n\trotree := l.Tree()\n\texpected := 3\n\n\t// Reverse iterate through the ReadOnlyTree\n\trotree.ReverseIterateByOffset(0, rotree.Size(), func(key string, value any) bool {\n\t\tintValue := value.(int)\n\t\tif intValue != expected {\n\t\t\tt.Errorf(\"ReadOnlyTree expected %d, got %d\", expected, intValue)\n\t\t}\n\t\texpected--\n\t\treturn false\n\t})\n}\n\nfunc TestList_EmptyOperations(t *testing.T) {\n\tvar l List\n\n\t// Operations on empty list\n\tif _, ok := l.Get(0); ok {\n\t\tt.Error(\"Get(0) on empty list should return ok=false\")\n\t}\n\n\t// Set should work at index 0 for empty list (append case)\n\tif ok := l.Set(0, 1); !ok {\n\t\tt.Error(\"Set(0,1) on empty list = false; want true\")\n\t}\n\tif v, _ := l.Get(0); v != 1 {\n\t\tt.Errorf(\"Get(0) after Set = %v; want 1\", v)\n\t}\n\n\tl = List{} // Reset to empty list\n\tif v, ok := l.Delete(0); ok || v != nil {\n\t\tt.Errorf(\"Delete(0) on empty list = %v, %v; want nil, false\", v, ok)\n\t}\n\tif values := l.Slice(0, 1); values != nil {\n\t\tt.Errorf(\"Range(0,1) on empty list = %v; want nil\", values)\n\t}\n}\n\nfunc TestList_DifferentTypes(t *testing.T) {\n\tvar l List\n\n\t// Test with different types\n\tl.Append(42, \"hello\", true, 3.14)\n\n\tv0, _ := l.Get(0)\n\tif v := v0.(int); v != 42 {\n\t\tt.Errorf(\"Get(0) = %v; want 42\", v)\n\t}\n\tv1, _ := l.Get(1)\n\tif v := v1.(string); v != \"hello\" {\n\t\tt.Errorf(\"Get(1) = %v; want 'hello'\", v)\n\t}\n\tv2, _ := l.Get(2)\n\tif v := v2.(bool); !v {\n\t\tt.Errorf(\"Get(2) = %v; want true\", v)\n\t}\n\tv3, _ := l.Get(3)\n\tif v := v3.(float64); v != 3.14 {\n\t\tt.Errorf(\"Get(3) = %v; want 3.14\", v)\n\t}\n}\n\nfunc TestList_LargeOperations(t *testing.T) {\n\tvar l List\n\n\t// Test with larger number of elements\n\tn := 1000\n\tfor i := 0; i \u003c n; i++ {\n\t\tl.Append(i)\n\t}\n\n\tif l.Len() != n {\n\t\tt.Errorf(\"Len() = %d; want %d\", l.Len(), n)\n\t}\n\n\t// Test range on large list\n\tvalues := l.Slice(n-3, n)\n\texpected := []any{n - 3, n - 2, n - 1}\n\tif !sliceEqual(values, expected) {\n\t\tt.Errorf(\"Range(%d,%d) = %v; want %v\", n-3, n, values, expected)\n\t}\n\n\t// Test large range deletion\n\tdeleted := l.DeleteRange(100, 900)\n\tif deleted != 800 {\n\t\tt.Errorf(\"DeleteRange(100,900) = %d; want 800\", deleted)\n\t}\n\tif l.Len() != 200 {\n\t\tt.Errorf(\"Len() after large delete = %d; want 200\", l.Len())\n\t}\n}\n\nfunc TestList_ChainedOperations(t *testing.T) {\n\tvar l List\n\n\t// Test sequence of operations\n\tl.Append(1, 2, 3)\n\tl.Delete(1)\n\tl.Append(4)\n\tl.Set(1, 5)\n\n\texpected := []any{1, 5, 4}\n\tfor i, want := range expected {\n\t\tif got, _ := l.Get(i); got != want {\n\t\t\tt.Errorf(\"index %d = %v; want %v\", i, got, want)\n\t\t}\n\t}\n}\n\nfunc TestList_RangeEdgeCases(t *testing.T) {\n\tvar l List\n\tl.Append(1, 2, 3, 4, 5)\n\n\t// Test various edge cases for Range\n\tcases := []struct {\n\t\tstart, end int\n\t\twant       []any\n\t}{\n\t\t{-10, 2, []any{1, 2}},\n\t\t{3, 10, []any{4, 5}},\n\t\t{0, 0, nil},\n\t\t{5, 5, nil},\n\t\t{4, 3, nil},\n\t\t{-1, -1, nil},\n\t}\n\n\tfor _, tc := range cases {\n\t\tgot := l.Slice(tc.start, tc.end)\n\t\tif !sliceEqual(got, tc.want) {\n\t\t\tt.Errorf(\"Slice(%d,%d) = %v; want %v\", tc.start, tc.end, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestList_IndexConsistency(t *testing.T) {\n\tvar l List\n\n\t// Initial additions\n\tl.Append(1, 2, 3, 4, 5) // [1,2,3,4,5]\n\n\t// Delete from middle\n\tl.Delete(2) // [1,2,4,5]\n\n\t// Add more elements\n\tl.Append(6, 7) // [1,2,4,5,6,7]\n\n\t// Delete range from middle\n\tl.DeleteRange(1, 4) // [1,6,7]\n\n\t// Add more elements\n\tl.Append(8, 9, 10) // [1,6,7,8,9,10]\n\n\t// Verify sequence is continuous\n\texpected := []any{1, 6, 7, 8, 9, 10}\n\tfor i, want := range expected {\n\t\tif got, _ := l.Get(i); got != want {\n\t\t\tt.Errorf(\"index %d = %v; want %v\", i, got, want)\n\t\t}\n\t}\n\n\t// Verify no extra elements exist\n\tif l.Len() != len(expected) {\n\t\tt.Errorf(\"length = %d; want %d\", l.Len(), len(expected))\n\t}\n\n\t// Verify all indices are accessible\n\tallValues := l.Slice(0, l.Len())\n\tif !sliceEqual(allValues, expected) {\n\t\tt.Errorf(\"Slice(0, Len()) = %v; want %v\", allValues, expected)\n\t}\n\n\t// Verify no gaps in iteration\n\tvar iteratedValues []any\n\tvar indices []int\n\tl.ForEach(func(index int, value any) bool {\n\t\titeratedValues = append(iteratedValues, value)\n\t\tindices = append(indices, index)\n\t\treturn false\n\t})\n\n\t// Check values from iteration\n\tif !sliceEqual(iteratedValues, expected) {\n\t\tt.Errorf(\"ForEach values = %v; want %v\", iteratedValues, expected)\n\t}\n\n\t// Check indices are sequential\n\tfor i, idx := range indices {\n\t\tif idx != i {\n\t\t\tt.Errorf(\"ForEach index %d = %d; want %d\", i, idx, i)\n\t\t}\n\t}\n}\n\nfunc TestList_RecursiveSafety(t *testing.T) {\n\t// Create a new list\n\tl := \u0026List{}\n\n\t// Add some initial values\n\tl.Append(\"id1\")\n\tl.Append(\"id2\")\n\tl.Append(\"id3\")\n\n\t// Test deep list traversal\n\tfound := false\n\tl.ForEach(func(i int, v any) bool {\n\t\tif str, ok := v.(string); ok {\n\t\t\tif str == \"id2\" {\n\t\t\t\tfound = true\n\t\t\t\treturn true // stop iteration\n\t\t\t}\n\t\t}\n\t\treturn false // continue iteration\n\t})\n\n\tif !found {\n\t\tt.Error(\"Failed to find expected value in list\")\n\t}\n\n\tshort := testing.Short()\n\n\t// Test recursive safety by performing multiple operations\n\tfor i := 0; i \u003c 1000; i++ {\n\t\t// Add new value\n\t\tl.Append(ufmt.Sprintf(\"id%d\", i+4))\n\n\t\tif !short {\n\t\t\t// Search for a value\n\t\t\tvar lastFound bool\n\t\t\tl.ForEach(func(j int, v any) bool {\n\t\t\t\tif str, ok := v.(string); ok {\n\t\t\t\t\tif str == ufmt.Sprintf(\"id%d\", i+3) {\n\t\t\t\t\t\tlastFound = true\n\t\t\t\t\t\treturn true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn false\n\t\t\t})\n\n\t\t\tif !lastFound {\n\t\t\t\tt.Errorf(\"Failed to find value id%d after insertion\", i+3)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Verify final length\n\texpectedLen := 1003 // 3 initial + 1000 added\n\tif l.Len() != expectedLen {\n\t\tt.Errorf(\"Expected length %d, got %d\", expectedLen, l.Len())\n\t}\n\n\tif short {\n\t\tt.Skip(\"skipping extended recursive safety test in short mode\")\n\t}\n}\n\nfunc TestList_NilValueDistinguishable(t *testing.T) {\n\tvar l List\n\tl.Append(nil)\n\tl.Append(\"hello\")\n\n\t// Stored nil is distinguishable from out-of-bounds.\n\tv, ok := l.Get(0)\n\tif !ok {\n\t\tt.Error(\"Get(0) should return ok=true for stored nil\")\n\t}\n\tif v != nil {\n\t\tt.Errorf(\"Get(0) should return nil value, got %v\", v)\n\t}\n\n\t// Out of bounds returns false.\n\t_, ok = l.Get(5)\n\tif ok {\n\t\tt.Error(\"Get(5) should return ok=false for out of bounds\")\n\t}\n\n\t// Normal value still works.\n\tv, ok = l.Get(1)\n\tif !ok || v != \"hello\" {\n\t\tt.Errorf(\"Get(1) = (%v, %v); want (hello, true)\", v, ok)\n\t}\n}\n\n// Helper function to compare slices\nfunc sliceEqual(a, b []any) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i := range a {\n\t\tif a[i] != b[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"eJrIW0Fnv0+gHkZCMoYPvZMGxe6Yi32stJ7CSDuRxA0kk531bVn3OrUBjQANre356V2jaraifDoqzbG4WMLnrg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"pager","path":"gno.land/p/nt/bptree/pager/v0","files":[{"name":"example_test.gno","body":"package pager\n\nimport (\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)\n\nfunc ExamplePager() {\n\t// Create a new tree and populate it with some key-value pairs.\n\tvar id seqid.ID\n\ttree := bptree.NewBPTree32()\n\tfor i := 0; i \u003c 42; i++ {\n\t\ttree.Set(id.Next().String(), i)\n\t}\n\n\t// Create a new pager.\n\tpager1 := NewPager(tree, 7, false)\n\n\tfor pn := -1; pn \u003c 8; pn++ {\n\t\tpage := pager1.GetPage(pn)\n\n\t\tprintln(ufmt.Sprintf(\"## Page %d of %d\", page.PageNumber, page.TotalPages))\n\t\tfor idx, item := range page.Items {\n\t\t\tprintln(ufmt.Sprintf(\"- idx=%d key=%s value=%d\", idx, item.Key, item.Value))\n\t\t}\n\t\tprintln(page.Picker(\"/\"))\n\t\tprintln()\n\t}\n\n\t// Output:\n\t// ## Page 0 of 6\n\t// _0_ | [1](?page=1) | [2](?page=2) | … | [6](?page=6)\n\t//\n\t// ## Page 0 of 6\n\t// _0_ | [1](?page=1) | [2](?page=2) | … | [6](?page=6)\n\t//\n\t// ## Page 1 of 6\n\t// - idx=0 key=0000001 value=0\n\t// - idx=1 key=0000002 value=1\n\t// - idx=2 key=0000003 value=2\n\t// - idx=3 key=0000004 value=3\n\t// - idx=4 key=0000005 value=4\n\t// - idx=5 key=0000006 value=5\n\t// - idx=6 key=0000007 value=6\n\t// **1** | [2](?page=2) | [3](?page=3) | … | [6](?page=6)\n\t//\n\t// ## Page 2 of 6\n\t// - idx=0 key=0000008 value=7\n\t// - idx=1 key=0000009 value=8\n\t// - idx=2 key=000000a value=9\n\t// - idx=3 key=000000b value=10\n\t// - idx=4 key=000000c value=11\n\t// - idx=5 key=000000d value=12\n\t// - idx=6 key=000000e value=13\n\t// [1](?page=1) | **2** | [3](?page=3) | [4](?page=4) | … | [6](?page=6)\n\t//\n\t// ## Page 3 of 6\n\t// - idx=0 key=000000f value=14\n\t// - idx=1 key=000000g value=15\n\t// - idx=2 key=000000h value=16\n\t// - idx=3 key=000000j value=17\n\t// - idx=4 key=000000k value=18\n\t// - idx=5 key=000000m value=19\n\t// - idx=6 key=000000n value=20\n\t// [1](?page=1) | [2](?page=2) | **3** | [4](?page=4) | [5](?page=5) | [6](?page=6)\n\t//\n\t// ## Page 4 of 6\n\t// - idx=0 key=000000p value=21\n\t// - idx=1 key=000000q value=22\n\t// - idx=2 key=000000r value=23\n\t// - idx=3 key=000000s value=24\n\t// - idx=4 key=000000t value=25\n\t// - idx=5 key=000000v value=26\n\t// - idx=6 key=000000w value=27\n\t// [1](?page=1) | [2](?page=2) | [3](?page=3) | **4** | [5](?page=5) | [6](?page=6)\n\t//\n\t// ## Page 5 of 6\n\t// - idx=0 key=000000x value=28\n\t// - idx=1 key=000000y value=29\n\t// - idx=2 key=000000z value=30\n\t// - idx=3 key=0000010 value=31\n\t// - idx=4 key=0000011 value=32\n\t// - idx=5 key=0000012 value=33\n\t// - idx=6 key=0000013 value=34\n\t// [1](?page=1) | … | [3](?page=3) | [4](?page=4) | **5** | [6](?page=6)\n\t//\n\t// ## Page 6 of 6\n\t// - idx=0 key=0000014 value=35\n\t// - idx=1 key=0000015 value=36\n\t// - idx=2 key=0000016 value=37\n\t// - idx=3 key=0000017 value=38\n\t// - idx=4 key=0000018 value=39\n\t// - idx=5 key=0000019 value=40\n\t// - idx=6 key=000001a value=41\n\t// [1](?page=1) | … | [4](?page=4) | [5](?page=5) | **6**\n\t//\n\t// ## Page 7 of 6\n\t// [1](?page=1) | … | [5](?page=5) | [6](?page=6) | _7_\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/bptree/pager/v0\"\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/rotree/v0\"\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"},{"name":"pager_test.gno","body":"package pager\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestPager_GetPage(t *testing.T) {\n\t// Create a new B+ tree and populate it with some key-value pairs.\n\ttree := bptree.NewBPTree32()\n\ttree.Set(\"a\", 1)\n\ttree.Set(\"b\", 2)\n\ttree.Set(\"c\", 3)\n\ttree.Set(\"d\", 4)\n\ttree.Set(\"e\", 5)\n\n\tt.Run(\"normal ordering\", func(t *testing.T) {\n\t\t// Create a new pager.\n\t\tpager := NewPager(tree, 10, false)\n\n\t\t// Define test cases.\n\t\ttests := []struct {\n\t\t\tpageNumber int\n\t\t\tpageSize   int\n\t\t\texpected   []Item\n\t\t}{\n\t\t\t{1, 2, []Item{{Key: \"a\", Value: 1}, {Key: \"b\", Value: 2}}},\n\t\t\t{2, 2, []Item{{Key: \"c\", Value: 3}, {Key: \"d\", Value: 4}}},\n\t\t\t{3, 2, []Item{{Key: \"e\", Value: 5}}},\n\t\t\t{1, 3, []Item{{Key: \"a\", Value: 1}, {Key: \"b\", Value: 2}, {Key: \"c\", Value: 3}}},\n\t\t\t{2, 3, []Item{{Key: \"d\", Value: 4}, {Key: \"e\", Value: 5}}},\n\t\t\t{1, 5, []Item{{Key: \"a\", Value: 1}, {Key: \"b\", Value: 2}, {Key: \"c\", Value: 3}, {Key: \"d\", Value: 4}, {Key: \"e\", Value: 5}}},\n\t\t\t{2, 5, []Item{}},\n\t\t}\n\n\t\tfor _, tt := range tests {\n\t\t\tpage := pager.GetPageWithSize(tt.pageNumber, tt.pageSize)\n\n\t\t\tuassert.Equal(t, len(tt.expected), len(page.Items))\n\n\t\t\tfor i, item := range page.Items {\n\t\t\t\tuassert.Equal(t, tt.expected[i].Key, item.Key)\n\t\t\t\tuassert.Equal(t, tt.expected[i].Value, item.Value)\n\t\t\t}\n\t\t}\n\t})\n\n\tt.Run(\"reversed ordering\", func(t *testing.T) {\n\t\t// Create a new pager.\n\t\tpager := NewPager(tree, 10, true)\n\n\t\t// Define test cases.\n\t\ttests := []struct {\n\t\t\tpageNumber int\n\t\t\tpageSize   int\n\t\t\texpected   []Item\n\t\t}{\n\t\t\t{1, 2, []Item{{Key: \"e\", Value: 5}, {Key: \"d\", Value: 4}}},\n\t\t\t{2, 2, []Item{{Key: \"c\", Value: 3}, {Key: \"b\", Value: 2}}},\n\t\t\t{3, 2, []Item{{Key: \"a\", Value: 1}}},\n\t\t\t{1, 3, []Item{{Key: \"e\", Value: 5}, {Key: \"d\", Value: 4}, {Key: \"c\", Value: 3}}},\n\t\t\t{2, 3, []Item{{Key: \"b\", Value: 2}, {Key: \"a\", Value: 1}}},\n\t\t\t{1, 5, []Item{{Key: \"e\", Value: 5}, {Key: \"d\", Value: 4}, {Key: \"c\", Value: 3}, {Key: \"b\", Value: 2}, {Key: \"a\", Value: 1}}},\n\t\t\t{2, 5, []Item{}},\n\t\t}\n\n\t\tfor _, tt := range tests {\n\t\t\tpage := pager.GetPageWithSize(tt.pageNumber, tt.pageSize)\n\n\t\t\tuassert.Equal(t, len(tt.expected), len(page.Items))\n\n\t\t\tfor i, item := range page.Items {\n\t\t\t\tuassert.Equal(t, tt.expected[i].Key, item.Key)\n\t\t\t\tuassert.Equal(t, tt.expected[i].Value, item.Value)\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc TestPager_GetPageByPath(t *testing.T) {\n\t// Create a new B+ tree and populate it with some key-value pairs.\n\ttree := bptree.NewBPTree32()\n\tfor i := 0; i \u003c 50; i++ {\n\t\ttree.Set(ufmt.Sprintf(\"key%d\", i), i)\n\t}\n\n\t// Create a new pager.\n\tpager := NewPager(tree, 10, false)\n\n\t// Define test cases.\n\ttests := []struct {\n\t\trawURL       string\n\t\texpectedPage int\n\t\texpectedSize int\n\t}{\n\t\t{\"/r/foo:bar/baz?size=10\u0026page=1\", 1, 10},\n\t\t{\"/r/foo:bar/baz?size=10\u0026page=2\", 2, 10},\n\t\t{\"/r/foo:bar/baz?page=3\", 3, pager.DefaultPageSize},\n\t\t{\"/r/foo:bar/baz?size=20\", 1, 20},\n\t\t{\"/r/foo:bar/baz\", 1, pager.DefaultPageSize},\n\t}\n\n\tfor _, tt := range tests {\n\t\tpage, err := pager.GetPageByPath(tt.rawURL)\n\t\turequire.NoError(t, err, ufmt.Sprintf(\"GetPageByPath(%s) returned error: %v\", tt.rawURL, err))\n\n\t\tuassert.Equal(t, tt.expectedPage, page.PageNumber)\n\t\tuassert.Equal(t, tt.expectedSize, page.PageSize)\n\t}\n}\n\nfunc TestPage_Picker(t *testing.T) {\n\t// Create a new B+ tree and populate it with some key-value pairs.\n\ttree := bptree.NewBPTree32()\n\ttree.Set(\"a\", 1)\n\ttree.Set(\"b\", 2)\n\ttree.Set(\"c\", 3)\n\ttree.Set(\"d\", 4)\n\ttree.Set(\"e\", 5)\n\n\t// Create a new pager.\n\tpager := NewPager(tree, 10, false)\n\n\t// Define test cases.\n\ttests := []struct {\n\t\tpageNumber int\n\t\tpageSize   int\n\t\tpath       string\n\t\texpected   string\n\t}{\n\t\t{1, 2, \"/test\", \"**1** | [2](?page=2) | [3](?page=3)\"},\n\t\t{2, 2, \"/test\", \"[1](?page=1) | **2** | [3](?page=3)\"},\n\t\t{3, 2, \"/test\", \"[1](?page=1) | [2](?page=2) | **3**\"},\n\t\t{1, 2, \"/test?foo=bar\", \"**1** | [2](?page=2\u0026foo=bar) | [3](?page=3\u0026foo=bar)\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tpage := pager.GetPageWithSize(tt.pageNumber, tt.pageSize)\n\n\t\tui := page.Picker(tt.path)\n\t\tuassert.Equal(t, tt.expected, ui)\n\t}\n}\n\nfunc TestPager_UI_WithManyPages(t *testing.T) {\n\t// Create a new B+ tree and populate it with many key-value pairs.\n\ttree := bptree.NewBPTree32()\n\tfor i := 0; i \u003c 100; i++ {\n\t\ttree.Set(ufmt.Sprintf(\"key%d\", i), i)\n\t}\n\n\t// Create a new pager.\n\tpager := NewPager(tree, 10, false)\n\n\t// Define test cases for a large number of pages.\n\ttests := []struct {\n\t\tpageNumber int\n\t\tpageSize   int\n\t\tpath       string\n\t\texpected   string\n\t}{\n\t\t{1, 10, \"/test\", \"**1** | [2](?page=2) | [3](?page=3) | … | [10](?page=10)\"},\n\t\t{2, 10, \"/test\", \"[1](?page=1) | **2** | [3](?page=3) | [4](?page=4) | … | [10](?page=10)\"},\n\t\t{3, 10, \"/test\", \"[1](?page=1) | [2](?page=2) | **3** | [4](?page=4) | [5](?page=5) | … | [10](?page=10)\"},\n\t\t{4, 10, \"/test\", \"[1](?page=1) | [2](?page=2) | [3](?page=3) | **4** | [5](?page=5) | [6](?page=6) | … | [10](?page=10)\"},\n\t\t{5, 10, \"/test\", \"[1](?page=1) | … | [3](?page=3) | [4](?page=4) | **5** | [6](?page=6) | [7](?page=7) | … | [10](?page=10)\"},\n\t\t{6, 10, \"/test\", \"[1](?page=1) | … | [4](?page=4) | [5](?page=5) | **6** | [7](?page=7) | [8](?page=8) | … | [10](?page=10)\"},\n\t\t{7, 10, \"/test\", \"[1](?page=1) | … | [5](?page=5) | [6](?page=6) | **7** | [8](?page=8) | [9](?page=9) | [10](?page=10)\"},\n\t\t{8, 10, \"/test\", \"[1](?page=1) | … | [6](?page=6) | [7](?page=7) | **8** | [9](?page=9) | [10](?page=10)\"},\n\t\t{9, 10, \"/test\", \"[1](?page=1) | … | [7](?page=7) | [8](?page=8) | **9** | [10](?page=10)\"},\n\t\t{10, 10, \"/test\", \"[1](?page=1) | … | [8](?page=8) | [9](?page=9) | **10**\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tpage := pager.GetPageWithSize(tt.pageNumber, tt.pageSize)\n\n\t\tui := page.Picker(tt.path)\n\t\tuassert.Equal(t, tt.expected, ui)\n\t}\n}\n\nfunc TestPager_ParseQuery(t *testing.T) {\n\t// Create a new B+ tree and populate it with some key-value pairs.\n\ttree := bptree.NewBPTree32()\n\ttree.Set(\"a\", 1)\n\ttree.Set(\"b\", 2)\n\ttree.Set(\"c\", 3)\n\ttree.Set(\"d\", 4)\n\ttree.Set(\"e\", 5)\n\n\t// Create a new pager.\n\tpager := NewPager(tree, 10, false)\n\n\t// Define test cases.\n\ttests := []struct {\n\t\trawURL        string\n\t\texpectedPage  int\n\t\texpectedSize  int\n\t\texpectedError bool\n\t}{\n\t\t{\"/r/foo:bar/baz?size=2\u0026page=1\", 1, 2, false},\n\t\t{\"/r/foo:bar/baz?size=3\u0026page=2\", 2, 3, false},\n\t\t{\"/r/foo:bar/baz?size=5\u0026page=3\", 3, 5, false},\n\t\t{\"/r/foo:bar/baz?page=2\", 2, pager.DefaultPageSize, false},\n\t\t{\"/r/foo:bar/baz?size=3\", 1, 3, false},\n\t\t{\"/r/foo:bar/baz\", 1, pager.DefaultPageSize, false},\n\t\t{\"/r/foo:bar/baz?size=0\u0026page=0\", 1, pager.DefaultPageSize, false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tpage, size, err := pager.ParseQuery(tt.rawURL)\n\t\tif tt.expectedError {\n\t\t\tuassert.Error(t, err, ufmt.Sprintf(\"ParseQuery(%s) expected error but got none\", tt.rawURL))\n\t\t} else {\n\t\t\turequire.NoError(t, err, ufmt.Sprintf(\"ParseQuery(%s) returned error: %v\", tt.rawURL, err))\n\t\t\tuassert.Equal(t, tt.expectedPage, page, ufmt.Sprintf(\"ParseQuery(%s) returned page %d, expected %d\", tt.rawURL, page, tt.expectedPage))\n\t\t\tuassert.Equal(t, tt.expectedSize, size, ufmt.Sprintf(\"ParseQuery(%s) returned size %d, expected %d\", tt.rawURL, size, tt.expectedSize))\n\t\t}\n\t}\n}\n\nfunc TestPage_PickerQueryParamPreservation(t *testing.T) {\n\ttree := bptree.NewBPTree32()\n\tfor i := 1; i \u003c= 6; i++ {\n\t\ttree.Set(ufmt.Sprintf(\"key%d\", i), i)\n\t}\n\n\tpager := NewPager(tree, 2, false)\n\n\ttests := []struct {\n\t\tname       string\n\t\tpageNumber int\n\t\tpath       string\n\t\texpected   string\n\t}{\n\t\t{\n\t\t\tname:       \"single query param\",\n\t\t\tpageNumber: 1,\n\t\t\tpath:       \"/test?foo=bar\",\n\t\t\texpected:   \"**1** | [2](?page=2\u0026foo=bar) | [3](?page=3\u0026foo=bar)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"multiple query params\",\n\t\t\tpageNumber: 2,\n\t\t\tpath:       \"/test?foo=bar\u0026baz=qux\",\n\t\t\texpected:   \"[1](?page=1\u0026baz=qux\u0026foo=bar) | **2** | [3](?page=3\u0026baz=qux\u0026foo=bar)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"overwrite existing page param\",\n\t\t\tpageNumber: 1,\n\t\t\tpath:       \"/test?param1=value1\u0026page=999\u0026param2=value2\",\n\t\t\texpected:   \"**1** | [2](?page=2\u0026param1=value1\u0026param2=value2) | [3](?page=3\u0026param1=value1\u0026param2=value2)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"empty query string\",\n\t\t\tpageNumber: 2,\n\t\t\tpath:       \"/test\",\n\t\t\texpected:   \"[1](?page=1) | **2** | [3](?page=3)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"query string with only page param\",\n\t\t\tpageNumber: 2,\n\t\t\tpath:       \"/test?page=2\",\n\t\t\texpected:   \"[1](?page=1) | **2** | [3](?page=3)\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tpage := pager.GetPageWithSize(tt.pageNumber, 2)\n\t\t\tresult := page.Picker(tt.path)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"\\nwant: %s\\ngot:  %s\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"i+O3NWjYtYBRClXBV8noc0rJiFRsQ3kS6H8+RptuTGYkU06NjXyuzkn0QQW8PtMkzg462aSwZr8telWBrMMVkw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g15ge0ae9077eh40erwrn2eq0xw6wupwqthpv34l","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":"bylaws_test.gno","body":"package bylaws\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestIsValidPath(t *testing.T) {\n\tvalid := []string{\n\t\t\"bylaws.md\",\n\t\t\"bylaws/quorum.md\",\n\t\t\"mandates/treasury/spend-policy.txt\",\n\t\t\"a\",\n\t\t\"A-b_c.d/e\",\n\t}\n\tfor _, p := range valid {\n\t\tuassert.True(t, IsValidPath(p), \"expect valid: \"+p)\n\t}\n\n\tinvalid := []string{\n\t\t\"\",\n\t\t\"/bylaws.md\",\n\t\t\"bylaws.md/\",\n\t\t\"bylaws//quorum.md\",\n\t\t\"bylaws/./quorum.md\",\n\t\t\"bylaws/../quorum.md\",\n\t\t\".\",\n\t\t\"..\",\n\t\t\"bylaws quorum.md\", // space\n\t\t\"bylaws:quorum\",    // encoding delimiter\n\t\t\"bylaws;quorum\",\n\t\t\"byläws.md\", // non-ASCII\n\t\t\"a\\nb\",\n\t\tstrings.Repeat(\"a\", MaxPathLen+1),\n\t}\n\tfor _, p := range invalid {\n\t\tuassert.False(t, IsValidPath(p), \"expect invalid: \"+p)\n\t}\n}\n\nfunc TestStoreReads(t *testing.T) {\n\tb := New()\n\n\t// Empty set.\n\tuassert.Equal(t, 0, b.Size())\n\tuassert.False(t, b.Has(\"bylaws/a.md\"))\n\t_, ok := b.Get(\"bylaws/a.md\")\n\tuassert.False(t, ok)\n\tuassert.Equal(t, \"\", b.Hash(\"bylaws/a.md\"))\n\tuassert.Equal(t, 0, len(b.List(\"\")))\n\n\t// Documents enter only through Apply.\n\tmustCreate(t, b, \"bylaws/quorum.md\", \"Quorum is half.\")\n\tmustCreate(t, b, \"bylaws/voting.md\", \"One member one vote.\")\n\tmustCreate(t, b, \"mandates/treasury.md\", \"Spend only by vote.\")\n\n\tuassert.Equal(t, 3, b.Size())\n\ttext, ok := b.Get(\"bylaws/quorum.md\")\n\turequire.True(t, ok, \"expect document\")\n\tuassert.Equal(t, \"Quorum is half.\", text)\n\tuassert.Equal(t, HashText(\"Quorum is half.\"), b.Hash(\"bylaws/quorum.md\"))\n\n\t// List is sorted; a prefix scopes to a folder.\n\tall := b.List(\"\")\n\turequire.Equal(t, 3, len(all))\n\tuassert.Equal(t, \"bylaws/quorum.md\", all[0])\n\tuassert.Equal(t, \"bylaws/voting.md\", all[1])\n\tuassert.Equal(t, \"mandates/treasury.md\", all[2])\n\n\tfolder := b.List(\"bylaws/\")\n\turequire.Equal(t, 2, len(folder))\n\tuassert.Equal(t, \"bylaws/quorum.md\", folder[0])\n\tuassert.Equal(t, \"bylaws/voting.md\", folder[1])\n\n\t// The prefix is raw: without the trailing slash it also matches a\n\t// sibling file sharing the prefix (documented List behavior).\n\tmustCreate(t, b, \"mandates-old.md\", \"Superseded.\")\n\traw := b.List(\"mandates\")\n\turequire.Equal(t, 2, len(raw))\n\tuassert.Equal(t, \"mandates-old.md\", raw[0])\n\tuassert.Equal(t, \"mandates/treasury.md\", raw[1])\n\tuassert.Equal(t, 1, len(b.List(\"mandates/\")))\n\n\t// Iterate stops when fn returns true.\n\tcount := 0\n\tstopped := b.Iterate(\"\", func(path, text string) bool {\n\t\tcount++\n\t\treturn true\n\t})\n\tuassert.True(t, stopped, \"expect stopped iteration\")\n\tuassert.Equal(t, 1, count)\n}\n\nfunc TestHashText(t *testing.T) {\n\t// sha256(\"\") — the fixed empty-text digest; documents are never stored\n\t// empty, so the \"\" base sentinel (absent) can never collide with it.\n\tuassert.Equal(t,\n\t\t\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\n\t\tHashText(\"\"))\n\tuassert.NotEqual(t, HashText(\"a\"), HashText(\"b\"))\n}\n\n// mustCreate applies a create patch for a new document.\nfunc mustCreate(t *testing.T, b *Bylaws, path, text string) {\n\tt.Helper()\n\n\tp, err := b.Diff(path, text)\n\turequire.NoError(t, err)\n\turequire.NoError(t, b.Apply(p))\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/v0\"\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"},{"name":"patch_test.gno","body":"package bylaws\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestDiffApplyLifecycle(t *testing.T) {\n\tb := New()\n\n\t// Create: diff against an absent document is a create patch.\n\tcreate, err := b.Diff(\"bylaws/quorum.md\", \"Quorum is half the council.\")\n\turequire.NoError(t, err)\n\tuassert.True(t, create.IsCreate(), \"expect create patch\")\n\tuassert.False(t, create.IsNoop())\n\turequire.NoError(t, b.Apply(create))\n\ttext, _ := b.Get(\"bylaws/quorum.md\")\n\tuassert.Equal(t, \"Quorum is half the council.\", text)\n\n\t// Amend: a small edit patches to the new text and re-pins the hash.\n\tamend, err := b.Diff(\"bylaws/quorum.md\", \"Quorum is two thirds of the council.\")\n\turequire.NoError(t, err)\n\tuassert.False(t, amend.IsCreate())\n\tuassert.Equal(t, HashText(\"Quorum is half the council.\"), amend.Base)\n\turequire.NoError(t, b.Apply(amend))\n\ttext, _ = b.Get(\"bylaws/quorum.md\")\n\tuassert.Equal(t, \"Quorum is two thirds of the council.\", text)\n\n\t// Remove: empty proposed text deletes the document.\n\tremove, err := b.Diff(\"bylaws/quorum.md\", \"\")\n\turequire.NoError(t, err)\n\tuassert.True(t, remove.IsRemove(), \"expect remove patch\")\n\turequire.NoError(t, b.Apply(remove))\n\tuassert.False(t, b.Has(\"bylaws/quorum.md\"))\n\tuassert.Equal(t, 0, b.Size())\n}\n\nfunc TestApplyStale(t *testing.T) {\n\tb := New()\n\tmustCreate(t, b, \"a.md\", \"one\")\n\n\t// Both patches diff against \"one\"; the first to apply wins.\n\tp1, err := b.Diff(\"a.md\", \"two\")\n\turequire.NoError(t, err)\n\tp2, err := b.Diff(\"a.md\", \"three\")\n\turequire.NoError(t, err)\n\n\turequire.NoError(t, b.Apply(p1))\n\tuassert.ErrorIs(t, b.Apply(p2), ErrStalePatch)\n\ttext, _ := b.Get(\"a.md\")\n\tuassert.Equal(t, \"two\", text) // loser did not clobber\n\n\t// A create patch against a now-existing document is stale too.\n\tcreate, err := DiffTexts(\"a.md\", \"\", \"fresh\", false)\n\turequire.NoError(t, err)\n\tuassert.ErrorIs(t, b.Apply(create), ErrStalePatch)\n\n\t// A patch pinning sha256(\"\") is NOT a create: documents are never\n\t// stored empty, so it can never match anything.\n\tnotCreate := Patch{Path: \"b.md\", Base: HashText(\"\"), Ops: []Op{{Type: OpInsert, Text: \"x\"}}}\n\tuassert.ErrorIs(t, b.Apply(notCreate), ErrStalePatch)\n}\n\nfunc TestApplyRejectsBadScripts(t *testing.T) {\n\tb := New()\n\tmustCreate(t, b, \"a.md\", \"abcdef\")\n\tbase := b.Hash(\"a.md\")\n\n\tcases := []struct {\n\t\tname string\n\t\tops  []Op\n\t}{\n\t\t{\"keep overruns the base\", []Op{{Type: OpKeep, N: 7}}},\n\t\t{\"delete overruns the base\", []Op{{Type: OpDelete, N: 7}}},\n\t\t{\"script underconsumes the base\", []Op{{Type: OpKeep, N: 3}}},\n\t\t{\"zero keep\", []Op{{Type: OpKeep, N: 0}, {Type: OpKeep, N: 6}}},\n\t\t{\"negative delete\", []Op{{Type: OpDelete, N: -1}, {Type: OpKeep, N: 6}}},\n\t\t{\"empty insert\", []Op{{Type: OpKeep, N: 6}, {Type: OpInsert}}},\n\t\t{\"unknown op\", []Op{{Type: OpType('X'), N: 6}}},\n\t}\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := b.Apply(Patch{Path: \"a.md\", Base: base, Ops: tc.ops})\n\t\t\tuassert.ErrorIs(t, err, ErrInvalidPatch)\n\t\t\ttext, _ := b.Get(\"a.md\")\n\t\t\tuassert.Equal(t, \"abcdef\", text) // unchanged on error\n\t\t})\n\t}\n\n\t// Path and size guards.\n\tuassert.ErrorIs(t, b.Apply(Patch{Path: \"bad path\"}), ErrInvalidPath)\n\tgrow := Patch{Path: \"a.md\", Base: base, Ops: []Op{\n\t\t{Type: OpDelete, N: 6},\n\t\t{Type: OpInsert, Text: strings.Repeat(\"x\", MaxDocLen+1)},\n\t}}\n\tuassert.ErrorIs(t, b.Apply(grow), ErrDocTooLarge)\n\n\t_, err := DiffTexts(\"a.md\", \"\", strings.Repeat(\"x\", MaxDocLen+1), false)\n\tuassert.ErrorIs(t, err, ErrDocTooLarge)\n}\n\nfunc TestApplyUnicode(t *testing.T) {\n\tb := New()\n\tmustCreate(t, b, \"a.md\", \"héllo wörld ✓\")\n\n\t// Ops count runes, not bytes: a diff over multi-byte text replays\n\t// exactly.\n\tp, err := b.Diff(\"a.md\", \"héllo brave wörld ✓✓\")\n\turequire.NoError(t, err)\n\turequire.NoError(t, b.Apply(p))\n\ttext, _ := b.Get(\"a.md\")\n\tuassert.Equal(t, \"héllo brave wörld ✓✓\", text)\n}\n\nfunc TestEncodeDecodeRoundTrip(t *testing.T) {\n\tb := New()\n\tmustCreate(t, b, \"bylaws/quorum.md\", \"Quorum is half the council.\\nVotes are public.\")\n\n\tpatches := []Patch{}\n\tp1, err := b.Diff(\"bylaws/quorum.md\", \"Quorum is two thirds.\\nVotes are public.\")\n\turequire.NoError(t, err)\n\tpatches = append(patches, p1)\n\tp2, err := DiffTexts(\"new/doc.md\", \"\", \"Created; with punct: [x] (y) \\\"z\\\"\\nand a second line.\", false)\n\turequire.NoError(t, err)\n\tpatches = append(patches, p2)\n\tp3, err := b.Diff(\"bylaws/quorum.md\", \"\") // remove\n\turequire.NoError(t, err)\n\tpatches = append(patches, p3)\n\n\tfor _, p := range patches {\n\t\tenc := p.Encode()\n\t\tdec, err := DecodePatch(enc)\n\t\turequire.NoError(t, err)\n\t\tuassert.Equal(t, p.Path, dec.Path)\n\t\tuassert.Equal(t, p.Base, dec.Base)\n\t\turequire.Equal(t, len(p.Ops), len(dec.Ops))\n\t\tfor i := range p.Ops {\n\t\t\tuassert.True(t, p.Ops[i].Type == dec.Ops[i].Type, \"op type\")\n\t\t\tuassert.Equal(t, p.Ops[i].N, dec.Ops[i].N)\n\t\t\tuassert.Equal(t, p.Ops[i].Text, dec.Ops[i].Text)\n\t\t}\n\t\t// Deterministic: encoding is stable.\n\t\tuassert.Equal(t, enc, dec.Encode())\n\t}\n\n\t// The decoded patch applies like the original.\n\tdec, err := DecodePatch(p1.Encode())\n\turequire.NoError(t, err)\n\turequire.NoError(t, b.Apply(dec))\n\ttext, _ := b.Get(\"bylaws/quorum.md\")\n\tuassert.Equal(t, \"Quorum is two thirds.\\nVotes are public.\", text)\n}\n\nfunc TestDecodePatchRejectsMalformed(t *testing.T) {\n\tvalid, err := DiffTexts(\"a.md\", \"\", \"hello\", false)\n\turequire.NoError(t, err)\n\tenc := valid.Encode()\n\tuassert.Equal(t, \"v0:a.md::I5:hello;\", enc) // pin the wire format\n\n\tbad := []string{\n\t\t\"\",\n\t\t\"v1:a.md::I5:hello;\",           // unknown version\n\t\t\"v0:bad path::I5:hello;\",       // invalid path\n\t\t\"v0:a.md:zz:I5:hello;\",         // malformed base\n\t\t\"v0:a.md::X5:hello;\",           // unknown op\n\t\t\"v0:a.md::I5:hell\",             // truncated literal\n\t\t\"v0:a.md::I5:helloX\",           // missing op terminator\n\t\t\"v0:a.md::K0;\",                 // zero count\n\t\t\"v0:a.md::K-1;\",                // negative count\n\t\t\"v0:a.md::Kx;\",                 // non-numeric count\n\t\t\"v0:a.md::K05;\",                // non-canonical count (leading zero)\n\t\t\"v0:a.md::K+5;\",                // non-canonical count (sign)\n\t\t\"v0:a.md::I+5:hello;\",          // non-canonical insert length\n\t\t\"v0:a.md::K1\",                  // missing terminator\n\t\t\"v0:a.md::I0:;\",                // empty insert\n\t\t\"v0:a.md\",                      // missing base separator\n\t\t\"v0:a.md::K99999999999999999;\", // over MaxDocLen\n\t\t\"v0:a.md::I1:\\xc3;\",            // insert is not valid UTF-8\n\t}\n\tfor _, s := range bad {\n\t\t_, err := DecodePatch(s)\n\t\tuassert.Error(t, err, \"expect decode error: \"+s)\n\t}\n}\n\nfunc TestDecodePatchMaxOps(t *testing.T) {\n\t// Exactly MaxOps ops decode; one more is rejected. (Shape-only: the\n\t// script need not fit any document to decode.)\n\tpayload := \"v0:a.md::\" + strings.Repeat(\"K1;\", MaxOps)\n\t_, err := DecodePatch(payload)\n\tuassert.NoError(t, err)\n\n\t_, err = DecodePatch(payload + \"K1;\")\n\tuassert.ErrorIs(t, err, ErrInvalidPatch)\n}\n\nfunc TestEncodeDecodeMultibyteInsert(t *testing.T) {\n\t// The insert length prefix is a BYTE length: pin the exact wire form\n\t// for a multi-byte literal so a rune-length regression cannot pass.\n\tp, err := DiffTexts(\"a.md\", \"\", \"héllo ✓ wörld\", false)\n\turequire.NoError(t, err)\n\tenc := p.Encode()\n\tuassert.Equal(t, \"v0:a.md::I17:héllo ✓ wörld;\", enc)\n\n\tdec, err := DecodePatch(enc)\n\turequire.NoError(t, err)\n\tb := New()\n\turequire.NoError(t, b.Apply(dec))\n\ttext, _ := b.Get(\"a.md\")\n\tuassert.Equal(t, \"héllo ✓ wörld\", text)\n}\n\nfunc TestDiffTextsRejectsInvalidUTF8(t *testing.T) {\n\t_, err := DiffTexts(\"a.md\", \"\", \"bad \\xc3 byte\", false)\n\tuassert.ErrorIs(t, err, ErrInvalidText)\n\n\t_, err = DiffTexts(\"a.md\", \"bad \\xc3 base\", \"fine\", true)\n\tuassert.ErrorIs(t, err, ErrInvalidText)\n}\n\nfunc TestFormat(t *testing.T) {\n\tb := New()\n\tmustCreate(t, b, \"a.md\", \"keep DELETE keep\")\n\n\tp, err := b.Diff(\"a.md\", \"keep INSERT keep\")\n\turequire.NoError(t, err)\n\tout, err := p.Format(\"keep DELETE keep\")\n\turequire.NoError(t, err)\n\tuassert.True(t, strings.Contains(out, \"unchanged\"), \"expect kept marker\")\n\n\t// The formatted change shows what leaves and what enters.\n\tuassert.True(t, strings.Contains(out, \"-\"), \"expect deletion marker\")\n\tuassert.True(t, strings.Contains(out, \"+\"), \"expect insertion marker\")\n\n\t// Format fails like replay on a script that does not fit the base by\n\t// rune count. (Fit is positional — content verification is the base\n\t// hash's job, checked by Apply, not Format.)\n\t_, err = p.Format(\"short\")\n\tuassert.ErrorIs(t, err, ErrInvalidPatch)\n}\n\nfunc TestDiffPrefixSuffixTrim(t *testing.T) {\n\tb := New()\n\tmustCreate(t, b, \"a.md\", \"The quick brown fox jumps over the lazy dog.\")\n\n\t// A small middle edit yields Keep(prefix), the middle change, and\n\t// Keep(suffix): the payload carries only the changed region.\n\tp, err := b.Diff(\"a.md\", \"The quick red fox jumps over the lazy dog.\")\n\turequire.NoError(t, err)\n\turequire.True(t, len(p.Ops) \u003e= 3, \"expect trimmed script\")\n\tuassert.True(t, p.Ops[0].Type == OpKeep, \"expect leading keep\")\n\tuassert.True(t, p.Ops[len(p.Ops)-1].Type == OpKeep, \"expect trailing keep\")\n\turequire.NoError(t, b.Apply(p))\n\ttext, _ := b.Get(\"a.md\")\n\tuassert.Equal(t, \"The quick red fox jumps over the lazy dog.\", text)\n}\n\nfunc TestDiffLargeRewriteFallsBackToReplace(t *testing.T) {\n\t// Two texts with a shared prefix/suffix but a differing middle beyond\n\t// the Myers budget: Diff must fall back to a whole-middle replace\n\t// (bounded ops, no quadratic diff) that still replays exactly.\n\tbase := \"HEAD\\n\" + strings.Repeat(\"ab\", 600) + \"\\nTAIL\"\n\tproposed := \"HEAD\\n\" + strings.Repeat(\"cd\", 600) + \"\\nTAIL\"\n\n\tp, err := DiffTexts(\"big.md\", base, proposed, true)\n\turequire.NoError(t, err)\n\turequire.True(t, len(p.Ops) \u003c= 4, \"expect fallback replace, got ops count\")\n\n\tb := New()\n\tcreate, err := b.Diff(\"big.md\", base)\n\turequire.NoError(t, err)\n\turequire.NoError(t, b.Apply(create))\n\turequire.NoError(t, b.Apply(p))\n\ttext, _ := b.Get(\"big.md\")\n\tuassert.Equal(t, proposed, text)\n}\n\nfunc TestFormatMarkerSpoof(t *testing.T) {\n\t// A multi-line insert literal gets every line marker-prefixed, so an\n\t// embedded \"- fake\" line cannot masquerade as a deletion marker.\n\tp, err := DiffTexts(\"a.md\", \"\", \"x\\n- fake deletion\", false)\n\turequire.NoError(t, err)\n\tout, err := p.Format(\"\")\n\turequire.NoError(t, err)\n\tuassert.True(t, strings.Contains(out, \"+ x\\n+ - fake deletion\\n\"), \"expect per-line insert markers\")\n\tuassert.False(t, strings.Contains(out, \"\\n- fake deletion\"), \"expect no bare deletion-marker line\")\n}\n\nfunc TestNoopPatch(t *testing.T) {\n\tb := New()\n\tmustCreate(t, b, \"a.md\", \"same\")\n\n\tp, err := b.Diff(\"a.md\", \"same\")\n\turequire.NoError(t, err)\n\tuassert.True(t, p.IsNoop(), \"expect noop patch\")\n\turequire.NoError(t, b.Apply(p)) // harmless\n\ttext, _ := b.Get(\"a.md\")\n\tuassert.Equal(t, \"same\", text)\n\n\t// Creating an empty document is a noop create: nothing is stored.\n\tpc, err := DiffTexts(\"empty.md\", \"\", \"\", false)\n\turequire.NoError(t, err)\n\tuassert.True(t, pc.IsNoop(), \"expect noop create\")\n\turequire.NoError(t, b.Apply(pc))\n\tuassert.False(t, b.Has(\"empty.md\"))\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"NdzysgggpXpHcMkwrmz8VdIb6KXk2jQTkDSa//T99XIhzQQJuUmsm7Fcdv+VbruuEzd746IvssYOzR79LHK3lg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"6jXWFqSLUbH7H92IexD37fwLjzNDN6JbhWtuWW0gr8ZkifHdEy1obkKkL8E9VmXVKKo1+dNBLGKwtSI0jn7vZQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g15ge0ae9077eh40erwrn2eq0xw6wupwqthpv34l","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/list/v0\"\n\t\"gno.land/p/nt/bptree/v0\"\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":"commondao_test.gno","body":"package commondao_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n\n\t\"gno.land/p/nt/commondao/v0\"\n)\n\nconst (\n\tmemberA address = \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"\n\tmemberB address = \"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\"\n\tmemberC address = \"g1w4ek2u3jta047h6lta047h6lta047h6l9huexc\"\n\tmemberD address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tmemberE address = \"g1yx35a8f4lhhhrarljrgr37u55gkqdrl0949ycf\"\n\tmemberF address = \"g1cyh9qm4y43w38gfk77qxczrhqj2asdr8l7uk54\"\n\tmemberG address = \"g1x95kxqpwvqjf98qpkvsay7tppkh6zs9fr7wwda\"\n\tmemberH address = \"g12m7ryva2vsw3twqdtuc9awufwzwtk5al4v4ksd\"\n\tmemberI address = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n)\n\n// probeKind is a pass-through proposal kind for tests: args is the\n// proposal definition itself.\ntype probeKind struct{}\n\nfunc (probeKind) Name() string { return \"probe\" }\nfunc (probeKind) New(_ commondao.ReadonlyCommonDAO, args any) (commondao.ProposalDefinition, error) {\n\td, _ := args.(commondao.ProposalDefinition)\n\treturn d, nil\n}\n\n// namedKind is a probe kind with a configurable name.\ntype namedKind string\n\nfunc (k namedKind) Name() string { return string(k) }\nfunc (namedKind) New(_ commondao.ReadonlyCommonDAO, _ any) (commondao.ProposalDefinition, error) {\n\treturn propDef{period: time.Hour}, nil\n}\n\n// failingKind is a probe kind whose factory always fails.\ntype failingKind struct{ err error }\n\nfunc (failingKind) Name() string { return \"failing\" }\nfunc (k failingKind) New(_ commondao.ReadonlyCommonDAO, _ any) (commondao.ProposalDefinition, error) {\n\treturn nil, k.err\n}\n\n// hostKind is a probe kind that records the ID of the readonly host view\n// passed to its factory. New receives only a ReadonlyCommonDAO (no mutable\n// handle), so the kind can identify the host but cannot mutate it.\ntype hostKind struct{ hostID *uint64 }\n\nfunc (hostKind) Name() string { return \"host\" }\nfunc (k hostKind) New(dao commondao.ReadonlyCommonDAO, _ any) (commondao.ProposalDefinition, error) {\n\t*k.hostID = dao.ID()\n\treturn propDef{period: time.Hour}, nil\n}\n\n// reProposeKind.New re-enters Propose on the captured DAO. Only a trusted\n// kind holding the handle can attempt this (external kinds get a readonly\n// view); the proposing latch must panic before the nested Propose runs.\ntype reProposeKind struct{ dao *commondao.CommonDAO }\n\nfunc (reProposeKind) Name() string { return \"re-propose\" }\nfunc (k reProposeKind) New(_ commondao.ReadonlyCommonDAO, _ any) (commondao.ProposalDefinition, error) {\n\t// \"probe\" is registered, so this reaches the proposing latch (not the\n\t// kind-lookup) and panics.\n\tk.dao.Propose(memberA, \"probe\", defaultPropDef{period: time.Hour})\n\treturn defaultPropDef{period: time.Hour}, nil\n}\n\n// propDef is a base proposal definition decided by the default rules.\ntype propDef struct {\n\ttitle  string\n\tbody   string\n\tperiod time.Duration\n}\n\nfunc (d propDef) Title() string               { return d.title }\nfunc (d propDef) Body() string                { return d.body }\nfunc (d propDef) VotingPeriod() time.Duration { return d.period }\nfunc (propDef) Threshold() commondao.Threshold {\n\treturn commondao.ThresholdSupermajority\n}\n\n// mustPropose creates a proposal through the probe kind or fails the test.\nfunc mustPropose(t *testing.T, dao *commondao.CommonDAO, creator address, d commondao.ProposalDefinition) *commondao.Proposal {\n\tt.Helper()\n\tp, err := dao.Propose(creator, \"probe\", d)\n\turequire.NoError(t, err, \"propose\")\n\treturn p\n}\n\n// defaultPropDef is decided by the constitution's default rules.\ntype defaultPropDef struct {\n\tperiod    time.Duration\n\tthreshold commondao.Threshold\n\texecuted  *bool\n\texecErr   error\n}\n\nfunc (defaultPropDef) Title() string                 { return \"Default\" }\nfunc (defaultPropDef) Body() string                  { return \"\" }\nfunc (d defaultPropDef) VotingPeriod() time.Duration { return d.period }\nfunc (d defaultPropDef) Threshold() commondao.Threshold {\n\treturn d.threshold\n}\n\nfunc (d defaultPropDef) Executor() commondao.ExecFunc {\n\treturn func(_ int, sub realm) error {\n\t\tif d.executed != nil {\n\t\t\t*d.executed = true\n\t\t}\n\t\treturn d.execErr\n\t}\n}\n\n// exemptPropDef is exempt from the active proposals cap.\ntype exemptPropDef struct{ defaultPropDef }\n\nfunc (exemptPropDef) CapExempt() {}\n\n// execFnPropDef executes an arbitrary function on approval.\ntype execFnPropDef struct {\n\tpropDef\n\tfn commondao.ExecFunc\n}\n\nfunc (d execFnPropDef) Executor() commondao.ExecFunc { return d.fn }\n\n// validatingExecPropDef is a default rule definition with state validation.\ntype validatingExecPropDef struct {\n\tdefaultPropDef\n\terr *error\n}\n\nfunc (d validatingExecPropDef) Validate() error { return *d.err }\n\nfunc TestNew(t *testing.T) {\n\tdao := commondao.New()\n\tuassert.Equal(t, uint64(0), dao.ID())\n\tuassert.Equal(t, \"\", dao.Name())\n\tuassert.Equal(t, 0, dao.Council().Size())\n\tuassert.Equal(t, 0, dao.ActiveProposalsSize())\n\tuassert.Equal(t, 0, dao.FinishedProposalsSize())\n\tuassert.Equal(t, 0, len(dao.KindNames()))\n\tuassert.False(t, dao.IsDeleted())\n\turequire.True(t, dao.Parent() == nil, \"expect no parent\")\n\n\tparent := commondao.New(commondao.WithName(\"root\"))\n\tdao = commondao.New(\n\t\tcommondao.WithID(7),\n\t\tcommondao.WithName(\"child\"),\n\t\tcommondao.WithDescription(\"test\"),\n\t\tcommondao.WithParent(parent),\n\t\tcommondao.WithCouncilMember(memberA),\n\t\tcommondao.WithCouncilMember(memberB),\n\t)\n\tuassert.Equal(t, uint64(7), dao.ID())\n\tuassert.Equal(t, \"child\", dao.Name())\n\tuassert.Equal(t, \"test\", dao.Description())\n\tuassert.Equal(t, 2, dao.Council().Size())\n\tuassert.True(t, dao.Council().Has(memberA))\n\tuassert.Equal(t, \"root\", dao.Parent().Name())\n}\n\nfunc TestUpdateCouncil(t *testing.T) {\n\tdao := commondao.New(commondao.WithCouncilMember(memberA), commondao.WithProposalKind(probeKind{}))\n\n\t// Idempotent set semantics\n\turequire.NoError(t, dao.UpdateCouncil([]address{memberB, memberB}, nil))\n\tuassert.Equal(t, 2, dao.Council().Size())\n\turequire.NoError(t, dao.UpdateCouncil(nil, []address{memberC})) // absent remove\n\tuassert.Equal(t, 2, dao.Council().Size())\n\n\t// Overlapping add/remove is rejected\n\terr := dao.UpdateCouncil([]address{memberC}, []address{memberC})\n\tuassert.ErrorIs(t, err, commondao.ErrCouncilUpdateOverlap)\n\n\t// Emptying a non-empty council is rejected\n\terr = dao.UpdateCouncil(nil, []address{memberA, memberB})\n\tuassert.ErrorIs(t, err, commondao.ErrEmptyCouncil)\n\tuassert.Equal(t, 2, dao.Council().Size())\n\n\t// Removing all but one is allowed\n\turequire.NoError(t, dao.UpdateCouncil(nil, []address{memberA}))\n\tuassert.Equal(t, 1, dao.Council().Size())\n\tuassert.True(t, dao.Council().Has(memberB))\n\n\t// Full council replacement in a single update is legal\n\turequire.NoError(t, dao.UpdateCouncil([]address{memberC, memberD}, []address{memberB}))\n\tuassert.Equal(t, 2, dao.Council().Size())\n\tuassert.False(t, dao.Council().Has(memberB))\n\tuassert.True(t, dao.Council().Has(memberC))\n\tuassert.True(t, dao.Council().Has(memberD))\n\n\t// More removals than members is fine as long as a member survives\n\t// (the emptiness check is by identity, not by count)\n\turequire.NoError(t, dao.UpdateCouncil(nil, []address{memberE, memberF, memberG}))\n\tuassert.Equal(t, 2, dao.Council().Size())\n\n\t// An empty council can be seeded\n\tempty := commondao.New()\n\turequire.NoError(t, empty.UpdateCouncil(nil, nil))\n\turequire.NoError(t, empty.UpdateCouncil([]address{memberA}, nil))\n\tuassert.Equal(t, 1, empty.Council().Size())\n}\n\nfunc TestPropose(t *testing.T) {\n\tdao := commondao.New(\n\t\tcommondao.WithCouncilMember(memberA),\n\t\tcommondao.WithProposalKind(probeKind{}),\n\t)\n\n\tp, err := dao.Propose(memberA, \"probe\", propDef{period: time.Hour})\n\turequire.NoError(t, err)\n\tuassert.Equal(t, uint64(1), p.ID())\n\tuassert.Equal(t, 1, dao.ActiveProposalsSize())\n\turequire.True(t, dao.GetProposal(p.ID()) != nil, \"expect proposal to be found\")\n\n\t// The electorate is the council snapshot taken at proposal creation\n\tuassert.Equal(t, 1, p.Electorate().Size())\n\turequire.NoError(t, dao.UpdateCouncil([]address{memberB}, nil))\n\tuassert.Equal(t, 1, p.Electorate().Size())\n\tuassert.False(t, p.Electorate().Has(memberB))\n\n\t// Definitions are required\n\t_, err = dao.Propose(memberA, \"probe\", nil)\n\tuassert.ErrorIs(t, err, commondao.ErrProposalDefinitionRequired)\n\n\t// Deleted DAOs reject proposals\n\tdeleted := commondao.New(\n\t\tcommondao.WithCouncilMember(memberA),\n\t\tcommondao.WithProposalKind(probeKind{}),\n\t)\n\tdeleted.Dissolve(\"test\")\n\t_, err = deleted.Propose(memberA, \"probe\", propDef{})\n\tuassert.ErrorIs(t, err, commondao.ErrDAOIsDeleted)\n}\n\nfunc TestProposeCap(t *testing.T) {\n\tdao := commondao.New(\n\t\tcommondao.WithCouncilMember(memberA),\n\t\tcommondao.WithProposalKind(probeKind{}),\n\t)\n\tdao.SetMaxActiveProposals(2)\n\n\t_, err := dao.Propose(memberA, \"probe\", propDef{period: time.Hour})\n\turequire.NoError(t, err)\n\t_, err = dao.Propose(memberA, \"probe\", propDef{period: time.Hour})\n\turequire.NoError(t, err)\n\n\t// The cap rejects further proposals\n\t_, err = dao.Propose(memberA, \"probe\", propDef{period: time.Hour})\n\tuassert.ErrorIs(t, err, commondao.ErrMaxActiveProposals)\n\n\t// Cap exempt definitions bypass the cap\n\t_, err = dao.Propose(memberA, \"probe\", exemptPropDef{defaultPropDef{period: time.Hour}})\n\turequire.NoError(t, err)\n\n\t// But are bounded to one active exempt proposal per creator\n\t_, err = dao.Propose(memberA, \"probe\", exemptPropDef{defaultPropDef{period: time.Hour}})\n\tuassert.ErrorIs(t, err, commondao.ErrMaxCapExemptProposals)\n\t_, err = dao.Propose(memberB, \"probe\", exemptPropDef{defaultPropDef{period: time.Hour}})\n\turequire.NoError(t, err)\n}\n\nfunc TestProposeCapSlotFrees(t *testing.T) {\n\tdao := commondao.New(\n\t\tcommondao.WithCouncilMember(memberA),\n\t\tcommondao.WithCouncilMember(memberB),\n\t\tcommondao.WithCouncilMember(memberC),\n\t\tcommondao.WithProposalKind(probeKind{}),\n\t)\n\tdao.SetMaxActiveProposals(1)\n\n\tp := mustPropose(t, dao, memberA, defaultPropDef{period: time.Hour})\n\t_, err := dao.Propose(memberA, \"probe\", defaultPropDef{period: time.Hour})\n\turequire.ErrorIs(t, err, commondao.ErrMaxActiveProposals)\n\n\t// An immediate NO majority dismissal frees the slot\n\turequire.NoError(t, dao.Vote(memberA, p.ID(), commondao.ChoiceNo, \"\"))\n\turequire.NoError(t, dao.Vote(memberB, p.ID(), commondao.ChoiceNo, \"\"))\n\turequire.Equal(t, string(commondao.StatusDismissed), string(p.Status()))\n\n\t_, err = dao.Propose(memberA, \"probe\", defaultPropDef{period: time.Hour})\n\turequire.NoError(t, err)\n}\n\nfunc TestDissolve(t *testing.T) {\n\tdao := commondao.New(\n\t\tcommondao.WithCouncilMember(memberA),\n\t\tcommondao.WithProposalKind(probeKind{}),\n\t)\n\n\t// An early passed (but unexecuted) proposal is still in-flight\n\tp := mustPropose(t, dao, memberA, defaultPropDef{period: time.Hour})\n\turequire.NoError(t, dao.Vote(memberA, p.ID(), commondao.ChoiceYes, \"\"))\n\turequire.Equal(t, string(commondao.StatusPassed), string(p.Status()))\n\n\tdao.Dissolve(\"gone\")\n\n\tuassert.True(t, dao.IsDeleted())\n\tuassert.Equal(t, 0, dao.ActiveProposalsSize())\n\tuassert.Equal(t, 1, dao.FinishedProposalsSize())\n\tuassert.Equal(t, string(commondao.StatusDismissed), string(p.Status()))\n\tuassert.Equal(t, \"gone\", p.StatusReason())\n}\n\nfunc TestVote(t *testing.T) {\n\tdao := commondao.New(\n\t\tcommondao.WithCouncilMember(memberA),\n\t\tcommondao.WithCouncilMember(memberB),\n\t\tcommondao.WithCouncilMember(memberC),\n\t\tcommondao.WithProposalKind(probeKind{}),\n\t)\n\tp := mustPropose(t, dao, memberA, propDef{period: time.Hour})\n\n\t// Only electorate members can vote\n\terr := dao.Vote(memberD, p.ID(), commondao.ChoiceYes, \"\")\n\tuassert.ErrorIs(t, err, commondao.ErrNotElectorateMember)\n\n\t// Members added after proposal creation are not in the electorate\n\turequire.NoError(t, dao.UpdateCouncil([]address{memberD}, nil))\n\terr = dao.Vote(memberD, p.ID(), commondao.ChoiceYes, \"\")\n\tuassert.ErrorIs(t, err, commondao.ErrNotElectorateMember)\n\n\t// Members removed after proposal creation remain in the electorate\n\turequire.NoError(t, dao.UpdateCouncil(nil, []address{memberC}))\n\turequire.NoError(t, dao.Vote(memberC, p.ID(), commondao.ChoiceAbstain, \"\"))\n\n\t// Invalid choices are rejected\n\terr = dao.Vote(memberA, p.ID(), commondao.VoteChoice(\"FOO\"), \"\")\n\tuassert.ErrorIs(t, err, commondao.ErrInvalidVoteChoice)\n\n\t// Members can vote and change their vote\n\turequire.NoError(t, dao.Vote(memberA, p.ID(), commondao.ChoiceYes, \"\"))\n\turequire.NoError(t, dao.Vote(memberA, p.ID(), commondao.ChoiceNo, \"\"))\n\tuassert.Equal(t, 0, p.VotingRecord().VoteCount(commondao.ChoiceYes))\n\tuassert.Equal(t, 1, p.VotingRecord().VoteCount(commondao.ChoiceNo))\n\n\t// Unknown proposals and deadlines are rejected\n\terr = dao.Vote(memberA, 404, commondao.ChoiceYes, \"\")\n\tuassert.ErrorIs(t, err, commondao.ErrProposalNotFound)\n\n\texpired := mustPropose(t, dao, memberA, propDef{}) // zero voting period\n\terr = dao.Vote(memberA, expired.ID(), commondao.ChoiceYes, \"\")\n\tuassert.ErrorIs(t, err, commondao.ErrVotingDeadlinePassed)\n\n\t// Deleted DAOs reject votes\n\tdao.Dissolve(\"test\")\n\terr = dao.Vote(memberA, p.ID(), commondao.ChoiceYes, \"\")\n\tuassert.ErrorIs(t, err, commondao.ErrDAOIsDeleted)\n}\n\nfunc TestVoteEarlyTermination(t *testing.T) {\n\tnewDAO := func() *commondao.CommonDAO {\n\t\treturn commondao.New(\n\t\t\tcommondao.WithCouncilMember(memberA),\n\t\t\tcommondao.WithCouncilMember(memberB),\n\t\t\tcommondao.WithCouncilMember(memberC),\n\t\t\tcommondao.WithProposalKind(probeKind{}),\n\t\t)\n\t}\n\n\tt.Run(\"supermajority YES passes immediately\", func(t *testing.T) {\n\t\tdao := newDAO()\n\t\tp := mustPropose(t, dao, memberA, defaultPropDef{period: time.Hour})\n\n\t\turequire.NoError(t, dao.Vote(memberA, p.ID(), commondao.ChoiceYes, \"\"))\n\t\tuassert.Equal(t, string(commondao.StatusActive), string(p.Status()))\n\n\t\turequire.NoError(t, dao.Vote(memberB, p.ID(), commondao.ChoiceYes, \"\"))\n\t\tuassert.Equal(t, string(commondao.StatusPassed), string(p.Status()))\n\n\t\t// Passed proposals stay active (pending execution) but reject votes\n\t\tuassert.Equal(t, 1, dao.ActiveProposalsSize())\n\t\terr := dao.Vote(memberC, p.ID(), commondao.ChoiceNo, \"\")\n\t\tuassert.ErrorIs(t, err, commondao.ErrStatusIsNotActive)\n\t})\n\n\tt.Run(\"NO majority dismisses immediately\", func(t *testing.T) {\n\t\tdao := newDAO()\n\t\tp := mustPropose(t, dao, memberA, defaultPropDef{period: time.Hour})\n\n\t\turequire.NoError(t, dao.Vote(memberA, p.ID(), commondao.ChoiceNo, \"\"))\n\t\turequire.NoError(t, dao.Vote(memberB, p.ID(), commondao.ChoiceNo, \"\"))\n\n\t\tuassert.Equal(t, string(commondao.StatusDismissed), string(p.Status()))\n\t\tuassert.Equal(t, 0, dao.ActiveProposalsSize())\n\t\tuassert.Equal(t, 1, dao.FinishedProposalsSize())\n\t})\n\n\tt.Run(\"abstain flip can trigger a pass\", func(t *testing.T) {\n\t\tdao := commondao.New(\n\t\t\tcommondao.WithCouncilMember(memberA),\n\t\t\tcommondao.WithCouncilMember(memberB),\n\t\t\tcommondao.WithCouncilMember(memberC),\n\t\t\tcommondao.WithCouncilMember(memberD),\n\t\t\tcommondao.WithCouncilMember(memberE),\n\t\t\tcommondao.WithProposalKind(probeKind{}),\n\t\t)\n\t\tp := mustPropose(t, dao, memberA, defaultPropDef{period: time.Hour})\n\n\t\turequire.NoError(t, dao.Vote(memberA, p.ID(), commondao.ChoiceYes, \"\"))\n\t\turequire.NoError(t, dao.Vote(memberB, p.ID(), commondao.ChoiceYes, \"\"))\n\t\turequire.NoError(t, dao.Vote(memberC, p.ID(), commondao.ChoiceYes, \"\"))\n\t\turequire.NoError(t, dao.Vote(memberD, p.ID(), commondao.ChoiceNo, \"\"))\n\t\turequire.NoError(t, dao.Vote(memberE, p.ID(), commondao.ChoiceNo, \"\"))\n\t\tuassert.Equal(t, string(commondao.StatusActive), string(p.Status())) // 9 \u003c 10\n\n\t\t// NO -\u003e ABSTAIN shrinks the denominator: D=4, 3*3 \u003e= 2*4\n\t\turequire.NoError(t, dao.Vote(memberE, p.ID(), commondao.ChoiceAbstain, \"\"))\n\t\tuassert.Equal(t, string(commondao.StatusPassed), string(p.Status()))\n\t})\n}\n\nfunc TestExpectedOutcome(t *testing.T) {\n\tdao := commondao.New(\n\t\tcommondao.WithCouncilMember(memberA),\n\t\tcommondao.WithCouncilMember(memberB),\n\t\tcommondao.WithCouncilMember(memberC),\n\t\tcommondao.WithProposalKind(probeKind{}),\n\t)\n\n\t// Default rule proposals report the live tally\n\tp := mustPropose(t, dao, memberA, defaultPropDef{period: time.Hour})\n\tuassert.Equal(t, int(commondao.OutcomePending), int(p.ExpectedOutcome()))\n\turequire.NoError(t, dao.Vote(memberA, p.ID(), commondao.ChoiceYes, \"\"))\n\tuassert.Equal(t, int(commondao.OutcomePending), int(p.ExpectedOutcome()))\n\turequire.NoError(t, dao.Vote(memberB, p.ID(), commondao.ChoiceYes, \"\")) // 3*2 \u003e= 2*3\n\tuassert.Equal(t, int(commondao.OutcomePassed), int(p.ExpectedOutcome()))\n\n}\n\nfunc TestExecute(cur realm, t *testing.T) {\n\tt.Run(\"early passed proposals execute before the deadline\", func(t *testing.T) {\n\t\texecuted := false\n\t\tdao := commondao.New(commondao.WithCouncilMember(memberA), commondao.WithProposalKind(probeKind{}))\n\t\tp := mustPropose(t, dao, memberA, defaultPropDef{period: time.Hour, executed: \u0026executed})\n\n\t\turequire.NoError(t, dao.Vote(memberA, p.ID(), commondao.ChoiceYes, \"\"))\n\t\turequire.Equal(t, string(commondao.StatusPassed), string(p.Status()))\n\n\t\turequire.NoError(t, dao.Execute(p.ID(), cur))\n\t\tuassert.True(t, executed)\n\t\tuassert.Equal(t, string(commondao.StatusExecuted), string(p.Status()))\n\t\tuassert.Equal(t, 0, dao.ActiveProposalsSize())\n\t\tuassert.Equal(t, 1, dao.FinishedProposalsSize())\n\t})\n\n\tt.Run(\"active proposals cannot execute before the deadline\", func(t *testing.T) {\n\t\tdao := commondao.New(commondao.WithCouncilMember(memberA), commondao.WithProposalKind(probeKind{}))\n\t\tp := mustPropose(t, dao, memberA, defaultPropDef{period: time.Hour})\n\n\t\terr := dao.Execute(p.ID(), cur)\n\t\tuassert.ErrorIs(t, err, commondao.ErrVotingDeadlineNotMet)\n\t})\n\n\tt.Run(\"undecided default proposals are dismissed at the deadline\", func(t *testing.T) {\n\t\tdao := commondao.New(commondao.WithCouncilMember(memberA), commondao.WithProposalKind(probeKind{}))\n\t\tp := mustPropose(t, dao, memberA, defaultPropDef{}) // zero voting period\n\n\t\turequire.NoError(t, dao.Execute(p.ID(), cur))\n\t\tuassert.Equal(t, string(commondao.StatusDismissed), string(p.Status()))\n\t\tuassert.Equal(t, 1, dao.FinishedProposalsSize())\n\t})\n\n\tt.Run(\"undecided proposals dismiss before validation\", func(t *testing.T) {\n\t\t// A proposal that never passed archives as Dismissed even when its\n\t\t// definition no longer validates: dismissal is the truthful status\n\t\t// and the executor could never have run.\n\t\tstale := errors.New(\"stale\")\n\t\tdao := commondao.New(commondao.WithCouncilMember(memberA), commondao.WithProposalKind(probeKind{}))\n\t\tp := mustPropose(t, dao, memberA, validatingExecPropDef{\n\t\t\tdefaultPropDef: defaultPropDef{}, // zero voting period\n\t\t\terr:            \u0026stale,\n\t\t})\n\n\t\turequire.NoError(t, dao.Execute(p.ID(), cur))\n\t\tuassert.Equal(t, string(commondao.StatusDismissed), string(p.Status()))\n\t})\n\n\tt.Run(\"executor errors fail the proposal\", func(t *testing.T) {\n\t\tdao := commondao.New(commondao.WithCouncilMember(memberA), commondao.WithProposalKind(probeKind{}))\n\t\tp := mustPropose(t, dao, memberA, defaultPropDef{period: time.Hour, execErr: errors.New(\"exec boom\")})\n\n\t\turequire.NoError(t, dao.Vote(memberA, p.ID(), commondao.ChoiceYes, \"\"))\n\t\turequire.NoError(t, dao.Execute(p.ID(), cur))\n\t\tuassert.Equal(t, string(commondao.StatusFailed), string(p.Status()))\n\t\tuassert.Equal(t, \"exec boom\", p.StatusReason())\n\t\tuassert.Equal(t, 1, dao.FinishedProposalsSize())\n\t})\n\n\tt.Run(\"emptying council updates fail the proposal\", func(t *testing.T) {\n\t\tdao := commondao.New(commondao.WithCouncilMember(memberA), commondao.WithProposalKind(probeKind{}))\n\t\tp := mustPropose(t, dao, memberA, execFnPropDef{\n\t\t\tpropDef: propDef{period: time.Hour},\n\t\t\tfn: func(_ int, sub realm) error {\n\t\t\t\treturn dao.UpdateCouncil(nil, []address{memberA})\n\t\t\t},\n\t\t})\n\n\t\turequire.NoError(t, dao.Vote(memberA, p.ID(), commondao.ChoiceYes, \"\"))\n\t\turequire.NoError(t, dao.Execute(p.ID(), cur))\n\t\tuassert.Equal(t, string(commondao.StatusFailed), string(p.Status()))\n\t\tuassert.Equal(t, 1, dao.Council().Size(), \"expect council unchanged\")\n\t})\n\n\tt.Run(\"early passed proposals still validate\", func(t *testing.T) {\n\t\texecuted := false\n\t\tstale := errors.New(\"stale\")\n\t\tdao := commondao.New(commondao.WithCouncilMember(memberA), commondao.WithProposalKind(probeKind{}))\n\t\tp := mustPropose(t, dao, memberA, validatingExecPropDef{\n\t\t\tdefaultPropDef: defaultPropDef{period: time.Hour, executed: \u0026executed},\n\t\t\terr:            \u0026stale,\n\t\t})\n\n\t\turequire.NoError(t, dao.Vote(memberA, p.ID(), commondao.ChoiceYes, \"\"))\n\t\turequire.Equal(t, string(commondao.StatusPassed), string(p.Status()))\n\n\t\turequire.NoError(t, dao.Execute(p.ID(), cur))\n\t\tuassert.Equal(t, string(commondao.StatusFailed), string(p.Status()))\n\t\tuassert.Equal(t, \"stale\", p.StatusReason())\n\t\tuassert.False(t, executed, \"expect executor to not run when validation fails\")\n\t})\n\n\tt.Run(\"executors cannot re-enter Execute\", func(t *testing.T) {\n\t\t// The per-DAO executing latch panics on a re-entrant Execute of\n\t\t// the same DAO (aborting the tx), even for a different proposal\n\t\t// than the one currently executing. Removing the latch would let\n\t\t// the re-entrant call proceed.\n\t\tvar (\n\t\t\tdao   = commondao.New(commondao.WithCouncilMember(memberA), commondao.WithProposalKind(probeKind{}))\n\t\t\texecs int\n\t\t\tpid2  uint64\n\t\t)\n\t\tp1 := mustPropose(t, dao, memberA, execFnPropDef{\n\t\t\tpropDef: propDef{period: time.Hour},\n\t\t\tfn: func(_ int, sub realm) error {\n\t\t\t\texecs++\n\t\t\t\treturn dao.Execute(pid2, sub) // re-entrant: must panic\n\t\t\t},\n\t\t})\n\t\tp2 := mustPropose(t, dao, memberA, defaultPropDef{period: time.Hour})\n\t\tpid2 = p2.ID()\n\n\t\turequire.NoError(t, dao.Vote(memberA, p1.ID(), commondao.ChoiceYes, \"\"))\n\t\turequire.NoError(t, dao.Vote(memberA, pid2, commondao.ChoiceYes, \"\"))\n\n\t\tuassert.PanicsWithMessage(t, cur, \"commondao: re-entrant Execute is not allowed\", func() {\n\t\t\tdao.Execute(p1.ID(), cur)\n\t\t})\n\t\tuassert.Equal(t, 1, execs, \"expect executor to run exactly once\")\n\t})\n\n\tt.Run(\"kinds cannot re-enter Propose\", func(t *testing.T) {\n\t\t// The per-DAO proposing latch panics on a re-entrant Propose from\n\t\t// inside a kind's New. Removing the latch would let the nested\n\t\t// Propose proceed. (The latch also, via defer, cannot stay stuck\n\t\t// if New panics.)\n\t\tdao := commondao.New(commondao.WithCouncilMember(memberA), commondao.WithProposalKind(probeKind{}))\n\t\turequire.NoError(t, dao.RegisterKind(reProposeKind{dao: dao}))\n\t\tuassert.PanicsWithMessage(t, cur, \"commondao: re-entrant Propose is not allowed\", func() {\n\t\t\tdao.Propose(memberA, \"re-propose\", nil)\n\t\t})\n\t})\n\n\tt.Run(\"concurrent council updates merge in execution order\", func(t *testing.T) {\n\t\tdao := commondao.New(\n\t\t\tcommondao.WithCouncilMember(memberA),\n\t\t\tcommondao.WithCouncilMember(memberB),\n\t\t\tcommondao.WithCouncilMember(memberC),\n\t\t\tcommondao.WithProposalKind(probeKind{}),\n\t\t)\n\t\tp1 := mustPropose(t, dao, memberA, execFnPropDef{\n\t\t\tpropDef: propDef{period: time.Hour},\n\t\t\tfn: func(_ int, sub realm) error {\n\t\t\t\treturn dao.UpdateCouncil([]address{memberD}, []address{memberA})\n\t\t\t},\n\t\t})\n\t\tp2 := mustPropose(t, dao, memberB, execFnPropDef{\n\t\t\tpropDef: propDef{period: time.Hour},\n\t\t\tfn: func(_ int, sub realm) error {\n\t\t\t\t// Removing memberA again is an idempotent no-op\n\t\t\t\treturn dao.UpdateCouncil([]address{memberE}, []address{memberA})\n\t\t\t},\n\t\t})\n\n\t\tfor _, p := range []*commondao.Proposal{p1, p2} {\n\t\t\turequire.NoError(t, dao.Vote(memberA, p.ID(), commondao.ChoiceYes, \"\"))\n\t\t\turequire.NoError(t, dao.Vote(memberB, p.ID(), commondao.ChoiceYes, \"\"))\n\t\t}\n\n\t\turequire.NoError(t, dao.Execute(p1.ID(), cur))\n\t\turequire.NoError(t, dao.Execute(p2.ID(), cur))\n\t\turequire.Equal(t, string(commondao.StatusExecuted), string(p1.Status()))\n\t\turequire.Equal(t, string(commondao.StatusExecuted), string(p2.Status()))\n\n\t\tuassert.Equal(t, 4, dao.Council().Size())\n\t\tuassert.False(t, dao.Council().Has(memberA))\n\t\tuassert.True(t, dao.Council().Has(memberB))\n\t\tuassert.True(t, dao.Council().Has(memberC))\n\t\tuassert.True(t, dao.Council().Has(memberD))\n\t\tuassert.True(t, dao.Council().Has(memberE))\n\t})\n\n\tt.Run(\"deleted DAOs reject execution\", func(t *testing.T) {\n\t\tdao := commondao.New(commondao.WithCouncilMember(memberA), commondao.WithProposalKind(probeKind{}))\n\t\tp := mustPropose(t, dao, memberA, propDef{period: time.Hour})\n\t\tdao.Dissolve(\"test\")\n\n\t\terr := dao.Execute(p.ID(), cur)\n\t\tuassert.ErrorIs(t, err, commondao.ErrDAOIsDeleted)\n\t})\n\n\tt.Run(\"unknown proposals are rejected\", func(t *testing.T) {\n\t\tdao := commondao.New(commondao.WithCouncilMember(memberA), commondao.WithProposalKind(probeKind{}))\n\t\terr := dao.Execute(404, cur)\n\t\tuassert.ErrorIs(t, err, commondao.ErrProposalNotFound)\n\t})\n}\n\nfunc TestWithdraw(t *testing.T) {\n\tdao := commondao.New(\n\t\tcommondao.WithCouncilMember(memberA),\n\t\tcommondao.WithCouncilMember(memberB),\n\t\tcommondao.WithCouncilMember(memberC),\n\t\tcommondao.WithProposalKind(probeKind{}),\n\t)\n\n\t// Proposals without votes can be withdrawn\n\tp := mustPropose(t, dao, memberA, propDef{period: time.Hour})\n\turequire.NoError(t, dao.Withdraw(p.ID()))\n\tuassert.Equal(t, string(commondao.StatusWithdrawn), string(p.Status()))\n\tuassert.Equal(t, 0, dao.ActiveProposalsSize())\n\tuassert.Equal(t, 1, dao.FinishedProposalsSize())\n\n\t// Withdrawn proposals cannot be withdrawn again\n\terr := dao.Withdraw(p.ID())\n\tuassert.ErrorIs(t, err, commondao.ErrProposalNotFound)\n\n\t// Proposals with votes cannot be withdrawn\n\tp = mustPropose(t, dao, memberA, propDef{period: time.Hour})\n\turequire.NoError(t, dao.Vote(memberA, p.ID(), commondao.ChoiceAbstain, \"\"))\n\terr = dao.Withdraw(p.ID())\n\tuassert.ErrorIs(t, err, commondao.ErrWithdrawalNotAllowed)\n\n\t// Early passed proposals cannot be withdrawn\n\tp2 := mustPropose(t, dao, memberA, defaultPropDef{period: time.Hour})\n\turequire.NoError(t, dao.Vote(memberA, p2.ID(), commondao.ChoiceYes, \"\"))\n\turequire.NoError(t, dao.Vote(memberB, p2.ID(), commondao.ChoiceYes, \"\"))\n\turequire.Equal(t, string(commondao.StatusPassed), string(p2.Status()))\n\terr = dao.Withdraw(p2.ID())\n\tuassert.ErrorIs(t, err, commondao.ErrStatusIsNotActive)\n}\n\nfunc TestReadonlyCommonDAO(t *testing.T) {\n\tparent := commondao.New(commondao.WithName(\"root\"))\n\tdao := commondao.New(\n\t\tcommondao.WithID(7),\n\t\tcommondao.WithName(\"child\"),\n\t\tcommondao.WithDescription(\"test\"),\n\t\tcommondao.WithParent(parent),\n\t\tcommondao.WithCouncilMember(memberA),\n\t\tcommondao.WithProposalKind(probeKind{}),\n\t)\n\tp := mustPropose(t, dao, memberA, propDef{title: \"T\", body: \"B\", period: time.Hour})\n\n\tview := dao.Readonly()\n\tuassert.Equal(t, uint64(7), view.ID())\n\tuassert.Equal(t, \"child\", view.Name())\n\tuassert.Equal(t, \"test\", view.Description())\n\tuassert.False(t, view.IsDeleted())\n\tuassert.Equal(t, 1, view.Council().Size())\n\tuassert.Equal(t, 1, view.ActiveProposalsSize())\n\tuassert.Equal(t, 0, view.FinishedProposalsSize())\n\tuassert.Equal(t, 0, view.ChildrenCount())\n\n\tparentView, found := view.Parent()\n\turequire.True(t, found, \"expect parent view\")\n\tuassert.Equal(t, \"root\", parentView.Name())\n\t_, found = parentView.Parent()\n\tuassert.False(t, found)\n\n\tpv, found := view.GetProposal(p.ID())\n\turequire.True(t, found, \"expect proposal view\")\n\tuassert.Equal(t, p.ID(), pv.ID())\n\tuassert.Equal(t, \"T\", pv.Title())\n\tuassert.Equal(t, \"B\", pv.Body())\n\tuassert.Equal(t, string(commondao.StatusActive), string(pv.Status()))\n\tuassert.Equal(t, memberA, pv.Creator())\n\tuassert.Equal(t, 1, pv.Electorate().Size())\n\tuassert.Equal(t, 0, pv.VotingRecord().Size())\n\n\tvisited := 0\n\tview.IterateActiveProposals(0, 10, false, func(commondao.ReadonlyProposal) bool {\n\t\tvisited++\n\t\treturn false\n\t})\n\tuassert.Equal(t, 1, visited)\n\n\t// WithParent registered dao as a child of parent at construction\n\tuassert.Equal(t, 1, parentView.ChildrenCount())\n\tchildID := uint64(0)\n\tparentView.IterateChildren(func(child commondao.ReadonlyCommonDAO) bool {\n\t\tchildID = child.ID()\n\t\treturn false\n\t})\n\tuassert.Equal(t, uint64(7), childID)\n\n\t// A withdrawn proposal is visible through the finished-proposals view\n\tfp := mustPropose(t, dao, memberA, propDef{period: time.Hour})\n\turequire.NoError(t, dao.Withdraw(fp.ID()))\n\tuassert.Equal(t, 1, view.FinishedProposalsSize())\n\tfinished := 0\n\tview.IterateFinishedProposals(0, 10, false, func(commondao.ReadonlyProposal) bool {\n\t\tfinished++\n\t\treturn false\n\t})\n\tuassert.Equal(t, 1, finished)\n\n\t// Recorded votes are visible through the voting-record view\n\turequire.NoError(t, dao.Vote(memberA, p.ID(), commondao.ChoiceYes, \"\"))\n\tpv, _ = view.GetProposal(p.ID())\n\tyes := 0\n\tpv.VotingRecord().IterateVotesCount(func(c commondao.VoteChoice, count int) bool {\n\t\tif c == commondao.ChoiceYes {\n\t\t\tyes = count\n\t\t}\n\t\treturn false\n\t})\n\tuassert.Equal(t, 1, yes)\n\n\t// Views are live handles\n\tdao.Dissolve(\"test\")\n\tuassert.True(t, view.IsDeleted())\n}\n\nfunc TestCharter(t *testing.T) {\n\tdao := commondao.New(\n\t\tcommondao.WithName(\"D\"),\n\t\tcommondao.WithPurpose(\"to do good\"),\n\t\tcommondao.WithDescription(\"a test DAO\"),\n\t)\n\tuassert.Equal(t, \"to do good\", dao.Purpose())\n\tuassert.Equal(t, \"a test DAO\", dao.Description())\n\tuassert.Equal(t, \"to do good\", dao.Readonly().Purpose())\n\n\t// Purpose defaults empty when unset (the realm enforces non-empty).\n\tuassert.Equal(t, \"\", commondao.New().Purpose())\n}\n\nfunc TestAddressAndTreasuryFrozen(t *testing.T) {\n\tdao := commondao.New(commondao.WithAddress(memberA))\n\tuassert.Equal(t, string(memberA), string(dao.Address()))\n\n\tview := dao.Readonly()\n\tuassert.Equal(t, string(memberA), string(view.Address()))\n\n\t// Unset addresses are empty\n\tuassert.Equal(t, \"\", string(commondao.New().Address()))\n\n\tuassert.False(t, dao.IsTreasuryFrozen())\n\tdao.SetTreasuryFrozen(true)\n\tuassert.True(t, dao.IsTreasuryFrozen())\n\tuassert.True(t, view.IsTreasuryFrozen())\n\tdao.SetTreasuryFrozen(false)\n\tuassert.False(t, view.IsTreasuryFrozen())\n}\n\nfunc TestSetMaxActiveProposals(t *testing.T) {\n\tdao := commondao.New(commondao.WithCouncilMember(memberA), commondao.WithProposalKind(probeKind{}))\n\n\tdao.SetMaxActiveProposals(2)\n\t_ = mustPropose(t, dao, memberA, propDef{period: time.Hour})\n\t_ = mustPropose(t, dao, memberA, propDef{period: time.Hour})\n\t_, err := dao.Propose(memberA, \"probe\", propDef{period: time.Hour})\n\tuassert.ErrorIs(t, err, commondao.ErrMaxActiveProposals)\n\n\tuassert.Equal(t, 2, dao.MaxActiveProposals())\n\n\t// Values below the floor are ignored (the floor is 1, not 0/negative)\n\tdao.SetMaxActiveProposals(0)\n\t_, err = dao.Propose(memberA, \"probe\", propDef{period: time.Hour})\n\tuassert.ErrorIs(t, err, commondao.ErrMaxActiveProposals)\n\tdao.SetMaxActiveProposals(-5)\n\t_, err = dao.Propose(memberA, \"probe\", propDef{period: time.Hour})\n\tuassert.ErrorIs(t, err, commondao.ErrMaxActiveProposals)\n}\n\nfunc TestKindRegistry(cur realm, t *testing.T) {\n\tdao := commondao.New(commondao.WithCouncilMember(memberA))\n\n\t// A fresh DAO has no registered kinds\n\tuassert.False(t, dao.HasKind(\"probe\"))\n\tuassert.Equal(t, 0, len(dao.KindNames()))\n\n\t// Proposing through an unregistered kind is rejected\n\t_, err := dao.Propose(memberA, \"probe\", propDef{period: time.Hour})\n\tuassert.ErrorIs(t, err, commondao.ErrProposalKindNotFound)\n\n\t// Registered kinds create proposals\n\turequire.NoError(t, dao.RegisterKind(probeKind{}))\n\tuassert.True(t, dao.HasKind(\"probe\"))\n\tp := mustPropose(t, dao, memberA, propDef{period: time.Hour})\n\n\t// Nil kinds and empty kind names are rejected\n\terr = dao.RegisterKind(nil)\n\tuassert.ErrorIs(t, err, commondao.ErrProposalKindRequired)\n\terr = dao.RegisterKind(namedKind(\"\"))\n\tuassert.ErrorIs(t, err, commondao.ErrProposalKindRequired)\n\n\t// Duplicate kind names are rejected\n\terr = dao.RegisterKind(probeKind{})\n\tuassert.ErrorIs(t, err, commondao.ErrProposalKindExists)\n\n\t// KindNames is sorted by name\n\turequire.NoError(t, dao.RegisterKind(namedKind(\"zeta\")))\n\turequire.NoError(t, dao.RegisterKind(namedKind(\"alpha\")))\n\tnames := dao.KindNames()\n\turequire.Equal(t, 3, len(names))\n\tuassert.Equal(t, \"alpha\", names[0])\n\tuassert.Equal(t, \"probe\", names[1])\n\tuassert.Equal(t, \"zeta\", names[2])\n\n\t// The readonly view mirrors the registry reads\n\tview := dao.Readonly()\n\tuassert.True(t, view.HasKind(\"alpha\"))\n\tuassert.False(t, view.HasKind(\"beta\"))\n\tuassert.Equal(t, 3, len(view.KindNames()))\n\n\t// Deregistering an absent kind is rejected\n\terr = dao.DeregisterKind(\"beta\")\n\tuassert.ErrorIs(t, err, commondao.ErrProposalKindNotFound)\n\n\t// Deregistering blocks new proposals of the kind...\n\turequire.NoError(t, dao.DeregisterKind(\"probe\"))\n\tuassert.False(t, dao.HasKind(\"probe\"))\n\t_, err = dao.Propose(memberA, \"probe\", propDef{period: time.Hour})\n\tuassert.ErrorIs(t, err, commondao.ErrProposalKindNotFound)\n\n\t// ...but never touches in-flight proposals: the frozen definition\n\t// still votes, decides and executes (vote-integrity)\n\turequire.NoError(t, dao.Vote(memberA, p.ID(), commondao.ChoiceYes, \"\"))\n\turequire.Equal(t, string(commondao.StatusPassed), string(p.Status()))\n\turequire.NoError(t, dao.Execute(p.ID(), cur))\n\tuassert.Equal(t, string(commondao.StatusExecuted), string(p.Status()))\n\n\t// Kind factory errors propagate out of Propose\n\tboom := errors.New(\"bad args\")\n\turequire.NoError(t, dao.RegisterKind(failingKind{err: boom}))\n\t_, err = dao.Propose(memberA, \"failing\", nil)\n\tuassert.ErrorIs(t, err, boom)\n\tuassert.Equal(t, 0, dao.ActiveProposalsSize(), \"expect no proposal from a failing factory\")\n\n\t// Propose hands the kind factory a readonly view of the host DAO\n\tvar hostID uint64 = 999 // sentinel: New must overwrite it\n\turequire.NoError(t, dao.RegisterKind(hostKind{hostID: \u0026hostID}))\n\t_, err = dao.Propose(memberA, \"host\", nil)\n\turequire.NoError(t, err)\n\tuassert.Equal(t, dao.ID(), hostID, \"expect the kind factory to receive a readonly view of the host DAO\")\n\n\t// Construction options panic on registration errors\n\tuassert.PanicsWithMessage(t, cur, commondao.ErrProposalKindRequired.Error(), func() {\n\t\tcommondao.New(commondao.WithProposalKind(nil))\n\t})\n\tuassert.PanicsWithMessage(t, cur, commondao.ErrProposalKindExists.Error(), func() {\n\t\tcommondao.New(\n\t\t\tcommondao.WithProposalKind(probeKind{}),\n\t\t\tcommondao.WithProposalKind(probeKind{}),\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":"execution_kind_test.gno","body":"package commondao_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n\n\t\"gno.land/p/nt/commondao/v0\"\n)\n\n// New receives a ReadonlyCommonDAO, which exposes no mutators, so a kind\n// cannot mutate the host (or its tree) at Propose time. This is enforced\n// structurally by the type system, not at runtime: the following method\n// does not compile, because ReadonlyCommonDAO has no UpdateCouncil (nor\n// any other mutator), and it cannot be downcast to *CommonDAO.\n//\n//\tfunc (mutatingKind) New(dao commondao.ReadonlyCommonDAO, _ any) (commondao.ProposalDefinition, error) {\n//\t\tdao.UpdateCouncil(nil, []address{memberA}) // compile error: undefined\n//\t\treturn propDef{}, nil\n//\t}\n\nfunc TestExecutionKind(cur realm, t *testing.T) {\n\tdao := commondao.New(\n\t\tcommondao.WithCouncilMember(memberA),\n\t\tcommondao.WithProposalKind(commondao.ExecutionKind{}),\n\t)\n\n\t// A nil Fn is rejected at Propose.\n\t_, err := dao.Propose(memberA, \"execution\", commondao.ExecutionArgs{Title: \"T\", Body: \"B\"})\n\tuassert.ErrorIs(t, err, commondao.ErrExecutionFuncRequired)\n\n\t// Wrong args type is rejected too.\n\t_, err = dao.Propose(memberA, \"execution\", \"not the args\")\n\tuassert.ErrorIs(t, err, commondao.ErrExecutionFuncRequired)\n\n\t// A valid execution proposal runs the supplied Fn on approval.\n\tran := false\n\tp, err := dao.Propose(memberA, \"execution\", commondao.ExecutionArgs{\n\t\tTitle: \"Do a thing\",\n\t\tBody:  \"details\",\n\t\tFn: func(_ int, _ realm) error {\n\t\t\tran = true\n\t\t\treturn nil\n\t\t},\n\t})\n\turequire.NoError(t, err)\n\tuassert.Equal(t, \"Do a thing\", p.Definition().Title())\n\tuassert.Equal(t, \"details\", p.Definition().Body())\n\n\turequire.NoError(t, dao.Vote(memberA, p.ID(), commondao.ChoiceYes, \"\"))\n\turequire.Equal(t, string(commondao.StatusPassed), string(p.Status()))\n\turequire.NoError(t, dao.Execute(p.ID(), cur))\n\tuassert.True(t, ran, \"expect the supplied Fn to run\")\n\tuassert.Equal(t, string(commondao.StatusExecuted), string(p.Status()))\n}\n\n// TestDeregisterKind pins that DeregisterKind is a plain primitive with no\n// reserved names: any registered kind can be removed (anti-brick is the\n// consuming realm's policy, not a /p/ lock), and an unknown name reports\n// ErrProposalKindNotFound.\nfunc TestDeregisterKind(t *testing.T) {\n\tdao := commondao.New(\n\t\tcommondao.WithCouncilMember(memberA),\n\t\tcommondao.WithProposalKind(commondao.ExecutionKind{}),\n\t\t// A kind whose name sounds reserved is still ordinary: /p/ reserves no\n\t\t// names, so it deregisters like any other.\n\t\tcommondao.WithProposalKind(namedKind(\"register-kind\")),\n\t)\n\n\t// Any registered kind deregisters, including the reserved-sounding name.\n\turequire.NoError(t, dao.DeregisterKind(\"register-kind\"))\n\tuassert.False(t, dao.HasKind(\"register-kind\"))\n\n\turequire.NoError(t, dao.DeregisterKind(\"execution\"))\n\tuassert.False(t, dao.HasKind(\"execution\"))\n\n\t// An unknown name is rejected.\n\terr := dao.DeregisterKind(\"unknown\")\n\tuassert.ErrorIs(t, err, commondao.ErrProposalKindNotFound)\n}\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":"proposal_test.gno","body":"package commondao_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\n\t\"gno.land/p/nt/commondao/v0\"\n)\n\n// validablePropDef supports state validation.\ntype validablePropDef struct {\n\tpropDef\n\terr error\n}\n\nfunc (d validablePropDef) Validate() error { return d.err }\n\nfunc TestProposalNew(t *testing.T) {\n\tdao := commondao.New(commondao.WithCouncilMember(memberA), commondao.WithProposalKind(probeKind{}))\n\n\tt.Run(\"creator must be valid\", func(t *testing.T) {\n\t\t_, err := dao.Propose(address(\"\"), \"probe\", propDef{})\n\t\tuassert.ErrorIs(t, err, commondao.ErrInvalidCreatorAddress)\n\t})\n\n\tt.Run(\"defaults\", func(t *testing.T) {\n\t\tp := mustPropose(t, dao, memberA, propDef{title: \"T\", body: \"B\", period: time.Hour})\n\n\t\tuassert.Equal(t, string(commondao.StatusActive), string(p.Status()))\n\t\tuassert.Equal(t, memberA, p.Creator())\n\t\tuassert.Equal(t, \"T\", p.Definition().Title())\n\t\tuassert.Equal(t, \"B\", p.Definition().Body())\n\t\tuassert.Equal(t, 0, p.VotingRecord().Size())\n\t\tuassert.Equal(t, \"\", p.StatusReason())\n\t\tuassert.False(t, p.HasVotingDeadlinePassed())\n\n\t})\n}\n\nfunc TestProposalValidate(t *testing.T) {\n\tdao := commondao.New(commondao.WithCouncilMember(memberA), commondao.WithProposalKind(probeKind{}))\n\n\tt.Run(\"non validable definitions are always valid\", func(t *testing.T) {\n\t\tp := mustPropose(t, dao, memberA, propDef{period: time.Hour})\n\t\tuassert.NoError(t, p.Validate())\n\t})\n\n\tt.Run(\"validable definitions validate while executable\", func(t *testing.T) {\n\t\tboom := errors.New(\"boom\")\n\t\tp := mustPropose(t, dao, memberA, validablePropDef{\n\t\t\tpropDef: propDef{period: time.Hour},\n\t\t\terr:     boom,\n\t\t})\n\t\tuassert.ErrorIs(t, p.Validate(), boom)\n\t})\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"},{"name":"record_test.gno","body":"package commondao_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/addrset/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n\n\t\"gno.land/p/nt/commondao/v0\"\n)\n\n// mustNewVote builds a reason-less vote or fails the test.\nfunc mustNewVote(t *testing.T, addr address, c commondao.VoteChoice) commondao.Vote {\n\tt.Helper()\n\tv, err := commondao.NewVote(addr, c, \"\")\n\turequire.NoError(t, err, \"new vote\")\n\treturn v\n}\n\nfunc TestVotingRecordDefaults(t *testing.T) {\n\tvar record commondao.VotingRecord\n\n\tuassert.Equal(t, 0, record.Size())\n\tuassert.Equal(t, 0, record.VoteCount(commondao.ChoiceYes))\n\tuassert.False(t, record.HasVoted(memberA))\n\n\t_, found := record.GetVote(memberA)\n\tuassert.False(t, found)\n}\n\nfunc TestVotingRecordAddVote(t *testing.T) {\n\tvar record commondao.VotingRecord\n\n\tvote, err := commondao.NewVote(memberA, commondao.ChoiceYes, \"foo\")\n\turequire.NoError(t, err)\n\tupdated := record.AddVote(vote)\n\tuassert.False(t, updated, \"expect first vote to be an insert\")\n\tuassert.Equal(t, 1, record.Size())\n\tuassert.Equal(t, 1, record.VoteCount(commondao.ChoiceYes))\n\tuassert.True(t, record.HasVoted(memberA))\n\n\tv, found := record.GetVote(memberA)\n\tuassert.True(t, found)\n\tuassert.Equal(t, string(commondao.ChoiceYes), string(v.Choice()))\n\tuassert.Equal(t, \"foo\", v.Reason())\n\n\t// Voting again overwrites the previous vote and updates the counters\n\tupdated = record.AddVote(mustNewVote(t, memberA, commondao.ChoiceNo))\n\tuassert.True(t, updated, \"expect second vote to be an update\")\n\tuassert.Equal(t, 1, record.Size())\n\tuassert.Equal(t, 0, record.VoteCount(commondao.ChoiceYes))\n\tuassert.Equal(t, 1, record.VoteCount(commondao.ChoiceNo))\n\n\trecord.AddVote(mustNewVote(t, memberB, commondao.ChoiceNo))\n\tuassert.Equal(t, 2, record.Size())\n\tuassert.Equal(t, 2, record.VoteCount(commondao.ChoiceNo))\n\n\tcount := 0\n\trecord.Iterate(0, record.Size(), false, func(commondao.Vote) bool {\n\t\tcount++\n\t\treturn false\n\t})\n\tuassert.Equal(t, 2, count)\n}\n\nfunc TestTallyDefault(t *testing.T) {\n\tcases := []struct {\n\t\tname       string\n\t\telectorate []address\n\t\tyes        []address\n\t\tno         []address\n\t\tabstain    []address\n\t\tthreshold  commondao.Threshold\n\t\twant       commondao.Outcome\n\t}{\n\t\t{\n\t\t\tname: \"empty electorate stays pending\",\n\t\t\twant: commondao.OutcomePending,\n\t\t},\n\t\t{\n\t\t\tname:       \"no votes stays pending\",\n\t\t\telectorate: []address{memberA, memberB, memberC},\n\t\t\twant:       commondao.OutcomePending,\n\t\t},\n\t\t{\n\t\t\tname:       \"all abstain stays pending\",\n\t\t\telectorate: []address{memberA, memberB, memberC},\n\t\t\tabstain:    []address{memberA, memberB, memberC},\n\t\t\twant:       commondao.OutcomePending,\n\t\t},\n\t\t{\n\t\t\tname:       \"supermajority boundary passes\",\n\t\t\telectorate: []address{memberA, memberB, memberC},\n\t\t\tyes:        []address{memberA, memberB}, // 3*2 \u003e= 2*3\n\t\t\twant:       commondao.OutcomePassed,\n\t\t},\n\t\t{\n\t\t\tname:       \"below supermajority stays pending\",\n\t\t\telectorate: []address{memberA, memberB, memberC},\n\t\t\tyes:        []address{memberA},\n\t\t\tno:         []address{memberB},\n\t\t\twant:       commondao.OutcomePending,\n\t\t},\n\t\t{\n\t\t\tname:       \"abstains shrink the denominator for passing\",\n\t\t\telectorate: []address{memberA, memberB, memberC},\n\t\t\tyes:        []address{memberA},\n\t\t\tabstain:    []address{memberB, memberC}, // D=1, 3*1 \u003e= 2*1\n\t\t\twant:       commondao.OutcomePassed,\n\t\t},\n\t\t{\n\t\t\tname:       \"silence counts against passage\",\n\t\t\telectorate: []address{memberA, memberB, memberC, memberD, memberE, memberF, memberG, memberH, memberI},\n\t\t\tyes:        []address{memberA, memberB, memberC, memberD},\n\t\t\tno:         []address{memberE}, // 4Y/1N/4 silent: 12 \u003c 18\n\t\t\twant:       commondao.OutcomePending,\n\t\t},\n\t\t{\n\t\t\tname:       \"NO majority boundary is strict\",\n\t\t\telectorate: []address{memberA, memberB, memberC, memberD},\n\t\t\tno:         []address{memberA, memberB}, // 2*2 == 4: not dismissed\n\t\t\twant:       commondao.OutcomePending,\n\t\t},\n\t\t{\n\t\t\tname:       \"NO majority dismisses\",\n\t\t\telectorate: []address{memberA, memberB, memberC, memberD},\n\t\t\tno:         []address{memberA, memberB, memberC}, // 2*3 \u003e 4\n\t\t\twant:       commondao.OutcomeDismissed,\n\t\t},\n\t\t{\n\t\t\tname:       \"abstains shrink the denominator for dismissal\",\n\t\t\telectorate: []address{memberA, memberB, memberC, memberD, memberE},\n\t\t\tno:         []address{memberA, memberB},\n\t\t\tabstain:    []address{memberC, memberD}, // D=3, 2*2 \u003e 3\n\t\t\twant:       commondao.OutcomeDismissed,\n\t\t},\n\t\t{\n\t\t\tname:       \"simple majority boundary does not pass\",\n\t\t\telectorate: []address{memberA, memberB, memberC, memberD},\n\t\t\tyes:        []address{memberA, memberB}, // 2*2 == 4\n\t\t\tthreshold:  commondao.ThresholdSimpleMajority,\n\t\t\twant:       commondao.OutcomePending,\n\t\t},\n\t\t{\n\t\t\tname:       \"simple majority passes\",\n\t\t\telectorate: []address{memberA, memberB, memberC, memberD},\n\t\t\tyes:        []address{memberA, memberB, memberC}, // 2*3 \u003e 4\n\t\t\tthreshold:  commondao.ThresholdSimpleMajority,\n\t\t\twant:       commondao.OutcomePassed,\n\t\t},\n\t\t{\n\t\t\tname:       \"simple majority minimal boundary passes\",\n\t\t\telectorate: []address{memberA, memberB, memberC},\n\t\t\tyes:        []address{memberA, memberB}, // 2*2 == D+1\n\t\t\tthreshold:  commondao.ThresholdSimpleMajority,\n\t\t\twant:       commondao.OutcomePassed,\n\t\t},\n\t\t{\n\t\t\tname:       \"votes from outside the electorate are not counted\",\n\t\t\telectorate: []address{memberA, memberB},\n\t\t\tyes:        []address{memberC, memberD}, // strangers\n\t\t\twant:       commondao.OutcomePending,\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar (\n\t\t\t\telectorate addrset.Set\n\t\t\t\trecord     commondao.VotingRecord\n\t\t\t)\n\t\t\tfor _, m := range tc.electorate {\n\t\t\t\telectorate.Add(m)\n\t\t\t}\n\t\t\tfor _, m := range tc.yes {\n\t\t\t\trecord.AddVote(mustNewVote(t, m, commondao.ChoiceYes))\n\t\t\t}\n\t\t\tfor _, m := range tc.no {\n\t\t\t\trecord.AddVote(mustNewVote(t, m, commondao.ChoiceNo))\n\t\t\t}\n\t\t\tfor _, m := range tc.abstain {\n\t\t\t\trecord.AddVote(mustNewVote(t, m, commondao.ChoiceAbstain))\n\t\t\t}\n\n\t\t\tgot := commondao.TallyDefault(record.Readonly(), electorate.Readonly(), tc.threshold)\n\n\t\t\tuassert.Equal(t, int(tc.want), int(got))\n\t\t})\n\t}\n}\n\n// refOutcome is an independent restatement of the constitutional tally\n// rule, used to exhaustively cross-check TallyDefault at every boundary.\nfunc refOutcome(electorateSize, yes, no, abstain int, threshold commondao.Threshold) commondao.Outcome {\n\td := electorateSize - abstain\n\tif d \u003c= 0 {\n\t\treturn commondao.OutcomePending\n\t}\n\tpassed := false\n\tif threshold == commondao.ThresholdSimpleMajority {\n\t\tpassed = 2*yes \u003e d // \"more than half\"\n\t} else {\n\t\tpassed = 3*yes \u003e= 2*d // \"two thirds or more\"\n\t}\n\tif passed {\n\t\treturn commondao.OutcomePassed\n\t}\n\tif 2*no \u003e d { // NO simple majority dismisses (both thresholds)\n\t\treturn commondao.OutcomeDismissed\n\t}\n\treturn commondao.OutcomePending\n}\n\n// TestTallyDefaultProperty exhaustively enumerates every electorate size\n// up to 8 and every (yes, no, abstain) split for both thresholds,\n// checking TallyDefault against refOutcome and machine-checking the\n// invariants the ADR argues in prose (mutual exclusivity, the D\u003e0 guard,\n// abstain-shrinks-D monotonicity, and electorate gating).\nfunc TestTallyDefaultProperty(t *testing.T) {\n\t// Eight electorate members plus one non-member \"stranger\".\n\tpool := []address{memberA, memberB, memberC, memberD, memberE, memberF, memberG, memberH}\n\tstranger := memberI\n\tthresholds := []commondao.Threshold{commondao.ThresholdSupermajority, commondao.ThresholdSimpleMajority}\n\n\tfor e := 0; e \u003c= len(pool); e++ {\n\t\tfor yes := 0; yes \u003c= e; yes++ {\n\t\t\tfor no := 0; no+yes \u003c= e; no++ {\n\t\t\t\tfor abstain := 0; abstain+no+yes \u003c= e; abstain++ {\n\t\t\t\t\tfor _, th := range thresholds {\n\t\t\t\t\t\tvar electorate addrset.Set\n\t\t\t\t\t\tfor i := 0; i \u003c e; i++ {\n\t\t\t\t\t\t\telectorate.Add(pool[i])\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tvar record commondao.VotingRecord\n\t\t\t\t\t\tidx := 0\n\t\t\t\t\t\tput := func(n int, c commondao.VoteChoice) {\n\t\t\t\t\t\t\tfor i := 0; i \u003c n; i++ {\n\t\t\t\t\t\t\t\tv, err := commondao.NewVote(pool[idx], c, \"\")\n\t\t\t\t\t\t\t\turequire.NoError(t, err)\n\t\t\t\t\t\t\t\trecord.AddVote(v)\n\t\t\t\t\t\t\t\tidx++\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tput(yes, commondao.ChoiceYes)\n\t\t\t\t\t\tput(no, commondao.ChoiceNo)\n\t\t\t\t\t\tput(abstain, commondao.ChoiceAbstain)\n\t\t\t\t\t\t// Remaining electorate members stay silent.\n\n\t\t\t\t\t\tgot := commondao.TallyDefault(record.Readonly(), electorate.Readonly(), th)\n\t\t\t\t\t\twant := refOutcome(e, yes, no, abstain, th)\n\t\t\t\t\t\turequire.Equal(t, int(want), int(got),\n\t\t\t\t\t\t\t\"TallyDefault disagreed with the reference formula at some (E, yes, no, abstain, threshold)\")\n\n\t\t\t\t\t\t// Invariant: pass and dismiss are mutually exclusive\n\t\t\t\t\t\t// (yes+no \u003c= D holds since yes+no+abstain \u003c= e).\n\t\t\t\t\t\td := e - abstain\n\t\t\t\t\t\tpassSuper := d \u003e 0 \u0026\u0026 3*yes \u003e= 2*d\n\t\t\t\t\t\tpassSimple := d \u003e 0 \u0026\u0026 2*yes \u003e d\n\t\t\t\t\t\tdismiss := d \u003e 0 \u0026\u0026 2*no \u003e d\n\t\t\t\t\t\tuassert.False(t, passSuper \u0026\u0026 dismiss, \"supermajority pass and dismiss both fired\")\n\t\t\t\t\t\tuassert.False(t, passSimple \u0026\u0026 dismiss, \"simple pass and dismiss both fired\")\n\n\t\t\t\t\t\t// Invariant: votes from outside the electorate never\n\t\t\t\t\t\t// change the outcome.\n\t\t\t\t\t\tsv, err := commondao.NewVote(stranger, commondao.ChoiceYes, \"\")\n\t\t\t\t\t\turequire.NoError(t, err)\n\t\t\t\t\t\trecord.AddVote(sv)\n\t\t\t\t\t\tgotWithStranger := commondao.TallyDefault(record.Readonly(), electorate.Readonly(), th)\n\t\t\t\t\t\tuassert.Equal(t, int(got), int(gotWithStranger), \"out-of-electorate vote changed the outcome\")\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n// TestTallyDefaultDGuard pins the load-bearing D\u003e0 guard: an all-abstain\n// or empty electorate never passes, even at a zero-YES tally.\nfunc TestTallyDefaultDGuard(t *testing.T) {\n\tvar empty addrset.Set\n\tvar record commondao.VotingRecord\n\tuassert.Equal(t, int(commondao.OutcomePending),\n\t\tint(commondao.TallyDefault(record.Readonly(), empty.Readonly(), commondao.ThresholdSupermajority)))\n\n\tvar electorate addrset.Set\n\telectorate.Add(memberA)\n\telectorate.Add(memberB)\n\tv, err := commondao.NewVote(memberA, commondao.ChoiceAbstain, \"\")\n\turequire.NoError(t, err)\n\trecord.AddVote(v)\n\tv, err = commondao.NewVote(memberB, commondao.ChoiceAbstain, \"\")\n\turequire.NoError(t, err)\n\trecord.AddVote(v)\n\t// D = 2 - 2 = 0: no YES, must stay pending (not a 3*0 \u003e= 2*0 pass).\n\tuassert.Equal(t, int(commondao.OutcomePending),\n\t\tint(commondao.TallyDefault(record.Readonly(), electorate.Readonly(), commondao.ThresholdSupermajority)))\n}\n\n// TestVotingRecordReVoteSameChoice pins that re-voting the identical choice\n// is idempotent: AddVote decrements the previous choice then increments the\n// new one, so a same-choice re-vote nets to zero — the counter must stay 1,\n// not double-count.\nfunc TestVotingRecordReVoteSameChoice(t *testing.T) {\n\tvar record commondao.VotingRecord\n\n\trecord.AddVote(mustNewVote(t, memberA, commondao.ChoiceYes))\n\tupdated := record.AddVote(mustNewVote(t, memberA, commondao.ChoiceYes))\n\n\tuassert.True(t, updated, \"expect a re-vote to be an update\")\n\tuassert.Equal(t, 1, record.Size())\n\tuassert.Equal(t, 1, record.VoteCount(commondao.ChoiceYes))\n}\n"},{"name":"z_commondao_execute_0_filetest.gno","body":"// PKGPATH: gno.land/r/test\npackage test\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/nt/commondao/v0\"\n)\n\nconst member address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\" // govdao t1 multisig\n\nvar (\n\tdao        *commondao.CommonDAO\n\tproposal   *commondao.Proposal\n\tsubValid   bool\n\tsubAddress address\n)\n\ntype testPropDef struct{}\n\nfunc (testPropDef) Title() string               { return \"\" }\nfunc (testPropDef) Body() string                { return \"\" }\nfunc (testPropDef) VotingPeriod() time.Duration { return time.Hour }\nfunc (testPropDef) Threshold() commondao.Threshold {\n\treturn commondao.ThresholdSupermajority\n}\n\nfunc (testPropDef) Executor() commondao.ExecFunc {\n\t// Executors are non-crossing: they must not rely on\n\t// unsafe.PreviousRealm() for realm identity (there is no crossing frame\n\t// for the executor). The DAO-scoped identity is the sub passed by the\n\t// host, which the executor can read and move value from.\n\treturn func(_ int, sub realm) error {\n\t\tsubValid = sub.Address().IsValid()\n\t\tsubAddress = sub.Address()\n\t\treturn nil\n\t}\n}\n\ntype testPropKind struct{}\n\nfunc (testPropKind) Name() string { return \"test\" }\nfunc (testPropKind) New(_ commondao.ReadonlyCommonDAO, _ any) (commondao.ProposalDefinition, error) {\n\treturn testPropDef{}, nil\n}\n\nfunc init() {\n\tdao = commondao.New(\n\t\tcommondao.WithCouncilMember(member),\n\t\tcommondao.WithProposalKind(testPropKind{}),\n\t)\n\tvar err error\n\tproposal, err = dao.Propose(member, \"test\", nil)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// A single YES from the one-member council passes the proposal early\n\tif err := dao.Vote(member, proposal.ID(), commondao.ChoiceYes, \"\"); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc main(cur realm) {\n\t// The host mints the DAO-scoped sub and passes it into Execute; the\n\t// executor runs with that sub as its value-movement authority.\n\tsub := cur.Sub(\"dao/1\")\n\terr := dao.Execute(proposal.ID(), sub)\n\n\tprintln(err == nil)\n\tprintln(string(proposal.Status()))\n\tprintln(subValid)\n\tprintln(subAddress == sub.Address())\n}\n\n// Output:\n// true\n// executed\n// true\n// true\n"},{"name":"z_commondao_execute_1_filetest.gno","body":"// PKGPATH: gno.land/r/test\npackage test\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"gno.land/p/nt/commondao/v0\"\n)\n\nconst member address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\" // govdao t1 multisig\n\nvar (\n\tdao      *commondao.CommonDAO\n\tproposal *commondao.Proposal\n)\n\ntype testPropDef struct{}\n\nfunc (testPropDef) Title() string               { return \"\" }\nfunc (testPropDef) Body() string                { return \"\" }\nfunc (testPropDef) VotingPeriod() time.Duration { return time.Hour }\nfunc (testPropDef) Threshold() commondao.Threshold {\n\treturn commondao.ThresholdSupermajority\n}\n\nfunc (testPropDef) Executor() commondao.ExecFunc {\n\treturn func(_ int, sub realm) error {\n\t\treturn errors.New(\"test error\")\n\t}\n}\n\ntype testPropKind struct{}\n\nfunc (testPropKind) Name() string { return \"test\" }\nfunc (testPropKind) New(_ commondao.ReadonlyCommonDAO, _ any) (commondao.ProposalDefinition, error) {\n\treturn testPropDef{}, nil\n}\n\nfunc init() {\n\tdao = commondao.New(\n\t\tcommondao.WithCouncilMember(member),\n\t\tcommondao.WithProposalKind(testPropKind{}),\n\t)\n\tvar err error\n\tproposal, err = dao.Propose(member, \"test\", nil)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// A single YES from the one-member council passes the proposal early\n\tif err := dao.Vote(member, proposal.ID(), commondao.ChoiceYes, \"\"); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc main(cur realm) {\n\terr := dao.Execute(proposal.ID(), cur.Sub(\"dao/1\"))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tprintln(string(proposal.Status()))\n\tprintln(proposal.StatusReason())\n}\n\n// Output:\n// failed\n// test error\n"},{"name":"z_commondao_execute_2_filetest.gno","body":"// PKGPATH: gno.land/r/test\npackage test\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/nt/commondao/v0\"\n)\n\nconst member address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\" // govdao t1 multisig\n\nvar (\n\tdao      *commondao.CommonDAO\n\tproposal *commondao.Proposal\n\texecuted bool\n)\n\ntype testPropDef struct{}\n\nfunc (testPropDef) Title() string                  { return \"\" }\nfunc (testPropDef) Body() string                   { return \"\" }\nfunc (testPropDef) VotingPeriod() time.Duration    { return time.Hour } // Voting ends in 1 hour\nfunc (testPropDef) Threshold() commondao.Threshold { return commondao.ThresholdSupermajority }\n\nfunc (testPropDef) Executor() commondao.ExecFunc {\n\treturn func(_ int, sub realm) error {\n\t\texecuted = true\n\t\treturn nil\n\t}\n}\n\ntype testPropKind struct{}\n\nfunc (testPropKind) Name() string { return \"test\" }\nfunc (testPropKind) New(_ commondao.ReadonlyCommonDAO, _ any) (commondao.ProposalDefinition, error) {\n\treturn testPropDef{}, nil\n}\n\nfunc init() {\n\tdao = commondao.New(\n\t\tcommondao.WithCouncilMember(member),\n\t\tcommondao.WithProposalKind(testPropKind{}),\n\t)\n\tvar err error\n\tproposal, err = dao.Propose(member, \"test\", nil)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc main(cur realm) {\n\t// A supermajority of YES decides the proposal immediately\n\terr := dao.Vote(member, proposal.ID(), commondao.ChoiceYes, \"\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tprintln(string(proposal.Status()))\n\n\t// Proposals decided early can be executed before the voting deadline.\n\t// The host mints the DAO-scoped sub and passes it into Execute.\n\terr = dao.Execute(proposal.ID(), cur.Sub(\"dao/1\"))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tprintln(executed)\n\tprintln(string(proposal.Status()))\n}\n\n// Output:\n// passed\n// true\n// executed\n"},{"name":"z_commondao_execute_3_filetest.gno","body":"// PKGPATH: gno.land/r/test\npackage test\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/nt/commondao/v0\"\n)\n\nconst member address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\" // govdao t1 multisig\n\nvar (\n\tdao                  *commondao.CommonDAO\n\tproposal             *commondao.Proposal\n\tactiveSizeDuringExec int\n\texecCount            int\n)\n\n// probePropDef observes, from inside its own executor, whether its proposal\n// is still in active storage. Execute removes the proposal from active\n// storage BEFORE running Validate/the executor (remove-before-run); that is\n// exactly what makes a re-entrant Execute of the same proposal impossible\n// (it would find nothing in active storage). So during the executor the\n// active-proposal count must already be 0. Deleting that removal would\n// leave the count at 1 — a re-entrancy hole.\ntype probePropDef struct{}\n\nfunc (probePropDef) Title() string               { return \"\" }\nfunc (probePropDef) Body() string                { return \"\" }\nfunc (probePropDef) VotingPeriod() time.Duration { return time.Hour }\nfunc (probePropDef) Threshold() commondao.Threshold {\n\treturn commondao.ThresholdSupermajority\n}\n\nfunc (probePropDef) Executor() commondao.ExecFunc {\n\treturn func(_ int, sub realm) error {\n\t\texecCount++\n\t\tactiveSizeDuringExec = dao.ActiveProposalsSize()\n\t\treturn nil\n\t}\n}\n\ntype probePropKind struct{}\n\nfunc (probePropKind) Name() string { return \"probe\" }\nfunc (probePropKind) New(_ commondao.ReadonlyCommonDAO, _ any) (commondao.ProposalDefinition, error) {\n\treturn probePropDef{}, nil\n}\n\nfunc init() {\n\tdao = commondao.New(\n\t\tcommondao.WithCouncilMember(member),\n\t\tcommondao.WithProposalKind(probePropKind{}),\n\t)\n\tvar err error\n\tproposal, err = dao.Propose(member, \"probe\", nil)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t// A single YES from the one-member council passes the proposal early.\n\tif err := dao.Vote(member, proposal.ID(), commondao.ChoiceYes, \"\"); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc main(cur realm) {\n\terr := dao.Execute(proposal.ID(), cur.Sub(\"dao/1\"))\n\n\tprintln(\"outer err nil:\", err == nil)\n\tprintln(\"status:\", string(proposal.Status()))\n\tprintln(\"executor ran once:\", execCount == 1)\n\tprintln(\"active count during executor:\", activeSizeDuringExec)\n}\n\n// Output:\n// outer err nil: true\n// status: executed\n// executor ran once: true\n// active count during executor: 0\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"H8LLiKr4fms8Gn6FVwI4F5JHT3ffankqe3OtXHNFlEEJbSbI7/J9XxmecbBiUbvpubj4TWXFS4rRBIoO2YQY2w=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"grc20","path":"gno.land/p/nt/grc20/v0","files":[{"name":"caller_teller_sub_realm_filetest.gno","body":"// PKGPATH: gno.land/r/demo/grc20subrealm\n\n// A frame-relative teller must keep working when the token's OWN realm\n// operates under an identity minted by its own cur.Sub(). The sub's synthesized\n// pkgpath is \"\u003chost\u003e#vault\", which is not origRealm verbatim, so a raw string\n// comparison would refuse the token's own realm — a false positive that would\n// break the one property the home binding promises unconditionally.\npackage grc20subrealm\n\nimport (\n\t\"chain\"\n\n\t\"gno.land/p/nt/grc20/v0\"\n)\n\nvar (\n\ttoken  *grc20.Token\n\tledger *grc20.PrivateLedger\n\tteller grc20.Teller\n)\n\nfunc init(cur realm) {\n\ttoken, ledger = grc20.NewToken(\"SubRealm\", \"SUBR\", 4, 0, cur)\n\tteller = ledger.CallerTeller()\n}\n\nfunc main(cur realm) {\n\tpayer := cur.Previous().Address() // whom the frame-relative teller debits\n\tbob := chain.PackageAddress(\"bob\")\n\tledger.Mint(payer, 1_000)\n\n\t// Control: the primary identity moves the caller's funds.\n\tif err := teller.Transfer(0, cur, bob, 100); err != nil {\n\t\tpanic(\"primary identity rejected: \" + err.Error())\n\t}\n\n\t// Same realm, same teller, under this realm's own \"vault\" sub identity.\n\tif err := teller.Transfer(0, cur.Sub(\"vault\"), bob, 100); err != nil {\n\t\tpanic(\"own sub identity rejected: \" + err.Error())\n\t}\n\n\tif got := token.BalanceOf(bob); got != 200 {\n\t\tpanic(\"unexpected bob balance\")\n\t}\n\n\t// The mirror image: a token CREATED from a sub frame must still be usable\n\t// from its host realm. origRealm drops the subpath at construction, so the\n\t// token belongs to the host rather than being stranded in the sub.\n\tsubTok, subLedger := grc20.NewToken(\"FromSub\", \"FSUB\", 4, 1, cur.Sub(\"vault\"))\n\tsubTeller := subLedger.CallerTeller()\n\tsubLedger.Mint(payer, 500)\n\tif err := subTeller.Transfer(0, cur, bob, 50); err != nil {\n\t\tpanic(\"host realm rejected for a sub-created token: \" + err.Error())\n\t}\n\tif got := subTok.BalanceOf(bob); got != 50 {\n\t\tpanic(\"unexpected bob balance on the sub-created token\")\n\t}\n\n\tprintln(\"ok\")\n}\n\n// Output:\n// ok\n"},{"name":"event_provenance_filetest.gno","body":"// PKGPATH: gno.land/r/demo/grc20provenance\n\n// This filetest is the reference for how an indexer attributes a grc20 event to\n// the realm that issued the token. It is the cross-realm counterpart to\n// newtoken_event_filetest.gno, which covers the within-realm duplicate signal.\n//\n// Two fields matter, and neither is sufficient alone:\n//\n//   - pkg_path is stamped by the VM from the package whose code calls\n//     chain.Emit. Every genuine grc20 event therefore carries\n//     gno.land/p/nt/grc20/v0 — a constant, so it identifies the library\n//     but never the token.\n//   - the \"token\" attribute carries Token.ID(), whose leading component is\n//     rlm.PkgPath() captured under IsCurrent() in NewToken. A realm cannot\n//     produce a Token whose id claims a different realm.\n//\n// Chained, they are sufficient: pkg_path proves the event came from grc20's\n// code, and grc20's code only ever writes an IsCurrent-verified realm prefix\n// into \"token\". So for any event bearing grc20's pkg_path, the realm prefix in\n// \"token\" is unforgeable.\n//\n// The realm below is an impostor trying to pass its own transfers off as\n// another realm's token, by both routes available to it, and failing at both.\npackage grc20provenance\n\nimport (\n\t\"chain\"\n\n\t\"gno.land/p/nt/grc20/v0\"\n)\n\n// Stand-in for a token issued and registered by some other realm. Written as a\n// literal rather than imported: a filetest under a /p/ package that imports an\n// /r/ realm which itself imports that /p/ package forms a genesis dependency\n// cycle (grc20 -\u003e realm -\u003e grc20), which breaks package loading chain-wide.\nconst canonical = \"gno.land/r/demo/canonicaltoken.FOO.0000000\"\n\nfunc main(cur realm) {\n\tprintln(\"canonical id:\", canonical)\n\n\t// Route 1: ask grc20 for a token using the canonical token's own\n\t// name/symbol/decimals/id. NewToken derives the id prefix from the live\n\t// crossing frame, so the id names *this* realm. No argument changes that.\n\timpostor, impostorLedger := grc20.NewToken(\"Foo\", \"FOO\", 4, 0, cur)\n\tprintln(\"impostor id: \", impostor.ID())\n\tprintln(\"impostor claims canonical id:\", impostor.ID() == canonical)\n\n\t// A genuine grc20 event from the impostor. It carries grc20's pkg_path —\n\t// identical to the canonical token's events — but the \"token\" prefix is\n\t// this realm, so an indexer attributes it here.\n\timpostorLedger.Mint(cur.Address(), 999_999)\n\n\t// Route 2: skip grc20 and hand-emit an event carrying the canonical id.\n\t// The attributes are fully attacker-chosen, but pkg_path is not: the VM\n\t// stamps this realm. An indexer that accepts Transfer only from grc20's\n\t// pkg_path drops this event.\n\tchain.Emit(\n\t\t\"Transfer\",\n\t\t\"token\", canonical,\n\t\t\"from\", \"\",\n\t\t\"to\", cur.Address().String(),\n\t\t\"value\", \"999999\",\n\t)\n}\n\n// Output:\n// canonical id: gno.land/r/demo/canonicaltoken.FOO.0000000\n// impostor id:  gno.land/r/demo/grc20provenance.FOO.0000000\n// impostor claims canonical id: false\n\n// Events:\n// [\n//   {\n//     \"type\": \"NewToken\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20provenance.FOO.0000000\"\n//       },\n//       {\n//         \"key\": \"name\",\n//         \"value\": \"Foo\"\n//       },\n//       {\n//         \"key\": \"symbol\",\n//         \"value\": \"FOO\"\n//       },\n//       {\n//         \"key\": \"decimals\",\n//         \"value\": \"4\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v0\"\n//   },\n//   {\n//     \"type\": \"Transfer\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20provenance.FOO.0000000\"\n//       },\n//       {\n//         \"key\": \"from\",\n//         \"value\": \"\"\n//       },\n//       {\n//         \"key\": \"to\",\n//         \"value\": \"g1vz4y807zq7p3s63dmyng9g80l58nmuc3lgmfjs\"\n//       },\n//       {\n//         \"key\": \"value\",\n//         \"value\": \"999999\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v0\"\n//   },\n//   {\n//     \"type\": \"Transfer\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/canonicaltoken.FOO.0000000\"\n//       },\n//       {\n//         \"key\": \"from\",\n//         \"value\": \"\"\n//       },\n//       {\n//         \"key\": \"to\",\n//         \"value\": \"g1vz4y807zq7p3s63dmyng9g80l58nmuc3lgmfjs\"\n//       },\n//       {\n//         \"key\": \"value\",\n//         \"value\": \"999999\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/r/demo/grc20provenance\"\n//   }\n// ]\n"},{"name":"examples_test.gno","body":"package grc20\n\n// XXX: write Examples\n\nfunc ExampleInit()                            {}\nfunc ExampleExposeBankForMaketxRunOrImports() {}\nfunc ExampleCustomTellerImpl()                {}\nfunc ExampleAllowance()                       {}\nfunc ExampleRealmBanker()                     {}\nfunc ExamplePreviousRealmBanker()             {}\nfunc ExampleAccountBanker()                   {}\nfunc ExampleTransfer()                        {}\nfunc ExampleApprove()                         {}\nfunc ExampleTransferFrom()                    {}\nfunc ExampleMint()                            {}\nfunc ExampleBurn()                            {}\n\n// ...\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/grc20/v0\"\ngno = \"0.9\"\n"},{"name":"mock.gno","body":"package grc20\n\n// XXX: func Mock(t *Token)\n"},{"name":"newtoken_event_filetest.gno","body":"// PKGPATH: gno.land/r/demo/grc20dupsignal\n\n// This filetest is the reference for the detection rule the NewToken event\n// enables. The realm below reuses seqid 0 twice, so both tokens end up with the\n// same Token.ID() and their Transfer events are indistinguishable — the\n// long-standing event-provenance ambiguity.\n//\n// What changes is that the ambiguity is now *announced*. Two NewToken events\n// carry the same \"token\" value, which is a complete signal: NewToken is the only\n// way a Token can exist (Token's fields are unexported), so an indexer watching\n// this event sees every token that will ever emit. On the second duplicate\n// announcement it should flag the realm and stop trusting its token events.\npackage grc20dupsignal\n\nimport (\n\t\"gno.land/p/nt/grc20/v0\"\n)\n\nfunc main(cur realm) {\n\tfirst, firstLedger := grc20.NewToken(\"Same\", \"DUP\", 6, 0, cur)\n\tsecond, secondLedger := grc20.NewToken(\"Same\", \"DUP\", 6, 0, cur)\n\n\tprintln(\"same id:\", first.ID() == second.ID())\n\n\t// Two independent ledgers, one identifier: these Transfers cannot be\n\t// attributed to either object from the event stream alone.\n\tfirstLedger.Mint(cur.Address(), 1)\n\tsecondLedger.Mint(cur.Address(), 999999)\n}\n\n// Output:\n// same id: true\n\n// Events:\n// [\n//   {\n//     \"type\": \"NewToken\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20dupsignal.DUP.0000000\"\n//       },\n//       {\n//         \"key\": \"name\",\n//         \"value\": \"Same\"\n//       },\n//       {\n//         \"key\": \"symbol\",\n//         \"value\": \"DUP\"\n//       },\n//       {\n//         \"key\": \"decimals\",\n//         \"value\": \"6\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v0\"\n//   },\n//   {\n//     \"type\": \"NewToken\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20dupsignal.DUP.0000000\"\n//       },\n//       {\n//         \"key\": \"name\",\n//         \"value\": \"Same\"\n//       },\n//       {\n//         \"key\": \"symbol\",\n//         \"value\": \"DUP\"\n//       },\n//       {\n//         \"key\": \"decimals\",\n//         \"value\": \"6\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v0\"\n//   },\n//   {\n//     \"type\": \"Transfer\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20dupsignal.DUP.0000000\"\n//       },\n//       {\n//         \"key\": \"from\",\n//         \"value\": \"\"\n//       },\n//       {\n//         \"key\": \"to\",\n//         \"value\": \"g1k29lxz0d8gx7m94n032ulum875p7gg0kduus5m\"\n//       },\n//       {\n//         \"key\": \"value\",\n//         \"value\": \"1\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v0\"\n//   },\n//   {\n//     \"type\": \"Transfer\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20dupsignal.DUP.0000000\"\n//       },\n//       {\n//         \"key\": \"from\",\n//         \"value\": \"\"\n//       },\n//       {\n//         \"key\": \"to\",\n//         \"value\": \"g1k29lxz0d8gx7m94n032ulum875p7gg0kduus5m\"\n//       },\n//       {\n//         \"key\": \"value\",\n//         \"value\": \"999999\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v0\"\n//   }\n// ]\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).\n//\n// SECURITY: this accessor hangs off *PrivateLedger, not *Token, and that is\n// load-bearing. A frame-relative teller debits whoever crossed into the realm\n// holding it, so it is only ever meaningful inside the token's own realm,\n// whose wrappers act for a caller who knowingly invoked the token. Anywhere\n// else it is a confused deputy. The *Token pointer is published (exported\n// vars, grc20factory, grc20reg) but the ledger is not — NewToken hands it to\n// the creating realm and nowhere else — so a foreign realm cannot mint one.\n//\n// Construction privacy alone is not enough: a realm may legally build a teller\n// and then export the VALUE. The write methods therefore also verify that the\n// invoking realm is the token's own (see guardHome), which makes a leaked\n// teller inert everywhere but home.\nfunc (ledger *PrivateLedger) CallerTeller() Teller {\n\tif ledger == nil {\n\t\tpanic(\"Ledger 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\thomeGuard: true,\n\t\tToken:     ledger.token,\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\n// guardHome confines a frame-relative (homeGuard) teller to the token's own\n// realm. Construction privacy stops a foreign realm from minting one;\n// this stops a minted one from travelling, which is what happens when a realm\n// legally builds a teller and then exports the value.\n//\n// The check is on the invoking realm's path alone — deliberately NOT on\n// whether the resolved actor is an end user. Keying on the actor only blocks\n// the case where the debited party is the signing user, which leaves two doors\n// open: a realm can be charged by a realm it calls, and TransferFrom resolves\n// the *spender* from the frame, so a realm reached from an honest hub spends\n// that hub's allowance against any owner who granted one. Both are the same\n// defect as the original one level up — frame-relative resolution means\n// whoever you call can act as you — and neither is reachable once the teller\n// only works at home.\n//\n// The host is compared after stripping any \"#subpath\" synthesized by\n// realm.Sub, so the token's own sub-realms are not falsely rejected.\n//\n// A foreign realm that needs to move a user's funds uses the ordinary route:\n// Approve, then RealmTeller().TransferFrom, which is eagerly bound to that\n// realm's own address and allowance-gated.\n//\n// The leading int keeps this a plain method.\nfunc (ft *fnTeller) guardHome(_ int, rlm realm) error {\n\tif !ft.homeGuard {\n\t\treturn nil\n\t}\n\thost, _, _ := chain.SplitPkgSubPath(rlm.PkgPath())\n\tif host != ft.Token.origRealm {\n\t\treturn ErrForeignCallerTeller\n\t}\n\treturn nil\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\tif err := ft.guardHome(0, rlm); err != nil {\n\t\treturn err\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\tif err := ft.guardHome(0, rlm); err != nil {\n\t\treturn err\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\tif err := ft.guardHome(0, rlm); err != nil {\n\t\treturn err\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":"tellers_test.gno","body":"package grc20\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestCallerTellerImpl(cur realm, t *testing.T) {\n\ttok, ledger := newTestToken(\"Dummy\", \"DUMMY\", 4, 0, cur)\n\tteller := ledger.CallerTeller()\n\turequire.False(t, tok == nil)\n\tvar _ Teller = teller\n}\n\n// The sub-realm case (guardHome must not lock out the token's own realm when it\n// operates under an identity minted by its own cur.Sub()) needs a real realm\n// frame, since cur.Sub() refuses to mint from a p/ package. It lives in\n// filetests/caller_teller_sub_realm_filetest.gno.\n\n// evilWrap embeds Teller; the seal-marker pattern (if we had one) would be\n// satisfied via promotion, but IsCanonicalTeller rejects it because *evilWrap\n// is nominally distinct from *fnTeller.\ntype evilWrap struct {\n\tTeller\n}\n\nfunc TestIsCanonicalTeller(cur realm, t *testing.T) {\n\t_, ledger := newTestToken(\"Dummy\", \"DUMMY\", 4, 0, cur)\n\tlegit := ledger.CallerTeller()\n\n\tuassert.True(t, IsCanonicalTeller(legit),\n\t\t\"canonical *fnTeller from CallerTeller must pass\")\n\n\tevil := \u0026evilWrap{Teller: legit}\n\tuassert.False(t, IsCanonicalTeller(evil),\n\t\t\"foreign type embedding a canonical Teller must be rejected\")\n}\n\nfunc TestTeller(cur realm, t *testing.T) {\n\tvar (\n\t\talice = testutils.TestAddress(\"alice\")\n\t\tbob   = testutils.TestAddress(\"bob\")\n\t\tcarl  = testutils.TestAddress(\"carl\")\n\t)\n\n\ttoken, ledger := newTestToken(\"Dummy\", \"DUMMY\", 6, 0, cur)\n\n\tcheckBalances := func(aliceEB, bobEB, carlEB int64) {\n\t\tt.Helper()\n\t\texp := ufmt.Sprintf(\"alice=%d bob=%d carl=%d\", aliceEB, bobEB, carlEB)\n\t\taliceGB := token.BalanceOf(alice)\n\t\tbobGB := token.BalanceOf(bob)\n\t\tcarlGB := token.BalanceOf(carl)\n\t\tgot := ufmt.Sprintf(\"alice=%d bob=%d carl=%d\", aliceGB, bobGB, carlGB)\n\t\tuassert.Equal(t, got, exp, \"invalid balances\")\n\t}\n\tcheckAllowances := func(abEB, acEB, baEB, bcEB, caEB, cbEB int64) {\n\t\tt.Helper()\n\t\texp := ufmt.Sprintf(\"ab=%d ac=%d ba=%d bc=%d ca=%d cb=%s\", abEB, acEB, baEB, bcEB, caEB, cbEB)\n\t\tabGB := token.Allowance(alice, bob)\n\t\tacGB := token.Allowance(alice, carl)\n\t\tbaGB := token.Allowance(bob, alice)\n\t\tbcGB := token.Allowance(bob, carl)\n\t\tcaGB := token.Allowance(carl, alice)\n\t\tcbGB := token.Allowance(carl, bob)\n\t\tgot := ufmt.Sprintf(\"ab=%d ac=%d ba=%d bc=%d ca=%d cb=%s\", abGB, acGB, baGB, bcGB, caGB, cbGB)\n\t\tuassert.Equal(t, got, exp, \"invalid allowances\")\n\t}\n\n\tcheckBalances(0, 0, 0)\n\tcheckAllowances(0, 0, 0, 0, 0, 0)\n\n\turequire.NoError(t, ledger.Mint(alice, 1000))\n\turequire.NoError(t, ledger.Mint(alice, 100))\n\tcheckBalances(1100, 0, 0)\n\tcheckAllowances(0, 0, 0, 0, 0, 0)\n\n\turequire.NoError(t, ledger.Approve(alice, bob, 99999999))\n\tcheckBalances(1100, 0, 0)\n\tcheckAllowances(99999999, 0, 0, 0, 0, 0)\n\n\turequire.NoError(t, ledger.Approve(alice, bob, 400))\n\tcheckBalances(1100, 0, 0)\n\tcheckAllowances(400, 0, 0, 0, 0, 0)\n\n\turequire.Error(t, ledger.TransferFrom(alice, bob, carl, 100000000))\n\tcheckBalances(1100, 0, 0)\n\tcheckAllowances(400, 0, 0, 0, 0, 0)\n\n\turequire.NoError(t, ledger.TransferFrom(alice, bob, carl, 100))\n\tcheckBalances(1000, 0, 100)\n\tcheckAllowances(300, 0, 0, 0, 0, 0)\n\n\turequire.Error(t, ledger.SpendAllowance(alice, bob, 2000000))\n\tcheckBalances(1000, 0, 100)\n\tcheckAllowances(300, 0, 0, 0, 0, 0)\n\n\turequire.NoError(t, ledger.SpendAllowance(alice, bob, 100))\n\tcheckBalances(1000, 0, 100)\n\tcheckAllowances(200, 0, 0, 0, 0, 0)\n}\n\nfunc TestCallerTeller(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\tcarl := testutils.TestAddress(\"carl\")\n\n\ttoken, ledger := newTestToken(\"Dummy\", \"DUMMY\", 6, 0, cur)\n\tteller := ledger.CallerTeller()\n\n\tcheckBalances := func(aliceEB, bobEB, carlEB int64) {\n\t\tt.Helper()\n\t\texp := ufmt.Sprintf(\"alice=%d bob=%d carl=%d\", aliceEB, bobEB, carlEB)\n\t\taliceGB := token.BalanceOf(alice)\n\t\tbobGB := token.BalanceOf(bob)\n\t\tcarlGB := token.BalanceOf(carl)\n\t\tgot := ufmt.Sprintf(\"alice=%d bob=%d carl=%d\", aliceGB, bobGB, carlGB)\n\t\tuassert.Equal(t, got, exp, \"invalid balances\")\n\t}\n\tcheckAllowances := func(abEB, acEB, baEB, bcEB, caEB, cbEB int64) {\n\t\tt.Helper()\n\t\texp := ufmt.Sprintf(\"ab=%d ac=%d ba=%d bc=%d ca=%d cb=%s\", abEB, acEB, baEB, bcEB, caEB, cbEB)\n\t\tabGB := token.Allowance(alice, bob)\n\t\tacGB := token.Allowance(alice, carl)\n\t\tbaGB := token.Allowance(bob, alice)\n\t\tbcGB := token.Allowance(bob, carl)\n\t\tcaGB := token.Allowance(carl, alice)\n\t\tcbGB := token.Allowance(carl, bob)\n\t\tgot := ufmt.Sprintf(\"ab=%d ac=%d ba=%d bc=%d ca=%d cb=%s\", abGB, acGB, baGB, bcGB, caGB, cbGB)\n\t\tuassert.Equal(t, got, exp, \"invalid allowances\")\n\t}\n\n\turequire.NoError(t, ledger.Mint(alice, 1000))\n\tcheckBalances(1000, 0, 0)\n\tcheckAllowances(0, 0, 0, 0, 0, 0)\n\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\tfunc(cur realm) { urequire.NoError(t, teller.Approve(0, cur, bob, 600)) }(cross(cur))\n\tcheckBalances(1000, 0, 0)\n\tcheckAllowances(600, 0, 0, 0, 0, 0)\n\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\tfunc(cur realm) { urequire.Error(t, teller.TransferFrom(0, cur, alice, carl, 700)) }(cross(cur))\n\tcheckBalances(1000, 0, 0)\n\tcheckAllowances(600, 0, 0, 0, 0, 0)\n\tfunc(cur realm) { urequire.NoError(t, teller.TransferFrom(0, cur, alice, carl, 400)) }(cross(cur))\n\tcheckBalances(600, 0, 400)\n\tcheckAllowances(200, 0, 0, 0, 0, 0)\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, with any realm.Sub subpath\n// stripped — becomes the Token's origRealm. Token.ID() keeps the raw\n// rlm.PkgPath(), and returns it + \".\" + 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 the\n// rlmPath.symbol prefix of Token.ID(), without its trailing 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\t// origRealm drops any realm.Sub subpath: a token created while its realm\n\t// operates under a sub identity still belongs to the host realm. guardHome\n\t// resolves the invoking host the same way, so storing the raw path here\n\t// would pin the token to that sub and lock its own realm out for good.\n\torigRealm, _, _ := chain.SplitPkgSubPath(pkgPath)\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\torigRealm: origRealm,\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 creating realm's raw pkgpath — including any\n// realm.Sub subpath, unlike origRealm — 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 owner == to {\n\t\treturn ErrCannotTransferToSelf\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":"token_identity_filetest.gno","body":"// PKGPATH: gno.land/r/demo/grc20identity\n\npackage grc20identity\n\nimport (\n\t\"chain\"\n\n\t\"gno.land/p/nt/grc20/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n)\n\nvar nextTokenID seqid.ID\n\nfunc main(cur realm) {\n\tfirst, firstLedger := newToken(cur)\n\tsecond, secondLedger := newToken(cur)\n\n\tif first.ID() != \"gno.land/r/demo/grc20identity.DUP.0000001\" {\n\t\tpanic(\"unexpected first token ID: \" + first.ID())\n\t}\n\tif second.ID() != \"gno.land/r/demo/grc20identity.DUP.0000002\" {\n\t\tpanic(\"unexpected second token ID: \" + second.ID())\n\t}\n\n\tholder := chain.PackageAddress(\"holder\")\n\tspender := chain.PackageAddress(\"spender\")\n\tfirstLedger.Mint(holder, 1)\n\tfirstLedger.Approve(holder, spender, 1)\n\tsecondLedger.Mint(holder, 2)\n\tsecondLedger.Approve(holder, spender, 2)\n}\n\nfunc newToken(cur realm) (*grc20.Token, *grc20.PrivateLedger) {\n\treturn grc20.NewToken(\"Same\", \"DUP\", 6, nextTokenID.Next(), cur)\n}\n\n// Events:\n// [\n//   {\n//     \"type\": \"NewToken\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20identity.DUP.0000001\"\n//       },\n//       {\n//         \"key\": \"name\",\n//         \"value\": \"Same\"\n//       },\n//       {\n//         \"key\": \"symbol\",\n//         \"value\": \"DUP\"\n//       },\n//       {\n//         \"key\": \"decimals\",\n//         \"value\": \"6\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v0\"\n//   },\n//   {\n//     \"type\": \"NewToken\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20identity.DUP.0000002\"\n//       },\n//       {\n//         \"key\": \"name\",\n//         \"value\": \"Same\"\n//       },\n//       {\n//         \"key\": \"symbol\",\n//         \"value\": \"DUP\"\n//       },\n//       {\n//         \"key\": \"decimals\",\n//         \"value\": \"6\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v0\"\n//   },\n//   {\n//     \"type\": \"Transfer\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20identity.DUP.0000001\"\n//       },\n//       {\n//         \"key\": \"from\",\n//         \"value\": \"\"\n//       },\n//       {\n//         \"key\": \"to\",\n//         \"value\": \"g18rwjsdf0kcy673ta0q4ujwvsncz4k2a8gcv94t\"\n//       },\n//       {\n//         \"key\": \"value\",\n//         \"value\": \"1\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v0\"\n//   },\n//   {\n//     \"type\": \"Approval\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20identity.DUP.0000001\"\n//       },\n//       {\n//         \"key\": \"owner\",\n//         \"value\": \"g18rwjsdf0kcy673ta0q4ujwvsncz4k2a8gcv94t\"\n//       },\n//       {\n//         \"key\": \"spender\",\n//         \"value\": \"g148tp8xkvvk3l73lmxywpdlsxgdujjlclx7wfyg\"\n//       },\n//       {\n//         \"key\": \"value\",\n//         \"value\": \"1\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v0\"\n//   },\n//   {\n//     \"type\": \"Transfer\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20identity.DUP.0000002\"\n//       },\n//       {\n//         \"key\": \"from\",\n//         \"value\": \"\"\n//       },\n//       {\n//         \"key\": \"to\",\n//         \"value\": \"g18rwjsdf0kcy673ta0q4ujwvsncz4k2a8gcv94t\"\n//       },\n//       {\n//         \"key\": \"value\",\n//         \"value\": \"2\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v0\"\n//   },\n//   {\n//     \"type\": \"Approval\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc20identity.DUP.0000002\"\n//       },\n//       {\n//         \"key\": \"owner\",\n//         \"value\": \"g18rwjsdf0kcy673ta0q4ujwvsncz4k2a8gcv94t\"\n//       },\n//       {\n//         \"key\": \"spender\",\n//         \"value\": \"g148tp8xkvvk3l73lmxywpdlsxgdujjlclx7wfyg\"\n//       },\n//       {\n//         \"key\": \"value\",\n//         \"value\": \"2\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc20/v0\"\n//   }\n// ]\n"},{"name":"token_test.gno","body":"package grc20\n\nimport (\n\t\"errors\"\n\t\"math\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\n// newTestToken constructs a Token. The IIFE same-realm cross\n// promotes the test's EOA-origin cur into a fresh /p/grc20\n// CodeRealm cur so NewToken's IsCurrent + non-empty rlm.PkgPath()\n// checks pass.\nfunc newTestToken(name, symbol string, decimals int, id seqid.ID, rlm realm) (tok *Token, adm *PrivateLedger) {\n\tfunc(cur realm) {\n\t\ttok, adm = NewToken(name, symbol, decimals, id, cur)\n\t}(cross(rlm))\n\treturn\n}\n\nfunc TestTestImpl(cur realm, t *testing.T) {\n\tbank, _ := newTestToken(\"Dummy\", \"DUMMY\", 4, 0, cur)\n\turequire.False(t, bank == nil, \"dummy should not be nil\")\n}\n\nfunc TestNewTokenAllowsDuplicateSymbolInSameRealm(cur realm, t *testing.T) {\n\tholder := testutils.TestAddress(\"holder\")\n\n\tfirst, firstLedger := newTestToken(\"Same\", \"DUP\", 6, 1, cur)\n\tsecond, secondLedger := newTestToken(\"Same\", \"DUP\", 6, 2, cur)\n\n\turequire.True(t, first.ID() != second.ID(), \"duplicate symbols should not force duplicate IDs\")\n\n\turequire.NoError(t, firstLedger.Mint(holder, 11))\n\turequire.NoError(t, secondLedger.Mint(holder, 22))\n\turequire.Equal(t, int64(11), first.BalanceOf(holder))\n\turequire.Equal(t, int64(22), second.BalanceOf(holder))\n}\n\nfunc TestNewTokenValidation(cur realm, t *testing.T) {\n\t// Wrap NewToken to satisfy IsCurrent and pkgPath requirements,\n\t// then validate name, symbol, and decimals.\n\tmustPanic := func(name, sym string, dec int, want error, label string) {\n\t\tt.Helper()\n\t\t// newTestToken wraps NewToken in cross(...), so the panic from\n\t\t// NewToken's validators crosses a realm boundary — use revive()\n\t\t// (defer-recover doesn't see cross-realm panics).\n\t\tr := revive(func() {\n\t\t\tnewTestToken(name, sym, dec, 0, cur)\n\t\t})\n\t\tif r == nil {\n\t\t\tt.Errorf(\"%s: expected panic, got none\", label)\n\t\t\treturn\n\t\t}\n\t\tif r != want {\n\t\t\tt.Errorf(\"%s: expected %v, got %v\", label, want, r)\n\t\t}\n\t}\n\n\t// Empty name / symbol.\n\tmustPanic(\"\", \"OK\", 4, ErrInvalidName, \"empty name\")\n\tmustPanic(\"Name\", \"\", 4, ErrInvalidSymbol, \"empty symbol\")\n\n\t// Length caps.\n\tmustPanic(strings.Repeat(\"a\", MaxNameLen+1), \"OK\", 4, ErrInvalidName, \"name too long\")\n\tmustPanic(\"Name\", strings.Repeat(\"A\", MaxSymbolLen+1), 4, ErrInvalidSymbol, \"symbol too long\")\n\n\t// Name control characters.\n\tmustPanic(\"bad\\x01name\", \"OK\", 4, ErrInvalidName, \"name with control char\")\n\tmustPanic(\"bad\\nname\", \"OK\", 4, ErrInvalidName, \"name with newline\")\n\n\t// Symbol charset — disallowed delimiters and whitespace.\n\tmustPanic(\"Name\", \"BA.D\", 4, ErrInvalidSymbol, \"symbol with dot\")\n\tmustPanic(\"Name\", \"BA/D\", 4, ErrInvalidSymbol, \"symbol with slash\")\n\tmustPanic(\"Name\", \"BA D\", 4, ErrInvalidSymbol, \"symbol with space\")\n\tmustPanic(\"Name\", \"BA\\\"D\", 4, ErrInvalidSymbol, \"symbol with quote\")\n\n\t// Decimals out of range.\n\tmustPanic(\"Name\", \"OK\", -1, ErrInvalidDecimals, \"negative decimals\")\n\tmustPanic(\"Name\", \"OK\", MaxDecimals+1, ErrInvalidDecimals, \"decimals over cap\")\n\n\t// Boundary positives — should NOT panic.\n\ttok, _ := newTestToken(strings.Repeat(\"a\", MaxNameLen), strings.Repeat(\"A\", MaxSymbolLen), MaxDecimals, 0, cur)\n\turequire.True(t, tok != nil, \"boundary name+symbol+decimals should succeed\")\n\n\t// UTF-8 name with non-ASCII is allowed.\n\ttok2, _ := newTestToken(\"Доллар\", \"RUB\", 2, 0, cur)\n\turequire.True(t, tok2 != nil, \"UTF-8 name should be allowed\")\n}\n\nfunc TestToken(cur realm, t *testing.T) {\n\tvar (\n\t\talice = testutils.TestAddress(\"alice\")\n\t\tbob   = testutils.TestAddress(\"bob\")\n\t\tcarl  = testutils.TestAddress(\"carl\")\n\t)\n\n\tbank, adm := newTestToken(\"Dummy\", \"DUMMY\", 6, 0, cur)\n\n\tcheckBalances := func(aliceEB, bobEB, carlEB int64) {\n\t\tt.Helper()\n\t\texp := ufmt.Sprintf(\"alice=%d bob=%d carl=%d\", aliceEB, bobEB, carlEB)\n\t\taliceGB := bank.BalanceOf(alice)\n\t\tbobGB := bank.BalanceOf(bob)\n\t\tcarlGB := bank.BalanceOf(carl)\n\t\tgot := ufmt.Sprintf(\"alice=%d bob=%d carl=%d\", aliceGB, bobGB, carlGB)\n\t\tuassert.Equal(t, got, exp, \"invalid balances\")\n\t}\n\tcheckAllowances := func(abEB, acEB, baEB, bcEB, caEB, cbEB int64) {\n\t\tt.Helper()\n\t\texp := ufmt.Sprintf(\"ab=%d ac=%d ba=%d bc=%d ca=%d cb=%s\", abEB, acEB, baEB, bcEB, caEB, cbEB)\n\t\tabGB := bank.Allowance(alice, bob)\n\t\tacGB := bank.Allowance(alice, carl)\n\t\tbaGB := bank.Allowance(bob, alice)\n\t\tbcGB := bank.Allowance(bob, carl)\n\t\tcaGB := bank.Allowance(carl, alice)\n\t\tcbGB := bank.Allowance(carl, bob)\n\t\tgot := ufmt.Sprintf(\"ab=%d ac=%d ba=%d bc=%d ca=%d cb=%s\", abGB, acGB, baGB, bcGB, caGB, cbGB)\n\t\tuassert.Equal(t, got, exp, \"invalid allowances\")\n\t}\n\n\tcheckBalances(0, 0, 0)\n\tcheckAllowances(0, 0, 0, 0, 0, 0)\n\n\turequire.NoError(t, adm.Mint(alice, 1000))\n\turequire.NoError(t, adm.Mint(alice, 100))\n\tcheckBalances(1100, 0, 0)\n\tcheckAllowances(0, 0, 0, 0, 0, 0)\n\n\turequire.NoError(t, adm.Approve(alice, bob, 99999999))\n\tcheckBalances(1100, 0, 0)\n\tcheckAllowances(99999999, 0, 0, 0, 0, 0)\n\n\turequire.NoError(t, adm.Approve(alice, bob, 400))\n\tcheckBalances(1100, 0, 0)\n\tcheckAllowances(400, 0, 0, 0, 0, 0)\n\n\turequire.Error(t, adm.TransferFrom(alice, bob, carl, 100000000))\n\tcheckBalances(1100, 0, 0)\n\tcheckAllowances(400, 0, 0, 0, 0, 0)\n\n\turequire.NoError(t, adm.TransferFrom(alice, bob, carl, 100))\n\tcheckBalances(1000, 0, 100)\n\tcheckAllowances(300, 0, 0, 0, 0, 0)\n\n\turequire.Error(t, adm.SpendAllowance(alice, bob, 2000000))\n\tcheckBalances(1000, 0, 100)\n\tcheckAllowances(300, 0, 0, 0, 0, 0)\n\n\turequire.NoError(t, adm.SpendAllowance(alice, bob, 100))\n\tcheckBalances(1000, 0, 100)\n\tcheckAllowances(200, 0, 0, 0, 0, 0)\n}\n\nfunc TestMintOverflow(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\ttok, adm := newTestToken(\"Dummy\", \"DUMMY\", 6, 0, cur)\n\n\tsafeValue := int64(1 \u003c\u003c 62)\n\turequire.NoError(t, adm.Mint(alice, safeValue))\n\turequire.Equal(t, tok.BalanceOf(alice), safeValue)\n\n\terr := adm.Mint(bob, safeValue)\n\tuassert.Error(t, err, \"expected ErrMintOverflow\")\n}\n\nfunc TestTransferFromAtomicity(cur realm, t *testing.T) {\n\tvar (\n\t\towner   = testutils.TestAddress(\"owner\")\n\t\tspender = testutils.TestAddress(\"spender\")\n\n\t\tinvalidRecipient = address(\"\")\n\t\trecipient        = testutils.TestAddress(\"to\")\n\t)\n\n\ttoken, admin := newTestToken(\"Test\", \"TEST\", 6, 0, cur)\n\n\t// owner has 100 tokens, spender has 50 allowance\n\tinitialBalance := int64(100)\n\tinitialAllowance := int64(50)\n\n\turequire.NoError(t, admin.Mint(owner, initialBalance))\n\turequire.NoError(t, admin.Approve(owner, spender, initialAllowance))\n\n\t// transfer to an invalid address to force a transfer failure\n\ttransferAmount := int64(30)\n\terr := admin.TransferFrom(owner, spender, invalidRecipient, transferAmount)\n\tuassert.Error(t, err, \"transfer should fail due to invalid address\")\n\n\townerBalance := token.BalanceOf(owner)\n\tuassert.Equal(t, ownerBalance, initialBalance, \"owner balance should remain unchanged\")\n\n\t// check if allowance was incorrectly reduced\n\tremainingAllowance := token.Allowance(owner, spender)\n\tuassert.Equal(t, remainingAllowance, initialAllowance,\n\t\t\"allowance should not be reduced when transfer fails\")\n\n\t// Self-transfers fail in Transfer after SpendAllowance. TransferFrom must\n\t// reject them before it can mutate the allowance.\n\terr = admin.TransferFrom(owner, spender, owner, transferAmount)\n\tuassert.True(t, errors.Is(err, ErrCannotTransferToSelf), \"self-transfer should fail\")\n\tuassert.Equal(t, initialBalance, token.BalanceOf(owner),\n\t\t\"owner balance should not change when self-transfer fails\")\n\tuassert.Equal(t, initialAllowance, token.Allowance(owner, spender),\n\t\t\"allowance should not be reduced when self-transfer fails\")\n\n\t// transfer all tokens\n\tadmin.Transfer(owner, recipient, 100)\n\tremainingBalance := token.BalanceOf(owner)\n\tuassert.Equal(t, remainingBalance, int64(0),\n\t\t\"balance should be zero\")\n\n\terr = admin.TransferFrom(owner, spender, recipient, transferAmount)\n\tuassert.Error(t, err, \"transfer should fail due to insufficient balance\")\n}\n\nfunc TestMintUntilOverflow(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\ttok, adm := newTestToken(\"Dummy\", \"DUMMY\", 6, 0, cur)\n\n\ttests := []struct {\n\t\tname           string\n\t\taddr           address\n\t\tamount         int64\n\t\texpectedError  error\n\t\texpectedSupply int64\n\t\tdescription    string\n\t}{\n\t\t{\n\t\t\tname:           \"mint negative value\",\n\t\t\taddr:           alice,\n\t\t\tamount:         -1,\n\t\t\texpectedError:  ErrInvalidAmount,\n\t\t\texpectedSupply: 0,\n\t\t\tdescription:    \"minting a negative number should fail with ErrInvalidAmount\",\n\t\t},\n\t\t{\n\t\t\tname:           \"mint MaxInt64\",\n\t\t\taddr:           alice,\n\t\t\tamount:         math.MaxInt64 - 1000,\n\t\t\texpectedError:  nil,\n\t\t\texpectedSupply: math.MaxInt64 - 1000,\n\t\t\tdescription:    \"minting almost MaxInt64 should succeed\",\n\t\t},\n\t\t{\n\t\t\tname:           \"mint small value\",\n\t\t\taddr:           bob,\n\t\t\tamount:         1000,\n\t\t\texpectedError:  nil,\n\t\t\texpectedSupply: math.MaxInt64,\n\t\t\tdescription:    \"minting a small value when close to MaxInt64 should succeed\",\n\t\t},\n\t\t{\n\t\t\tname:           \"mint value that would exceed MaxInt64\",\n\t\t\taddr:           bob,\n\t\t\tamount:         1,\n\t\t\texpectedError:  ErrMintOverflow,\n\t\t\texpectedSupply: math.MaxInt64,\n\t\t\tdescription:    \"minting any value when at MaxInt64 should fail with ErrMintOverflow\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := adm.Mint(tt.addr, tt.amount)\n\n\t\t\tif tt.expectedError != nil {\n\t\t\t\tuassert.Error(t, err, tt.description)\n\t\t\t\tif !errors.Is(err, tt.expectedError) {\n\t\t\t\t\tt.Errorf(\"expected error %v, got %v\", tt.expectedError, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tuassert.NoError(t, err, tt.description)\n\t\t\t}\n\n\t\t\ttotalSupply := tok.TotalSupply()\n\t\t\tuassert.Equal(t, totalSupply, tt.expectedSupply, \"totalSupply should match expected value\")\n\t\t})\n\t}\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\t// origRealm is the PkgPath of the realm that created the token, captured\n\t// unforgeably in NewToken. A frame-relative teller only works there.\n\torigRealm string\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\tErrForeignCallerTeller   = errors.New(\"caller teller is confined to the token's own realm\")\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// homeGuard marks a frame-relative teller (PrivateLedger.CallerTeller),\n\t// whose actor is resolved from the invoking frame. Such a teller is only\n\t// meaningful in the token's own realm and is refused anywhere else, so\n\t// exporting the value cannot hand out a spend capability.\n\thomeGuard bool\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"7wKupoKdhHiwK4bjfH99jZsldSI8YiEPOoW45+rlXjYVMuvpGem2tNGmSoABM0aqc7qUK7C161vlRybW68SbJA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"grc721","path":"gno.land/p/nt/grc721/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/grc721/v0\"\ngno = \"0.9\"\n"},{"name":"newtoken_event_filetest.gno","body":"// PKGPATH: gno.land/r/demo/grc721dupsignal\n\n// This filetest is the reference for the detection rule the NewToken event\n// enables. The realm below reuses seqid 0 twice, so both tokens end up with the\n// same Token.ID() and their Transfer events are indistinguishable — the\n// long-standing event-provenance ambiguity.\n//\n// What changes is that the ambiguity is now *announced*. Two NewToken events\n// carry the same \"token\" value, which is a complete signal: NewToken is the only\n// way a Token can exist (Token's fields are unexported), so an indexer watching\n// this event sees every token that will ever emit. On the second duplicate\n// announcement it should flag the realm and stop trusting its token events.\npackage grc721dupsignal\n\nimport (\n\t\"gno.land/p/nt/grc721/v0\"\n)\n\nfunc main(cur realm) {\n\tfirst, firstLedger := grc721.NewToken(\"Same\", \"DUP\", 0, cur)\n\tsecond, secondLedger := grc721.NewToken(\"Same\", \"DUP\", 0, cur)\n\n\tprintln(\"same id:\", first.ID() == second.ID())\n\n\t// Two independent ledgers, one identifier: these Transfers cannot be\n\t// attributed to either object from the event stream alone.\n\tfirstLedger.Mint(cur.Address(), \"1\")\n\tsecondLedger.Mint(cur.Address(), \"2\")\n}\n\n// Output:\n// same id: true\n\n// Events:\n// [\n//   {\n//     \"type\": \"NewToken\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc721dupsignal.DUP.0000000\"\n//       },\n//       {\n//         \"key\": \"name\",\n//         \"value\": \"Same\"\n//       },\n//       {\n//         \"key\": \"symbol\",\n//         \"value\": \"DUP\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc721/v0\"\n//   },\n//   {\n//     \"type\": \"NewToken\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc721dupsignal.DUP.0000000\"\n//       },\n//       {\n//         \"key\": \"name\",\n//         \"value\": \"Same\"\n//       },\n//       {\n//         \"key\": \"symbol\",\n//         \"value\": \"DUP\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc721/v0\"\n//   },\n//   {\n//     \"type\": \"Transfer\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc721dupsignal.DUP.0000000\"\n//       },\n//       {\n//         \"key\": \"from\",\n//         \"value\": \"\"\n//       },\n//       {\n//         \"key\": \"to\",\n//         \"value\": \"g1f2p7p8jhjmg6atgukt8kveqgsrrk3qqhe6vg9m\"\n//       },\n//       {\n//         \"key\": \"tokenId\",\n//         \"value\": \"1\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc721/v0\"\n//   },\n//   {\n//     \"type\": \"Transfer\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc721dupsignal.DUP.0000000\"\n//       },\n//       {\n//         \"key\": \"from\",\n//         \"value\": \"\"\n//       },\n//       {\n//         \"key\": \"to\",\n//         \"value\": \"g1f2p7p8jhjmg6atgukt8kveqgsrrk3qqhe6vg9m\"\n//       },\n//       {\n//         \"key\": \"tokenId\",\n//         \"value\": \"2\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc721/v0\"\n//   }\n// ]\n"},{"name":"tellers.gno","body":"package grc721\n\nimport (\n\t\"chain\"\n)\n\n// CallerTeller resolves the acting account at each write as rlm.Previous() —\n// the realm that crossed into the realm holding the teller.\n//\n// SECURITY: this accessor hangs off *PrivateLedger, not *Token, and that is\n// load-bearing. A frame-relative teller acts as whoever crossed into its\n// holder, so it is only meaningful inside the token's own realm, where the\n// wrappers act for a caller who knowingly invoked the collection. Anywhere\n// else it is a confused deputy: a realm a user merely calls could move that\n// user's tokens. The *Token pointer is published — exported vars, the\n// collection facade, grc721reg — while the ledger is not, since NewToken hands\n// it to the creating realm and nowhere else. So a foreign realm cannot mint\n// one.\n//\n// Construction privacy alone is not enough: a realm may legally build a teller\n// and then export the VALUE. The write methods therefore also verify that the\n// invoking realm is the token's own (see guardHome), which leaves a leaked\n// teller inert everywhere but home.\nfunc (led *PrivateLedger) CallerTeller() Teller {\n\tif led == nil {\n\t\tpanic(\"Ledger 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\thomeGuard: true,\n\t\tToken:     led.token,\n\t}\n}\n\n// Write methods return ErrReadonly.\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// Permanently acts as the calling realm (verified via IsCurrent).\nfunc (tok *Token) RealmTeller(_ int, rlm realm) Teller {\n\tif tok == nil {\n\t\tpanic(\"Token cannot be nil\")\n\t}\n\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// Like RealmTeller but acts as a sub-account derived from slug.\nfunc (tok *Token) RealmSubTeller(_ int, rlm realm, slug string) Teller {\n\tif tok == nil {\n\t\tpanic(\"Token cannot be nil\")\n\t}\n\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// Admin-grade: issuer-only (holds the PrivateLedger). Panics on invalid addr to close the empty-sentinel hole.\nfunc (led *PrivateLedger) ImpersonateTeller(addr address) Teller {\n\tif led == nil {\n\t\tpanic(\"Ledger cannot be nil\")\n\t}\n\n\tif !addr.IsValid() {\n\t\tpanic(ErrInvalidAddress)\n\t}\n\n\treturn \u0026fnTeller{\n\t\taccountFn: func(_ int, _ realm) address {\n\t\t\treturn addr\n\t\t},\n\t\tToken: led.token,\n\t}\n}\n\n// guardHome confines a frame-relative teller to the token's own realm.\n// Construction privacy stops a foreign realm from minting one; this stops a\n// minted one from travelling, which is what happens when a realm legally builds\n// a teller and then exports the value.\n//\n// The check is on the invoking realm's path alone — deliberately NOT on whether\n// the resolved actor is an end user. Keying on the actor only blocks the case\n// where the debited party is the signing user, and leaves two doors open: a\n// realm can be charged by a realm it calls, and TransferFrom resolves the\n// *spender* from the frame, so a realm reached from an honest hub spends that\n// hub's approvals against any owner who granted one. Both are the same defect\n// as the original one level up — frame-relative resolution means whoever you\n// call can act as you — and neither is reachable once the teller only works at\n// home.\n//\n// The host is compared after stripping any \"#subpath\" synthesized by realm.Sub,\n// so the token's own sub-realms are not falsely rejected.\n//\n// A foreign realm that needs to move a user's tokens uses the ordinary route:\n// the owner Approves it, and it spends as itself through RealmTeller, which is\n// eagerly bound to its own address and approval-gated.\n//\n// The leading int keeps this a plain method.\nfunc (ft *fnTeller) guardHome(_ int, rlm realm) error {\n\tif !ft.homeGuard {\n\t\treturn nil\n\t}\n\n\thost, _, _ := chain.SplitPkgSubPath(rlm.PkgPath())\n\tif host != ft.Token.origRealm {\n\t\treturn ErrForeignCallerTeller\n\t}\n\n\treturn nil\n}\n\nfunc (ft *fnTeller) Approve(_ int, rlm realm, to address, tid TokenID) error {\n\tif ft.accountFn == nil {\n\t\treturn ErrReadonly\n\t}\n\n\tif !rlm.IsCurrent() {\n\t\treturn ErrSpoofedRealm\n\t}\n\n\tif err := ft.guardHome(0, rlm); err != nil {\n\t\treturn err\n\t}\n\n\tcaller := ft.accountFn(0, rlm)\n\n\treturn ft.Token.ledger.Approve(caller, to, tid)\n}\n\nfunc (ft *fnTeller) SetApprovalForAll(_ int, rlm realm, operator address, approved bool) error {\n\tif ft.accountFn == nil {\n\t\treturn ErrReadonly\n\t}\n\n\tif !rlm.IsCurrent() {\n\t\treturn ErrSpoofedRealm\n\t}\n\n\tif err := ft.guardHome(0, rlm); err != nil {\n\t\treturn err\n\t}\n\n\tcaller := ft.accountFn(0, rlm)\n\n\treturn ft.Token.ledger.SetApprovalForAll(caller, operator, approved)\n}\n\nfunc (ft *fnTeller) TransferFrom(_ int, rlm realm, from, to address, tid TokenID) error {\n\tif ft.accountFn == nil {\n\t\treturn ErrReadonly\n\t}\n\n\tif !rlm.IsCurrent() {\n\t\treturn ErrSpoofedRealm\n\t}\n\n\tif err := ft.guardHome(0, rlm); err != nil {\n\t\treturn err\n\t}\n\n\tspender := ft.accountFn(0, rlm)\n\n\treturn ft.Token.ledger.TransferFrom(spender, from, to, tid)\n}\n\nfunc accountSlugAddr(addr address, slug string) address {\n\tif slug == \"\" {\n\t\treturn addr\n\t}\n\n\tkey := addr.String() + \"/\" + slug\n\n\treturn chain.PackageAddress(key)\n}\n"},{"name":"tellers_test.gno","body":"package grc721\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\ntype evilTeller struct{ Teller }\n\nfunc TestIsCanonicalTeller(cur realm, t *testing.T) {\n\ttok, led := newTestToken(\"Foo\", \"FOO\", 0, cur)\n\n\tvar realmTeller Teller\n\tfunc(cur realm) {\n\t\trealmTeller = tok.RealmTeller(0, cur)\n\t}(cross(cur))\n\n\ttests := []struct {\n\t\tname string\n\t\tte   Teller\n\t\twant bool\n\t}{\n\t\t{\"CallerTeller is canonical\", led.CallerTeller(), true},\n\t\t{\"ReadonlyTeller is canonical\", tok.ReadonlyTeller(), true},\n\t\t{\"RealmTeller is canonical\", realmTeller, true},\n\t\t{\"ImpersonateTeller is canonical\", led.ImpersonateTeller(alice), true},\n\t\t{\"embedding wrapper is rejected\", \u0026evilTeller{led.CallerTeller()}, false},\n\t}\n\n\tfor _, tc := range tests {\n\t\ttc := tc\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tuassert.Equal(t, tc.want, IsCanonicalTeller(tc.te))\n\t\t})\n\t}\n}\n\nfunc TestTellerNilGuards(cur realm, t *testing.T) {\n\tvar nilTok *Token\n\tvar nilLed *PrivateLedger\n\n\ttests := []struct {\n\t\tname string\n\t\tmsg  string\n\t\tfn   func()\n\t}{\n\t\t{\"CallerTeller on nil ledger panics\", \"Ledger cannot be nil\", func() {\n\t\t\tnilLed.CallerTeller()\n\t\t}},\n\t\t{\"ReadonlyTeller on nil token panics\", \"Token cannot be nil\", func() {\n\t\t\tnilTok.ReadonlyTeller()\n\t\t}},\n\t\t{\"RealmTeller on nil token panics\", \"Token cannot be nil\", func() {\n\t\t\tnilTok.RealmTeller(0, cur)\n\t\t}},\n\t\t{\"RealmSubTeller on nil token panics\", \"Token cannot be nil\", func() {\n\t\t\tnilTok.RealmSubTeller(0, cur, \"slug\")\n\t\t}},\n\t\t{\"ImpersonateTeller on nil ledger panics\", \"Ledger cannot be nil\", func() {\n\t\t\tnilLed.ImpersonateTeller(alice)\n\t\t}},\n\t}\n\n\tfor _, tc := range tests {\n\t\ttc := tc\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tuassert.PanicsWithMessage(t, cur, tc.msg, tc.fn)\n\t\t})\n\t}\n}\n\nfunc TestReadonlyTeller(cur realm, t *testing.T) {\n\ttok, led := newTestToken(\"Foo\", \"FOO\", 0, cur)\n\turequire.NoError(t, led.Mint(alice, \"1\"))\n\tro := tok.ReadonlyTeller()\n\n\ttests := []struct {\n\t\tname string\n\t\top   string\n\t}{\n\t\t{\"Approve rejected with ErrReadonly\", \"approve\"},\n\t\t{\"TransferFrom rejected with ErrReadonly\", \"transfer\"},\n\t\t{\"SetApprovalForAll rejected with ErrReadonly\", \"setall\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\ttc := tc\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar err error\n\t\t\ttesting.SetRealm(testing.NewUserRealm(alice))\n\t\t\tfunc(cur realm) {\n\t\t\t\tswitch tc.op {\n\t\t\t\tcase \"approve\":\n\t\t\t\t\terr = ro.Approve(0, cur, bob, \"1\")\n\t\t\t\tcase \"transfer\":\n\t\t\t\t\terr = ro.TransferFrom(0, cur, alice, bob, \"1\")\n\t\t\t\tcase \"setall\":\n\t\t\t\t\terr = ro.SetApprovalForAll(0, cur, bob, true)\n\t\t\t\t}\n\t\t\t}(cross(cur))\n\t\t\tuassert.ErrorIs(t, err, ErrReadonly)\n\t\t})\n\t}\n}\n\nfunc TestCallerTellerFlow(cur realm, t *testing.T) {\n\ttok, led := newTestToken(\"Foo\", \"FOO\", 0, cur)\n\turequire.NoError(t, led.Mint(alice, \"1\"))\n\turequire.NoError(t, led.Mint(alice, \"2\"))\n\n\tteller := led.CallerTeller()\n\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\tfunc(cur realm) {\n\t\turequire.NoError(t, teller.Approve(0, cur, bob, \"1\"))\n\t}(cross(cur))\n\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\tfunc(cur realm) {\n\t\turequire.NoError(t, teller.SetApprovalForAll(0, cur, bob, true))\n\t}(cross(cur))\n\tuassert.True(t, tok.IsApprovedForAll(alice, bob))\n\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\tfunc(cur realm) {\n\t\turequire.NoError(t, teller.TransferFrom(0, cur, alice, carl, \"1\"))\n\t}(cross(cur))\n\n\towner, err := tok.OwnerOf(\"1\")\n\turequire.NoError(t, err)\n\tuassert.Equal(t, carl, owner)\n\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\tfunc(cur realm) {\n\t\tuassert.ErrorIs(t, teller.TransferFrom(0, cur, carl, bob, \"1\"), ErrCallerIsNotOwnerOrApproved)\n\t}(cross(cur))\n}\n\nfunc TestCallerTellerSpoofedRealm(cur realm, t *testing.T) {\n\t_, led := newTestToken(\"Foo\", \"FOO\", 0, cur)\n\turequire.NoError(t, led.Mint(alice, \"1\"))\n\tteller := led.CallerTeller()\n\n\t// Stale outer cur while a fresh frame is current → ErrSpoofedRealm.\n\ttests := []struct {\n\t\tname string\n\t\top   string\n\t}{\n\t\t{\"Approve rejects spoofed realm\", \"approve\"},\n\t\t{\"TransferFrom rejects spoofed realm\", \"transfer\"},\n\t\t{\"SetApprovalForAll rejects spoofed realm\", \"setall\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\ttc := tc\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar err error\n\t\t\tstale := cur\n\t\t\tfunc(cur realm) {\n\t\t\t\tswitch tc.op {\n\t\t\t\tcase \"approve\":\n\t\t\t\t\terr = teller.Approve(0, stale, bob, \"1\")\n\t\t\t\tcase \"transfer\":\n\t\t\t\t\terr = teller.TransferFrom(0, stale, alice, bob, \"1\")\n\t\t\t\tcase \"setall\":\n\t\t\t\t\terr = teller.SetApprovalForAll(0, stale, bob, true)\n\t\t\t\t}\n\t\t\t}(cross(cur))\n\t\t\tuassert.ErrorIs(t, err, ErrSpoofedRealm)\n\t\t})\n\t}\n}\n\nfunc TestRealmTellerFlow(cur realm, t *testing.T) {\n\ttok, led := newTestToken(\"Foo\", \"FOO\", 0, cur)\n\n\tvar teller Teller\n\tvar realmAddr address\n\tfunc(cur realm) {\n\t\tteller = tok.RealmTeller(0, cur)\n\t\trealmAddr = cur.Address()\n\t}(cross(cur))\n\n\turequire.NoError(t, led.Mint(realmAddr, \"1\"))\n\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\tfunc(cur realm) {\n\t\turequire.NoError(t, teller.TransferFrom(0, cur, realmAddr, bob, \"1\"))\n\t}(cross(cur))\n\n\towner, err := tok.OwnerOf(\"1\")\n\turequire.NoError(t, err)\n\tuassert.Equal(t, bob, owner)\n}\n\nfunc TestRealmSubTellerFlow(cur realm, t *testing.T) {\n\ttok, led := newTestToken(\"Foo\", \"FOO\", 0, cur)\n\n\ttests := []struct {\n\t\tname           string\n\t\tslug           string\n\t\ttid            TokenID\n\t\twantsRealmAddr bool\n\t}{\n\t\t{\"empty slug derives the realm address itself\", \"\", \"10\", true},\n\t\t{\"non-empty slug derives a sub-account\", \"vault\", \"11\", false},\n\t}\n\n\tfor _, tc := range tests {\n\t\ttc := tc\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar teller Teller\n\t\t\tvar realmAddr, account address\n\t\t\tfunc(cur realm) {\n\t\t\t\tteller = tok.RealmSubTeller(0, cur, tc.slug)\n\t\t\t\trealmAddr = cur.Address()\n\t\t\t\taccount = accountSlugAddr(cur.Address(), tc.slug)\n\t\t\t}(cross(cur))\n\n\t\t\tif tc.wantsRealmAddr {\n\t\t\t\tuassert.Equal(t, realmAddr, account)\n\t\t\t} else {\n\t\t\t\tuassert.True(t, realmAddr != account, \"sub-account must differ from realm address\")\n\t\t\t}\n\n\t\t\turequire.NoError(t, led.Mint(account, tc.tid))\n\t\t\ttesting.SetRealm(testing.NewUserRealm(alice))\n\t\t\tfunc(cur realm) {\n\t\t\t\turequire.NoError(t, teller.TransferFrom(0, cur, account, bob, tc.tid))\n\t\t\t}(cross(cur))\n\n\t\t\towner, err := tok.OwnerOf(tc.tid)\n\t\t\turequire.NoError(t, err)\n\t\t\tuassert.Equal(t, bob, owner)\n\t\t})\n\t}\n}\n\nfunc TestRealmTellerSpoofedRealm(cur realm, t *testing.T) {\n\ttok, _ := newTestToken(\"Foo\", \"FOO\", 0, cur)\n\n\ttests := []struct {\n\t\tname string\n\t\tfn   func()\n\t}{\n\t\t{\"RealmTeller rejects spoofed realm\", func() {\n\t\t\tstale := cur\n\t\t\tfunc(cur realm) {\n\t\t\t\ttok.RealmTeller(0, stale)\n\t\t\t}(cross(cur))\n\t\t}},\n\t\t{\"RealmSubTeller rejects spoofed realm\", func() {\n\t\t\tstale := cur\n\t\t\tfunc(cur realm) {\n\t\t\t\ttok.RealmSubTeller(0, stale, \"vault\")\n\t\t\t}(cross(cur))\n\t\t}},\n\t}\n\n\tfor _, tc := range tests {\n\t\ttc := tc\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\turequire.AbortsContains(t, cur, \"rlm does not match the current crossing frame\", tc.fn)\n\t\t})\n\t}\n}\n\nfunc TestImpersonateTeller(cur realm, t *testing.T) {\n\ttok, led := newTestToken(\"Foo\", \"FOO\", 0, cur)\n\turequire.NoError(t, led.Mint(alice, \"1\"))\n\n\tteller := led.ImpersonateTeller(alice)\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\tfunc(cur realm) {\n\t\turequire.NoError(t, teller.TransferFrom(0, cur, alice, bob, \"1\"))\n\t}(cross(cur))\n\n\towner, err := tok.OwnerOf(\"1\")\n\turequire.NoError(t, err)\n\tuassert.Equal(t, bob, owner)\n}\n\nfunc TestImpersonateTellerRejectsInvalidAddress(cur realm, t *testing.T) {\n\t_, led := newTestToken(\"Foo\", \"FOO\", 0, cur)\n\tuassert.PanicsWithMessage(t, cur, \"invalid address\", func() {\n\t\tled.ImpersonateTeller(zeroAddress)\n\t})\n}\n"},{"name":"token.gno","body":"package grc721\n\nimport (\n\t\"chain\"\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 core token and its ledger.\n// rlm must be the caller's own captured cur (IsCurrent); its PkgPath becomes the unforgeable origRealm.\n//\n// Every successful call emits a NewToken event carrying the resulting Token.ID().\n// Because Token's fields are unexported, NewToken is the only way a Token can come\n// into existence, so this event makes token creation fully observable: an indexer\n// that sees the same Token.ID() announced twice knows the realm built two\n// independent ledgers behind one identifier, and that every later\n// Mint/Burn/Transfer/Approval carrying that id is ambiguous. Such a realm is\n// emitting untrustworthy events and should be flagged or ignored wholesale.\nfunc NewToken(name, symbol string, id seqid.ID, rlm realm) (*Token, *PrivateLedger) {\n\tif !rlm.IsCurrent() {\n\t\tpanic(ErrSpoofedRealm)\n\t}\n\n\tpkgPath := rlm.PkgPath()\n\tif pkgPath == \"\" {\n\t\tpanic(ErrNotRealm)\n\t}\n\n\tif !validName(name) {\n\t\tpanic(ErrInvalidName)\n\t}\n\n\tif !validSymbol(symbol) {\n\t\tpanic(ErrInvalidSymbol)\n\t}\n\n\t// origRealm is the host path, with any \"#subpath\" synthesized by realm.Sub\n\t// stripped. guardHome strips the invoking path the same way, so a token\n\t// created from a sub frame is not locked out of its own host realm, and a\n\t// host's sub-realms are not falsely rejected. Token.ID() keeps the raw\n\t// pkgPath, so identities and the NewToken event are unaffected.\n\torigRealm, _, _ := chain.SplitPkgSubPath(pkgPath)\n\n\tledger := \u0026PrivateLedger{}\n\ttoken := \u0026Token{\n\t\tid:        pkgPath + \".\" + symbol + \".\" + id.String(),\n\t\tname:      name,\n\t\tsymbol:    symbol,\n\t\tledger:    ledger,\n\t\torigRealm: origRealm,\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)\n\n\treturn token, ledger\n}\n\nfunc validName(name string) bool {\n\tif name == \"\" || len(name) \u003e MaxNameLen {\n\t\treturn false\n\t}\n\n\tfor _, c := range name {\n\t\tif c \u003c 0x20 || c == 0x7f {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\nfunc validSymbol(s string) bool {\n\tif s == \"\" || len(s) \u003e MaxSymbolLen {\n\t\treturn false\n\t}\n\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\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 (tok *Token) GetName() string { return tok.name }\n\nfunc (tok *Token) GetSymbol() string { return tok.symbol }\n\nfunc (tok *Token) ID() string { return tok.id }\n\nfunc (tok *Token) TotalSupply() int64 { return tok.ledger.totalSupply }\n\nfunc (tok *Token) KnownAccounts() int { return tok.ledger.balances.Size() }\n\n// EIP-721: balanceOf throws for queries about the zero address,\n// since NFTs assigned to it are considered invalid.\nfunc (tok *Token) BalanceOf(addr address) (int64, error) {\n\tif !addr.IsValid() {\n\t\treturn 0, ErrInvalidAddress\n\t}\n\n\treturn tok.ledger.balanceOf(addr), nil\n}\n\nfunc (tok *Token) OwnerOf(tid TokenID) (address, error) { return tok.ledger.ownerOf(tid) }\n\n// EIP-721: getApproved throws for a token that is not a valid NFT, so a missing\n// token is ErrInvalidTokenId and a live but unapproved one is ErrTokenIdNotApproved.\nfunc (tok *Token) GetApproved(tid TokenID) (address, error) {\n\tif !tok.ledger.exists(tid) {\n\t\treturn zeroAddress, ErrInvalidTokenId\n\t}\n\n\taddr := tok.ledger.tokenApprovals.Get(tid.String())\n\tif addr == nil {\n\t\treturn zeroAddress, ErrTokenIdNotApproved\n\t}\n\n\treturn addr.(address), nil\n}\n\nfunc (tok *Token) IsApprovedForAll(owner, operator address) bool {\n\treturn tok.ledger.isApprovedForAll(owner, operator)\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(\"* **Total supply**: %d\\n\", tok.ledger.totalSupply)\n\tstr += ufmt.Sprintf(\"* **Known accounts**: %d\\n\", tok.KnownAccounts())\n\n\treturn str\n}\n\n// RegisterExtension attaches an issuer-supplied hook set. Issuer-only (holds the\n// PrivateLedger), which keeps the surface from being attacker-attachable — but the\n// attachment is still a privilege: per the Extension doc, an attached hook can veto\n// transfers (a panic aborts the transaction before the Transfer event is emitted) and\n// adds gas to every movement.\n// One extension per kind; a second registration of an attached kind panics.\nfunc (led *PrivateLedger) RegisterExtension(ext Extension) {\n\tif ext == nil {\n\t\tpanic(\"grc721: nil extension\")\n\t}\n\n\tkind := ext.ExtensionKind()\n\tfor _, e := range led.extensions {\n\t\tif e.ExtensionKind() == kind {\n\t\t\tpanic(\"grc721: extension kind already registered: \" + kind)\n\t\t}\n\t}\n\n\tled.extensions = append(led.extensions, ext)\n}\n\nfunc (led *PrivateLedger) ReadToken() *Token { return led.token }\n\nfunc (led *PrivateLedger) Mint(to address, tid TokenID) error {\n\tif !to.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tif tid == \"\" {\n\t\treturn ErrInvalidTokenId\n\t}\n\n\tif led.exists(tid) {\n\t\treturn ErrTokenIdAlreadyExists\n\t}\n\n\tled.balances.Set(to.String(), overflow.Add64p(led.balanceOf(to), 1))\n\tled.owners.Set(tid.String(), to)\n\tled.totalSupply = overflow.Add64p(led.totalSupply, 1)\n\n\tfor _, ext := range led.extensions {\n\t\text.OnMint(to, tid)\n\t}\n\n\t// Mint emits Transfer from the empty address (EIP-721); indexers reconstruct ownership from Transfer alone.\n\tchain.Emit(\n\t\tTransferEvent,\n\t\t\"token\", led.token.id,\n\t\t\"from\", \"\",\n\t\t\"to\", to.String(),\n\t\t\"tokenId\", tid.String(),\n\t)\n\n\treturn nil\n}\n\n// Fans OnBurn out to every extension so per-token state cannot resurface if tid is re-minted.\nfunc (led *PrivateLedger) Burn(tid TokenID) error {\n\towner, err := led.ownerOf(tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttidStr := tid.String()\n\tled.tokenApprovals.Remove(tidStr)\n\tled.setBalance(owner, overflow.Sub64p(led.balanceOf(owner), 1))\n\tled.owners.Remove(tidStr)\n\tled.totalSupply = overflow.Sub64p(led.totalSupply, 1)\n\n\tfor _, ext := range led.extensions {\n\t\text.OnBurn(tid)\n\t}\n\n\t// Burn emits Transfer to the empty address (EIP-721).\n\tchain.Emit(\n\t\tTransferEvent,\n\t\t\"token\", led.token.id,\n\t\t\"from\", owner.String(),\n\t\t\"to\", \"\",\n\t\t\"tokenId\", tidStr,\n\t)\n\n\treturn nil\n}\n\n// spender must own or be approved for tid; callers derive it from a trusted per-frame identity.\nfunc (led *PrivateLedger) TransferFrom(spender, from, to address, tid TokenID) error {\n\tif !led.isApprovedOrOwner(spender, tid) {\n\t\treturn ErrCallerIsNotOwnerOrApproved\n\t}\n\n\treturn led.transfer(from, to, tid)\n}\n\n// caller must own tid or be an approved operator of the owner.\n// EIP-721: approving the zero address clears (revokes) any single-token approval.\nfunc (led *PrivateLedger) Approve(caller, to address, tid TokenID) error {\n\trevoke := to == zeroAddress\n\tif !revoke \u0026\u0026 !to.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\n\towner, err := led.ownerOf(tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif owner == to {\n\t\treturn ErrApprovalToCurrentOwner\n\t}\n\n\tif caller != owner \u0026\u0026 !led.isApprovedForAll(owner, caller) {\n\t\treturn ErrCallerIsNotOwnerOrApproved\n\t}\n\n\tif revoke {\n\t\tled.tokenApprovals.Remove(tid.String())\n\t} else {\n\t\tled.tokenApprovals.Set(tid.String(), to)\n\t}\n\n\tchain.Emit(\n\t\tApprovalEvent,\n\t\t\"token\", led.token.id,\n\t\t\"owner\", owner.String(),\n\t\t\"to\", to.String(), // empty for a revoke\n\t\t\"tokenId\", tid.String(),\n\t)\n\n\treturn nil\n}\n\nfunc (led *PrivateLedger) SetApprovalForAll(owner, operator address, approved bool) error {\n\tif !owner.IsValid() || !operator.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tif owner == operator {\n\t\treturn ErrApprovalToCurrentOwner\n\t}\n\n\tkey := operatorKey(owner, operator)\n\tif approved {\n\t\tled.operatorApprovals.Set(key, approved)\n\t} else {\n\t\tled.operatorApprovals.Remove(key)\n\t}\n\n\tchain.Emit(\n\t\tApprovalForAllEvent,\n\t\t\"token\", led.token.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\nfunc (led *PrivateLedger) transfer(from, to address, tid TokenID) error {\n\tif !from.IsValid() || !to.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tif from == to {\n\t\treturn ErrCannotTransferToSelf\n\t}\n\n\towner, err := led.ownerOf(tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif owner != from {\n\t\treturn ErrTransferFromIncorrectOwner\n\t}\n\n\ttidStr := tid.String()\n\tled.tokenApprovals.Remove(tidStr)\n\tled.setBalance(from, overflow.Sub64p(led.balanceOf(from), 1))\n\tled.setBalance(to, overflow.Add64p(led.balanceOf(to), 1))\n\tled.owners.Set(tidStr, to)\n\n\tfor _, ext := range led.extensions {\n\t\text.OnTransfer(from, to, tid)\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\"tokenId\", tidStr,\n\t)\n\n\treturn nil\n}\n\nfunc (led *PrivateLedger) ownerOf(tid TokenID) (address, error) {\n\towner := led.owners.Get(tid.String())\n\tif owner == nil {\n\t\treturn zeroAddress, ErrInvalidTokenId\n\t}\n\n\treturn owner.(address), nil\n}\n\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\n\treturn balance.(int64)\n}\n\nfunc (led *PrivateLedger) setBalance(addr address, balance int64) {\n\tif balance == 0 {\n\t\tled.balances.Remove(addr.String())\n\n\t\treturn\n\t}\n\n\tled.balances.Set(addr.String(), balance)\n}\n\nfunc (led *PrivateLedger) isApprovedForAll(owner, operator address) bool {\n\tapproved := led.operatorApprovals.Get(operatorKey(owner, operator))\n\tif approved == nil {\n\t\treturn false\n\t}\n\n\treturn approved.(bool)\n}\n\nfunc (led *PrivateLedger) isApprovedOrOwner(addr address, tid TokenID) bool {\n\towner := led.owners.Get(tid.String())\n\tif owner == nil {\n\t\treturn false\n\t}\n\n\townerAddr := owner.(address)\n\tif addr == ownerAddr || led.isApprovedForAll(ownerAddr, addr) {\n\t\treturn true\n\t}\n\n\tapproved := led.tokenApprovals.Get(tid.String())\n\n\treturn approved != nil \u0026\u0026 approved.(address) == addr\n}\n\nfunc (led *PrivateLedger) exists(tid TokenID) bool {\n\treturn led.owners.Has(tid.String())\n}\n\nfunc operatorKey(owner, operator address) string {\n\treturn owner.String() + \":\" + operator.String()\n}\n"},{"name":"token_test.gno","body":"package grc721\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc mustBalanceOf(t *testing.T, tok *Token, addr address) int64 {\n\tbalance, err := tok.BalanceOf(addr)\n\turequire.NoError(t, err)\n\treturn balance\n}\n\n// newTestToken builds via same-realm cross (NewToken requires rlm.IsCurrent()).\nfunc newTestToken(name, symbol string, id seqid.ID, rlm realm) (tok *Token, led *PrivateLedger) {\n\tfunc(cur realm) {\n\t\ttok, led = NewToken(name, symbol, id, cur)\n\t}(cross(rlm))\n\treturn\n}\n\ntype mockExtension struct {\n\tmints, transfers, burns int\n\tlastMintTo              address\n\tlastTransferFrom        address\n\tlastTransferTo          address\n\tlastBurn                TokenID\n}\n\nfunc (m *mockExtension) ExtensionKind() string { return \"mock\" }\n\nfunc (m *mockExtension) OnMint(to address, tid TokenID) {\n\tm.mints++\n\tm.lastMintTo = to\n}\n\nfunc (m *mockExtension) OnTransfer(from, to address, tid TokenID) {\n\tm.transfers++\n\tm.lastTransferFrom = from\n\tm.lastTransferTo = to\n}\n\nfunc (m *mockExtension) OnBurn(tid TokenID) {\n\tm.burns++\n\tm.lastBurn = tid\n}\n\n// Value type holding a slice: uncomparable, so `==` on two of them is a runtime fault.\ntype journalExtension struct{ seen []TokenID }\n\nfunc (e journalExtension) ExtensionKind() string                    { return \"journal\" }\nfunc (e journalExtension) OnMint(to address, tid TokenID)           {}\nfunc (e journalExtension) OnTransfer(from, to address, tid TokenID) {}\nfunc (e journalExtension) OnBurn(tid TokenID)                       {}\n\nvar (\n\talice = testutils.TestAddress(\"alice\")\n\tbob   = testutils.TestAddress(\"bob\")\n\tcarl  = testutils.TestAddress(\"carl\")\n)\n\nfunc TestNewToken(cur realm, t *testing.T) {\n\ttok, led := newTestToken(\"Foo NFT\", \"FOO\", 0, cur)\n\turequire.False(t, tok == nil, \"token should not be nil\")\n\turequire.False(t, led == nil, \"ledger should not be nil\")\n\n\tuassert.Equal(t, \"Foo NFT\", tok.GetName())\n\tuassert.Equal(t, \"FOO\", tok.GetSymbol())\n\tuassert.Equal(t, int64(0), tok.TotalSupply())\n\tuassert.Equal(t, 0, tok.KnownAccounts())\n\tuassert.True(t, strings.HasSuffix(tok.ID(), \".FOO.\"+seqid.ID(0).String()), \"ID must end with .symbol.id\")\n\tuassert.True(t, strings.HasPrefix(tok.ID(), \"gno.land/\"), \"ID must begin with the origin realm path\")\n\tuassert.Equal(t, tok.ID(), led.ReadToken().ID())\n}\n\nfunc TestNewTokenDistinctIDs(cur realm, t *testing.T) {\n\tfirst, _ := newTestToken(\"Same\", \"DUP\", 1, cur)\n\tsecond, _ := newTestToken(\"Same\", \"DUP\", 2, cur)\n\tuassert.True(t, first.ID() != second.ID(), \"distinct seqid must yield distinct IDs\")\n\n\ta, _ := newTestToken(\"Same\", \"DUP\", 3, cur)\n\tb, _ := newTestToken(\"Same\", \"DUP\", 3, cur)\n\tuassert.Equal(t, a.ID(), b.ID())\n}\n\nfunc TestNewTokenValidation(cur realm, t *testing.T) {\n\tlongName := strings.Repeat(\"a\", MaxNameLen+1)\n\tmaxName := strings.Repeat(\"a\", MaxNameLen)\n\tlongSymbol := strings.Repeat(\"A\", MaxSymbolLen+1)\n\n\ttests := []struct {\n\t\tname       string\n\t\ttokenName  string\n\t\tsymbol     string\n\t\twantAbort  bool\n\t\twantSubstr string\n\t}{\n\t\t{\"valid name and symbol succeeds\", \"Foo\", \"FOO\", false, \"\"},\n\t\t{\"symbol with full slug charset succeeds\", \"Foo\", \"aZ9_-\", false, \"\"},\n\t\t{\"name at max length succeeds\", maxName, \"FOO\", false, \"\"},\n\t\t{\"empty name returns ErrInvalidName\", \"\", \"FOO\", true, \"invalid token name\"},\n\t\t{\"name over max length returns ErrInvalidName\", longName, \"FOO\", true, \"invalid token name\"},\n\t\t{\"name with low control char returns ErrInvalidName\", \"Foo\\x01\", \"FOO\", true, \"invalid token name\"},\n\t\t{\"name with DEL control char returns ErrInvalidName\", \"Foo\\x7f\", \"FOO\", true, \"invalid token name\"},\n\t\t{\"empty symbol returns ErrInvalidSymbol\", \"Foo\", \"\", true, \"invalid token symbol\"},\n\t\t{\"symbol over max length returns ErrInvalidSymbol\", \"Foo\", longSymbol, true, \"invalid token symbol\"},\n\t\t{\"symbol with space returns ErrInvalidSymbol\", \"Foo\", \"has space\", true, \"invalid token symbol\"},\n\t\t{\"symbol with dot returns ErrInvalidSymbol\", \"Foo\", \"FO.O\", true, \"invalid token symbol\"},\n\t\t{\"symbol with slash returns ErrInvalidSymbol\", \"Foo\", \"FO/O\", true, \"invalid token symbol\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\ttc := tc\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tif tc.wantAbort {\n\t\t\t\turequire.AbortsContains(t, cur, tc.wantSubstr, func() {\n\t\t\t\t\tnewTestToken(tc.tokenName, tc.symbol, 0, cur)\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttok, _ := newTestToken(tc.tokenName, tc.symbol, 0, cur)\n\t\t\tuassert.Equal(t, tc.tokenName, tok.GetName())\n\t\t\tuassert.Equal(t, tc.symbol, tok.GetSymbol())\n\t\t})\n\t}\n}\n\nfunc TestNewTokenRejectsSpoofedRealm(cur realm, t *testing.T) {\n\t// Stale outer cur in a fresh frame → ErrSpoofedRealm.\n\turequire.AbortsContains(t, cur, \"rlm does not match the current crossing frame\", func() {\n\t\tstale := cur\n\t\tfunc(cur realm) {\n\t\t\tNewToken(\"Foo\", \"FOO\", 0, stale)\n\t\t}(cross(cur))\n\t})\n}\n\nfunc TestMint(cur realm, t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\taction  func(led *PrivateLedger) error\n\t\twantErr error\n\t\tverify  func(t *testing.T, tok *Token, led *PrivateLedger)\n\t}{\n\t\t{\n\t\t\tname: \"mint to valid address succeeds\",\n\t\t\taction: func(led *PrivateLedger) error {\n\t\t\t\treturn led.Mint(alice, \"1\")\n\t\t\t},\n\t\t\twantErr: nil,\n\t\t\tverify: func(t *testing.T, tok *Token, led *PrivateLedger) {\n\t\t\t\tuassert.Equal(t, int64(1), tok.TotalSupply())\n\t\t\t\tuassert.Equal(t, int64(1), mustBalanceOf(t, tok, alice))\n\t\t\t\tuassert.Equal(t, 1, tok.KnownAccounts())\n\t\t\t\towner, err := tok.OwnerOf(\"1\")\n\t\t\t\turequire.NoError(t, err)\n\t\t\t\tuassert.Equal(t, alice, owner)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"mint to invalid address returns ErrInvalidAddress\",\n\t\t\taction: func(led *PrivateLedger) error {\n\t\t\t\treturn led.Mint(zeroAddress, \"1\")\n\t\t\t},\n\t\t\twantErr: ErrInvalidAddress,\n\t\t\tverify: func(t *testing.T, tok *Token, led *PrivateLedger) {\n\t\t\t\tuassert.Equal(t, int64(0), tok.TotalSupply())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"mint duplicate token id returns ErrTokenIdAlreadyExists\",\n\t\t\taction: func(led *PrivateLedger) error {\n\t\t\t\turequire.NoError(t, led.Mint(alice, \"1\"))\n\t\t\t\treturn led.Mint(bob, \"1\")\n\t\t\t},\n\t\t\twantErr: ErrTokenIdAlreadyExists,\n\t\t\tverify: func(t *testing.T, tok *Token, led *PrivateLedger) {\n\t\t\t\towner, err := tok.OwnerOf(\"1\")\n\t\t\t\turequire.NoError(t, err)\n\t\t\t\tuassert.Equal(t, alice, owner)\n\t\t\t\tuassert.Equal(t, int64(1), tok.TotalSupply())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"minting distinct ids to same owner accumulates balance\",\n\t\t\taction: func(led *PrivateLedger) error {\n\t\t\t\turequire.NoError(t, led.Mint(alice, \"1\"))\n\t\t\t\treturn led.Mint(alice, \"2\")\n\t\t\t},\n\t\t\twantErr: nil,\n\t\t\tverify: func(t *testing.T, tok *Token, led *PrivateLedger) {\n\t\t\t\tuassert.Equal(t, int64(2), mustBalanceOf(t, tok, alice))\n\t\t\t\tuassert.Equal(t, int64(2), tok.TotalSupply())\n\t\t\t\tuassert.Equal(t, 1, tok.KnownAccounts())\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\ttc := tc\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\ttok, led := newTestToken(\"Foo\", \"FOO\", 0, cur)\n\t\t\terr := tc.action(led)\n\t\t\tif tc.wantErr == nil {\n\t\t\t\tuassert.NoError(t, err)\n\t\t\t} else {\n\t\t\t\tuassert.ErrorIs(t, err, tc.wantErr)\n\t\t\t}\n\t\t\tif tc.verify != nil {\n\t\t\t\ttc.verify(t, tok, led)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBurn(cur realm, t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\taction  func(led *PrivateLedger) error\n\t\twantErr error\n\t\tverify  func(t *testing.T, tok *Token, led *PrivateLedger)\n\t}{\n\t\t{\n\t\t\tname: \"burn existing token clears all state\",\n\t\t\taction: func(led *PrivateLedger) error {\n\t\t\t\turequire.NoError(t, led.Mint(alice, \"1\"))\n\t\t\t\treturn led.Burn(\"1\")\n\t\t\t},\n\t\t\twantErr: nil,\n\t\t\tverify: func(t *testing.T, tok *Token, led *PrivateLedger) {\n\t\t\t\tuassert.Equal(t, int64(0), tok.TotalSupply())\n\t\t\t\tuassert.Equal(t, int64(0), mustBalanceOf(t, tok, alice))\n\t\t\t\tuassert.Equal(t, 0, tok.KnownAccounts())\n\t\t\t\t_, err := tok.OwnerOf(\"1\")\n\t\t\t\tuassert.ErrorIs(t, err, ErrInvalidTokenId)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"burn clears any outstanding approval\",\n\t\t\taction: func(led *PrivateLedger) error {\n\t\t\t\turequire.NoError(t, led.Mint(alice, \"1\"))\n\t\t\t\turequire.NoError(t, led.Approve(alice, bob, \"1\"))\n\t\t\t\treturn led.Burn(\"1\")\n\t\t\t},\n\t\t\twantErr: nil,\n\t\t\tverify: func(t *testing.T, tok *Token, led *PrivateLedger) {\n\t\t\t\tuassert.Equal(t, 0, led.tokenApprovals.Size())\n\t\t\t\t_, err := tok.GetApproved(\"1\")\n\t\t\t\tuassert.ErrorIs(t, err, ErrInvalidTokenId)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"burn missing token returns ErrInvalidTokenId\",\n\t\t\taction: func(led *PrivateLedger) error {\n\t\t\t\treturn led.Burn(\"999\")\n\t\t\t},\n\t\t\twantErr: ErrInvalidTokenId,\n\t\t},\n\t\t{\n\t\t\tname: \"burn one of two tokens keeps the account known\",\n\t\t\taction: func(led *PrivateLedger) error {\n\t\t\t\turequire.NoError(t, led.Mint(alice, \"1\"))\n\t\t\t\turequire.NoError(t, led.Mint(alice, \"2\"))\n\t\t\t\treturn led.Burn(\"1\")\n\t\t\t},\n\t\t\twantErr: nil,\n\t\t\tverify: func(t *testing.T, tok *Token, led *PrivateLedger) {\n\t\t\t\tuassert.Equal(t, int64(1), mustBalanceOf(t, tok, alice))\n\t\t\t\tuassert.Equal(t, 1, tok.KnownAccounts())\n\t\t\t\tuassert.Equal(t, int64(1), tok.TotalSupply())\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\ttc := tc\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\ttok, led := newTestToken(\"Foo\", \"FOO\", 0, cur)\n\t\t\terr := tc.action(led)\n\t\t\tif tc.wantErr == nil {\n\t\t\t\tuassert.NoError(t, err)\n\t\t\t} else {\n\t\t\t\tuassert.ErrorIs(t, err, tc.wantErr)\n\t\t\t}\n\t\t\tif tc.verify != nil {\n\t\t\t\ttc.verify(t, tok, led)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestApprove(cur realm, t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\taction  func(led *PrivateLedger) error\n\t\twantErr error\n\t\tverify  func(t *testing.T, tok *Token, led *PrivateLedger)\n\t}{\n\t\t{\n\t\t\tname: \"owner approves another account\",\n\t\t\taction: func(led *PrivateLedger) error {\n\t\t\t\turequire.NoError(t, led.Mint(alice, \"1\"))\n\t\t\t\treturn led.Approve(alice, bob, \"1\")\n\t\t\t},\n\t\t\twantErr: nil,\n\t\t\tverify: func(t *testing.T, tok *Token, led *PrivateLedger) {\n\t\t\t\tapproved, err := tok.GetApproved(\"1\")\n\t\t\t\turequire.NoError(t, err)\n\t\t\t\tuassert.Equal(t, bob, approved)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"operator approves on behalf of owner\",\n\t\t\taction: func(led *PrivateLedger) error {\n\t\t\t\turequire.NoError(t, led.Mint(alice, \"1\"))\n\t\t\t\turequire.NoError(t, led.SetApprovalForAll(alice, bob, true))\n\t\t\t\treturn led.Approve(bob, carl, \"1\")\n\t\t\t},\n\t\t\twantErr: nil,\n\t\t\tverify: func(t *testing.T, tok *Token, led *PrivateLedger) {\n\t\t\t\tapproved, err := tok.GetApproved(\"1\")\n\t\t\t\turequire.NoError(t, err)\n\t\t\t\tuassert.Equal(t, carl, approved)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"approve to zero address revokes an existing approval (EIP-721)\",\n\t\t\taction: func(led *PrivateLedger) error {\n\t\t\t\turequire.NoError(t, led.Mint(alice, \"1\"))\n\t\t\t\turequire.NoError(t, led.Approve(alice, bob, \"1\"))\n\t\t\t\treturn led.Approve(alice, zeroAddress, \"1\")\n\t\t\t},\n\t\t\twantErr: nil,\n\t\t\tverify: func(t *testing.T, tok *Token, led *PrivateLedger) {\n\t\t\t\t_, err := tok.GetApproved(\"1\")\n\t\t\t\tuassert.ErrorIs(t, err, ErrTokenIdNotApproved)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"revoke with no prior approval is a no-op\",\n\t\t\taction: func(led *PrivateLedger) error {\n\t\t\t\turequire.NoError(t, led.Mint(alice, \"1\"))\n\t\t\t\treturn led.Approve(alice, zeroAddress, \"1\")\n\t\t\t},\n\t\t\twantErr: nil,\n\t\t\tverify: func(t *testing.T, tok *Token, led *PrivateLedger) {\n\t\t\t\t_, err := tok.GetApproved(\"1\")\n\t\t\t\tuassert.ErrorIs(t, err, ErrTokenIdNotApproved)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"approve on missing token returns ErrInvalidTokenId\",\n\t\t\taction: func(led *PrivateLedger) error {\n\t\t\t\treturn led.Approve(alice, bob, \"999\")\n\t\t\t},\n\t\t\twantErr: ErrInvalidTokenId,\n\t\t},\n\t\t{\n\t\t\tname: \"approve to current owner returns ErrApprovalToCurrentOwner\",\n\t\t\taction: func(led *PrivateLedger) error {\n\t\t\t\turequire.NoError(t, led.Mint(alice, \"1\"))\n\t\t\t\treturn led.Approve(alice, alice, \"1\")\n\t\t\t},\n\t\t\twantErr: ErrApprovalToCurrentOwner,\n\t\t},\n\t\t{\n\t\t\tname: \"non-owner non-operator approve returns ErrCallerIsNotOwnerOrApproved\",\n\t\t\taction: func(led *PrivateLedger) error {\n\t\t\t\turequire.NoError(t, led.Mint(alice, \"1\"))\n\t\t\t\treturn led.Approve(bob, carl, \"1\")\n\t\t\t},\n\t\t\twantErr: ErrCallerIsNotOwnerOrApproved,\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\ttc := tc\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\ttok, led := newTestToken(\"Foo\", \"FOO\", 0, cur)\n\t\t\terr := tc.action(led)\n\t\t\tif tc.wantErr == nil {\n\t\t\t\tuassert.NoError(t, err)\n\t\t\t} else {\n\t\t\t\tuassert.ErrorIs(t, err, tc.wantErr)\n\t\t\t}\n\t\t\tif tc.verify != nil {\n\t\t\t\ttc.verify(t, tok, led)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetApprovalForAll(cur realm, t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\taction  func(led *PrivateLedger) error\n\t\twantErr error\n\t\tverify  func(t *testing.T, tok *Token, led *PrivateLedger)\n\t}{\n\t\t{\n\t\t\tname: \"grant operator approval\",\n\t\t\taction: func(led *PrivateLedger) error {\n\t\t\t\treturn led.SetApprovalForAll(alice, bob, true)\n\t\t\t},\n\t\t\twantErr: nil,\n\t\t\tverify: func(t *testing.T, tok *Token, led *PrivateLedger) {\n\t\t\t\tuassert.True(t, tok.IsApprovedForAll(alice, bob))\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"revoke operator approval releases the node\",\n\t\t\taction: func(led *PrivateLedger) error {\n\t\t\t\turequire.NoError(t, led.SetApprovalForAll(alice, bob, true))\n\t\t\t\treturn led.SetApprovalForAll(alice, bob, false)\n\t\t\t},\n\t\t\twantErr: nil,\n\t\t\tverify: func(t *testing.T, tok *Token, led *PrivateLedger) {\n\t\t\t\tuassert.False(t, tok.IsApprovedForAll(alice, bob))\n\t\t\t\tuassert.Equal(t, 0, led.operatorApprovals.Size())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"revoke without a prior grant stores nothing\",\n\t\t\taction: func(led *PrivateLedger) error {\n\t\t\t\treturn led.SetApprovalForAll(alice, bob, false)\n\t\t\t},\n\t\t\twantErr: nil,\n\t\t\tverify: func(t *testing.T, tok *Token, led *PrivateLedger) {\n\t\t\t\tuassert.False(t, tok.IsApprovedForAll(alice, bob))\n\t\t\t\tuassert.Equal(t, 0, led.operatorApprovals.Size())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"invalid operator returns ErrInvalidAddress\",\n\t\t\taction: func(led *PrivateLedger) error {\n\t\t\t\treturn led.SetApprovalForAll(alice, zeroAddress, true)\n\t\t\t},\n\t\t\twantErr: ErrInvalidAddress,\n\t\t},\n\t\t{\n\t\t\tname: \"owner equals operator returns ErrApprovalToCurrentOwner\",\n\t\t\taction: func(led *PrivateLedger) error {\n\t\t\t\treturn led.SetApprovalForAll(alice, alice, true)\n\t\t\t},\n\t\t\twantErr: ErrApprovalToCurrentOwner,\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\ttc := tc\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\ttok, led := newTestToken(\"Foo\", \"FOO\", 0, cur)\n\t\t\terr := tc.action(led)\n\t\t\tif tc.wantErr == nil {\n\t\t\t\tuassert.NoError(t, err)\n\t\t\t} else {\n\t\t\t\tuassert.ErrorIs(t, err, tc.wantErr)\n\t\t\t}\n\t\t\tif tc.verify != nil {\n\t\t\t\ttc.verify(t, tok, led)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTransferFrom(cur realm, t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\taction  func(led *PrivateLedger) error\n\t\twantErr error\n\t\tverify  func(t *testing.T, tok *Token, led *PrivateLedger)\n\t}{\n\t\t{\n\t\t\tname: \"owner transfers own token\",\n\t\t\taction: func(led *PrivateLedger) error {\n\t\t\t\turequire.NoError(t, led.Mint(alice, \"1\"))\n\t\t\t\treturn led.TransferFrom(alice, alice, bob, \"1\")\n\t\t\t},\n\t\t\twantErr: nil,\n\t\t\tverify: func(t *testing.T, tok *Token, led *PrivateLedger) {\n\t\t\t\towner, err := tok.OwnerOf(\"1\")\n\t\t\t\turequire.NoError(t, err)\n\t\t\t\tuassert.Equal(t, bob, owner)\n\t\t\t\tuassert.Equal(t, int64(0), mustBalanceOf(t, tok, alice))\n\t\t\t\tuassert.Equal(t, int64(1), mustBalanceOf(t, tok, bob))\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"approved spender transfers and approval is cleared\",\n\t\t\taction: func(led *PrivateLedger) error {\n\t\t\t\turequire.NoError(t, led.Mint(alice, \"1\"))\n\t\t\t\turequire.NoError(t, led.Approve(alice, bob, \"1\"))\n\t\t\t\treturn led.TransferFrom(bob, alice, carl, \"1\")\n\t\t\t},\n\t\t\twantErr: nil,\n\t\t\tverify: func(t *testing.T, tok *Token, led *PrivateLedger) {\n\t\t\t\towner, err := tok.OwnerOf(\"1\")\n\t\t\t\turequire.NoError(t, err)\n\t\t\t\tuassert.Equal(t, carl, owner)\n\t\t\t\t_, err = tok.GetApproved(\"1\")\n\t\t\t\tuassert.ErrorIs(t, err, ErrTokenIdNotApproved)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"operator transfers owner token\",\n\t\t\taction: func(led *PrivateLedger) error {\n\t\t\t\turequire.NoError(t, led.Mint(alice, \"1\"))\n\t\t\t\turequire.NoError(t, led.SetApprovalForAll(alice, bob, true))\n\t\t\t\treturn led.TransferFrom(bob, alice, carl, \"1\")\n\t\t\t},\n\t\t\twantErr: nil,\n\t\t\tverify: func(t *testing.T, tok *Token, led *PrivateLedger) {\n\t\t\t\towner, err := tok.OwnerOf(\"1\")\n\t\t\t\turequire.NoError(t, err)\n\t\t\t\tuassert.Equal(t, carl, owner)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"unauthorized spender returns ErrCallerIsNotOwnerOrApproved\",\n\t\t\taction: func(led *PrivateLedger) error {\n\t\t\t\turequire.NoError(t, led.Mint(alice, \"1\"))\n\t\t\t\treturn led.TransferFrom(bob, alice, carl, \"1\")\n\t\t\t},\n\t\t\twantErr: ErrCallerIsNotOwnerOrApproved,\n\t\t},\n\t\t{\n\t\t\tname: \"spender on missing token returns ErrCallerIsNotOwnerOrApproved\",\n\t\t\taction: func(led *PrivateLedger) error {\n\t\t\t\treturn led.TransferFrom(alice, alice, bob, \"999\")\n\t\t\t},\n\t\t\twantErr: ErrCallerIsNotOwnerOrApproved,\n\t\t},\n\t\t{\n\t\t\tname: \"transfer to self returns ErrCannotTransferToSelf\",\n\t\t\taction: func(led *PrivateLedger) error {\n\t\t\t\turequire.NoError(t, led.Mint(alice, \"1\"))\n\t\t\t\treturn led.TransferFrom(alice, alice, alice, \"1\")\n\t\t\t},\n\t\t\twantErr: ErrCannotTransferToSelf,\n\t\t},\n\t\t{\n\t\t\tname: \"transfer from invalid address returns ErrInvalidAddress\",\n\t\t\taction: func(led *PrivateLedger) error {\n\t\t\t\turequire.NoError(t, led.Mint(alice, \"1\"))\n\t\t\t\treturn led.TransferFrom(alice, zeroAddress, bob, \"1\")\n\t\t\t},\n\t\t\twantErr: ErrInvalidAddress,\n\t\t},\n\t\t{\n\t\t\tname: \"transfer to invalid address returns ErrInvalidAddress\",\n\t\t\taction: func(led *PrivateLedger) error {\n\t\t\t\turequire.NoError(t, led.Mint(alice, \"1\"))\n\t\t\t\treturn led.TransferFrom(alice, alice, zeroAddress, \"1\")\n\t\t\t},\n\t\t\twantErr: ErrInvalidAddress,\n\t\t},\n\t\t{\n\t\t\tname: \"transfer from incorrect owner returns ErrTransferFromIncorrectOwner\",\n\t\t\taction: func(led *PrivateLedger) error {\n\t\t\t\turequire.NoError(t, led.Mint(alice, \"1\"))\n\t\t\t\treturn led.TransferFrom(alice, bob, carl, \"1\")\n\t\t\t},\n\t\t\twantErr: ErrTransferFromIncorrectOwner,\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\ttc := tc\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\ttok, led := newTestToken(\"Foo\", \"FOO\", 0, cur)\n\t\t\terr := tc.action(led)\n\t\t\tif tc.wantErr == nil {\n\t\t\t\tuassert.NoError(t, err)\n\t\t\t} else {\n\t\t\t\tuassert.ErrorIs(t, err, tc.wantErr)\n\t\t\t}\n\t\t\tif tc.verify != nil {\n\t\t\t\ttc.verify(t, tok, led)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestReadViews(cur realm, t *testing.T) {\n\ttok, led := newTestToken(\"Foo NFT\", \"FOO\", 0, cur)\n\turequire.NoError(t, led.Mint(alice, \"1\"))\n\turequire.NoError(t, led.Mint(alice, \"2\"))\n\turequire.NoError(t, led.Mint(bob, \"3\"))\n\turequire.NoError(t, led.Approve(alice, carl, \"1\"))\n\turequire.NoError(t, led.SetApprovalForAll(alice, bob, true))\n\n\ttests := []struct {\n\t\tname  string\n\t\tcheck func(t *testing.T)\n\t}{\n\t\t{\"GetName returns collection name\", func(t *testing.T) {\n\t\t\tuassert.Equal(t, \"Foo NFT\", tok.GetName())\n\t\t}},\n\t\t{\"GetSymbol returns collection symbol\", func(t *testing.T) {\n\t\t\tuassert.Equal(t, \"FOO\", tok.GetSymbol())\n\t\t}},\n\t\t{\"ID carries realm, symbol and seqid\", func(t *testing.T) {\n\t\t\tuassert.True(t, strings.HasPrefix(tok.ID(), \"gno.land/\"))\n\t\t\tuassert.True(t, strings.Contains(tok.ID(), \".FOO.\"))\n\t\t}},\n\t\t{\"TotalSupply counts live tokens\", func(t *testing.T) {\n\t\t\tuassert.Equal(t, int64(3), tok.TotalSupply())\n\t\t}},\n\t\t{\"KnownAccounts counts distinct holders\", func(t *testing.T) {\n\t\t\tuassert.Equal(t, 2, tok.KnownAccounts())\n\t\t}},\n\t\t{\"BalanceOf of a holder\", func(t *testing.T) {\n\t\t\tuassert.Equal(t, int64(2), mustBalanceOf(t, tok, alice))\n\t\t}},\n\t\t{\"BalanceOf of a non-holder is zero\", func(t *testing.T) {\n\t\t\tuassert.Equal(t, int64(0), mustBalanceOf(t, tok, carl))\n\t\t}},\n\t\t{\"BalanceOf of the zero address returns ErrInvalidAddress\", func(t *testing.T) {\n\t\t\t_, err := tok.BalanceOf(zeroAddress)\n\t\t\tuassert.ErrorIs(t, err, ErrInvalidAddress)\n\t\t}},\n\t\t{\"OwnerOf existing token\", func(t *testing.T) {\n\t\t\towner, err := tok.OwnerOf(\"3\")\n\t\t\turequire.NoError(t, err)\n\t\t\tuassert.Equal(t, bob, owner)\n\t\t}},\n\t\t{\"OwnerOf missing token returns ErrInvalidTokenId\", func(t *testing.T) {\n\t\t\t_, err := tok.OwnerOf(\"999\")\n\t\t\tuassert.ErrorIs(t, err, ErrInvalidTokenId)\n\t\t}},\n\t\t{\"GetApproved returns the approved account\", func(t *testing.T) {\n\t\t\tapproved, err := tok.GetApproved(\"1\")\n\t\t\turequire.NoError(t, err)\n\t\t\tuassert.Equal(t, carl, approved)\n\t\t}},\n\t\t{\"GetApproved on unapproved token returns ErrTokenIdNotApproved\", func(t *testing.T) {\n\t\t\t_, err := tok.GetApproved(\"2\")\n\t\t\tuassert.ErrorIs(t, err, ErrTokenIdNotApproved)\n\t\t}},\n\t\t{\"GetApproved on missing token returns ErrInvalidTokenId\", func(t *testing.T) {\n\t\t\t_, err := tok.GetApproved(\"999\")\n\t\t\tuassert.ErrorIs(t, err, ErrInvalidTokenId)\n\t\t}},\n\t\t{\"IsApprovedForAll true for operator\", func(t *testing.T) {\n\t\t\tuassert.True(t, tok.IsApprovedForAll(alice, bob))\n\t\t}},\n\t\t{\"IsApprovedForAll false for non-operator\", func(t *testing.T) {\n\t\t\tuassert.False(t, tok.IsApprovedForAll(alice, carl))\n\t\t}},\n\t\t{\"RenderHome renders header and counters\", func(t *testing.T) {\n\t\t\tout := tok.RenderHome()\n\t\t\tuassert.True(t, strings.Contains(out, \"# Foo NFT ($FOO)\"), \"header\")\n\t\t\tuassert.True(t, strings.Contains(out, \"**Total supply**: 3\"), \"supply\")\n\t\t\tuassert.True(t, strings.Contains(out, \"**Known accounts**: 2\"), \"accounts\")\n\t\t}},\n\t}\n\n\tfor _, tc := range tests {\n\t\ttc := tc\n\t\tt.Run(tc.name, tc.check)\n\t}\n}\n\nfunc TestRegisterExtensionNil(cur realm, t *testing.T) {\n\t_, led := newTestToken(\"Foo\", \"FOO\", 0, cur)\n\tuassert.PanicsWithMessage(t, cur, \"grc721: nil extension\", func() {\n\t\tled.RegisterExtension(nil)\n\t})\n}\n\nfunc TestRegisterExtensionRejectsDuplicateKind(cur realm, t *testing.T) {\n\t_, led := newTestToken(\"Foo\", \"FOO\", 0, cur)\n\text := \u0026mockExtension{}\n\tled.RegisterExtension(ext)\n\n\tuassert.PanicsWithMessage(t, cur, \"grc721: extension kind already registered: mock\", func() {\n\t\tled.RegisterExtension(ext)\n\t})\n\tuassert.PanicsWithMessage(t, cur, \"grc721: extension kind already registered: mock\", func() {\n\t\tled.RegisterExtension(\u0026mockExtension{})\n\t})\n}\n\nfunc TestRegisterExtensionRejectsDuplicateKindOfUncomparableType(cur realm, t *testing.T) {\n\t_, led := newTestToken(\"Foo\", \"FOO\", 0, cur)\n\tled.RegisterExtension(journalExtension{seen: []TokenID{\"1\"}})\n\n\tuassert.PanicsWithMessage(t, cur, \"grc721: extension kind already registered: journal\", func() {\n\t\tled.RegisterExtension(journalExtension{seen: []TokenID{\"2\"}})\n\t})\n}\n\nfunc TestExtensionHookFanOut(cur realm, t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\trun    func(t *testing.T, led *PrivateLedger, ext *mockExtension)\n\t\tverify func(t *testing.T, ext *mockExtension)\n\t}{\n\t\t{\n\t\t\tname: \"mint, transfer and burn each notify the extension\",\n\t\t\trun: func(t *testing.T, led *PrivateLedger, ext *mockExtension) {\n\t\t\t\tled.RegisterExtension(ext)\n\t\t\t\turequire.NoError(t, led.Mint(alice, \"1\"))\n\t\t\t\turequire.NoError(t, led.TransferFrom(alice, alice, bob, \"1\"))\n\t\t\t\turequire.NoError(t, led.Burn(\"1\"))\n\t\t\t},\n\t\t\tverify: func(t *testing.T, ext *mockExtension) {\n\t\t\t\tuassert.Equal(t, 1, ext.mints)\n\t\t\t\tuassert.Equal(t, 1, ext.transfers)\n\t\t\t\tuassert.Equal(t, alice, ext.lastTransferFrom)\n\t\t\t\tuassert.Equal(t, bob, ext.lastTransferTo)\n\t\t\t\tuassert.Equal(t, 1, ext.burns)\n\t\t\t\tuassert.Equal(t, \"1\", ext.lastBurn.String())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"failed operations do not notify the extension\",\n\t\t\trun: func(t *testing.T, led *PrivateLedger, ext *mockExtension) {\n\t\t\t\tled.RegisterExtension(ext)\n\t\t\t\tuassert.ErrorIs(t, led.Mint(zeroAddress, \"1\"), ErrInvalidAddress)\n\t\t\t\tuassert.ErrorIs(t, led.Burn(\"999\"), ErrInvalidTokenId)\n\t\t\t},\n\t\t\tverify: func(t *testing.T, ext *mockExtension) {\n\t\t\t\tuassert.Equal(t, 0, ext.mints)\n\t\t\t\tuassert.Equal(t, 0, ext.burns)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\ttc := tc\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t_, led := newTestToken(\"Foo\", \"FOO\", 0, cur)\n\t\t\text := \u0026mockExtension{}\n\t\t\ttc.run(t, led, ext)\n\t\t\ttc.verify(t, ext)\n\t\t})\n\t}\n}\n\nfunc TestReadToken(cur realm, t *testing.T) {\n\ttok, led := newTestToken(\"Foo\", \"FOO\", 0, cur)\n\tuassert.Equal(t, tok.ID(), led.ReadToken().ID())\n\tuassert.True(t, led.ReadToken() == tok, \"ReadToken returns the same *Token\")\n}\n"},{"name":"types.gno","body":"package grc721\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/avl/v0\"\n)\n\ntype TokenID string\n\nfunc (t TokenID) String() string { return string(t) }\n\n// A Teller is a capability that acts as some account whenever it writes.\n// IsCanonicalTeller confirms a Teller was minted by this package — an embedding\n// forgery fails the check — but it does NOT reveal which account the Teller acts\n// as: a caller-scoped CallerTeller and the admin-grade ImpersonateTeller (which\n// acts as an arbitrary address) are both canonical. So it is an authenticity\n// check on the implementation, not an authorization guard on the acting account;\n// a caller accepting a Teller from outside must still establish, out of band, what\n// account that Teller is entitled to act as.\n// safeTransferFrom is omitted: EIP-721's receiver check needs a registry, and aliasing it would imply false safety.\ntype Teller interface {\n\tGetName() string\n\tGetSymbol() string\n\tID() string\n\tTotalSupply() 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\tApprove(_ int, rlm realm, to address, tid TokenID) error\n\tSetApprovalForAll(_ int, rlm realm, operator address, approved bool) error\n\tTransferFrom(_ int, rlm realm, from, to address, tid TokenID) error\n}\n\n// Extension hooks fire on every mint/transfer/burn. This is a trust grant to the\n// issuer, not an EIP-721 feature: only the issuer can attach one (RegisterExtension\n// holds the PrivateLedger), but once attached the hook runs arbitrary issuer code on\n// every movement, a panic in OnMint/OnTransfer/OnBurn aborts the transaction before\n// the movement is announced — an implicit veto over transfers — and gas scales\n// linearly with the extension count. Hooks run after the ledger write so they observe\n// the post-movement state; only a caller that recovers from the panic keeps that write.\n// Holders of a collection's tokens are therefore trusting the issuer not to freeze or\n// tax movement through this surface. Hooks take no rlm params (they cannot capture cur);\n// attach only via RegisterExtension.\ntype Extension interface {\n\tExtensionKind() string // unique; duplicates rejected at register\n\tOnMint(to address, tid TokenID)\n\tOnTransfer(from, to address, tid TokenID)\n\tOnBurn(tid TokenID)\n}\n\ntype ExtensionView interface {\n\tExtensionKind() string\n\tTokenID() string // core Token.ID\n}\n\ntype Token struct {\n\tid     string // raw creation pkgpath + \".\" + symbol + \".\" + id\n\tname   string\n\tsymbol string\n\tledger *PrivateLedger\n\t// origRealm is the PkgPath of the realm that created the token, captured\n\t// unforgeably in NewToken. A frame-relative teller only works there.\n\torigRealm string\n}\n\ntype PrivateLedger struct {\n\ttoken             *Token\n\ttotalSupply       int64\n\towners            avl.Tree // TokenID -\u003e owner address\n\tbalances          avl.Tree // owner address -\u003e int64\n\ttokenApprovals    avl.Tree // TokenID -\u003e approved address\n\toperatorApprovals avl.Tree // \"owner:operator\" -\u003e bool\n\textensions        []Extension\n}\n\nvar (\n\tErrInvalidTokenId             = errors.New(\"invalid token id\")\n\tErrInvalidAddress             = errors.New(\"invalid address\")\n\tErrTokenIdNotApproved         = 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\tErrCannotTransferToSelf       = errors.New(\"cannot send transfer to self\")\n\tErrTransferFromIncorrectOwner = errors.New(\"transfer from incorrect owner\")\n\tErrCallerIsNotOwnerOrApproved = errors.New(\"caller is not token owner or approved\")\n\tErrTokenIdAlreadyExists       = errors.New(\"token id already exists\")\n\tErrReadonly                   = errors.New(\"teller is readonly\")\n\tErrSpoofedRealm               = errors.New(\"rlm does not match the current crossing frame\")\n\tErrForeignCallerTeller        = errors.New(\"frame-relative teller used outside the token's realm\")\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\n// Symbol charset matches grc20reg slug (embedded in Token.ID / events).\nconst (\n\tMaxNameLen   = 64\n\tMaxSymbolLen = 11\n)\n\nconst (\n\t// NewToken announces every token creation; see NewToken for why it is a\n\t// complete provenance signal.\n\tNewTokenEvent = \"NewToken\"\n\t// Mint emits from empty addr; burn emits to empty addr (EIP-721).\n\tTransferEvent       = \"Transfer\"\n\tApprovalEvent       = \"Approval\"\n\tApprovalForAllEvent = \"ApprovalForAll\"\n)\n\nvar zeroAddress = address(\"\")\n\ntype fnTeller struct {\n\taccountFn func(_ int, rlm realm) address\n\t// homeGuard marks a frame-relative teller, whose writes are confined to\n\t// the token's own realm. See fnTeller.guardHome.\n\thomeGuard bool\n\t*Token\n}\n\nvar _ Teller = (*fnTeller)(nil)\n\n// IsCanonicalTeller reports whether t was minted by this package, rejecting Tellers\n// forged by embedding *fnTeller in a wrapper. It does NOT discriminate by capability\n// grade: CallerTeller, ReadonlyTeller, RealmTeller, RealmSubTeller and the admin-grade\n// ImpersonateTeller all return true. Do not treat a true result as proof that a Teller\n// is a caller-scoped capability — establish the acting account separately.\nfunc IsCanonicalTeller(t Teller) bool {\n\t_, ok := t.(*fnTeller)\n\treturn ok\n}\n"},{"name":"veto_no_event_filetest.gno","body":"// PKGPATH: gno.land/r/demo/grc721veto\n\n// Extension hooks run before the Transfer event is emitted, so a hook that vetoes\n// a movement leaves nothing on the event stream. Only the second mint, which no\n// hook refuses, is announced.\npackage grc721veto\n\nimport (\n\t\"gno.land/p/nt/grc721/v0\"\n)\n\ntype vetoExtension struct {\n\trefuse grc721.TokenID\n}\n\nfunc (e *vetoExtension) ExtensionKind() string { return \"veto\" }\n\nfunc (e *vetoExtension) OnMint(to address, tid grc721.TokenID) {\n\tif tid == e.refuse {\n\t\tpanic(\"veto: mint refused\")\n\t}\n}\n\nfunc (e *vetoExtension) OnTransfer(from, to address, tid grc721.TokenID) {}\n\nfunc (e *vetoExtension) OnBurn(tid grc721.TokenID) {}\n\nfunc main(cur realm) {\n\t_, ledger := grc721.NewToken(\"Veto\", \"VETO\", 0, cur)\n\tledger.RegisterExtension(\u0026vetoExtension{refuse: \"1\"})\n\n\tfunc() {\n\t\tdefer func() { println(\"recovered:\", recover()) }()\n\t\tledger.Mint(cur.Address(), \"1\")\n\t}()\n\n\tledger.Mint(cur.Address(), \"2\")\n}\n\n// Output:\n// recovered: veto: mint refused\n\n// Events:\n// [\n//   {\n//     \"type\": \"NewToken\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc721veto.VETO.0000000\"\n//       },\n//       {\n//         \"key\": \"name\",\n//         \"value\": \"Veto\"\n//       },\n//       {\n//         \"key\": \"symbol\",\n//         \"value\": \"VETO\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc721/v0\"\n//   },\n//   {\n//     \"type\": \"Transfer\",\n//     \"attrs\": [\n//       {\n//         \"key\": \"token\",\n//         \"value\": \"gno.land/r/demo/grc721veto.VETO.0000000\"\n//       },\n//       {\n//         \"key\": \"from\",\n//         \"value\": \"\"\n//       },\n//       {\n//         \"key\": \"to\",\n//         \"value\": \"g1dywcn8v2jdpl6fuks8tw3l8gnzer0hv8mfkraa\"\n//       },\n//       {\n//         \"key\": \"tokenId\",\n//         \"value\": \"2\"\n//       }\n//     ],\n//     \"pkg_path\": \"gno.land/p/nt/grc721/v0\"\n//   }\n// ]\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"sKHj1uJgEMpoPORW8ZV5wVDe885TSTF8lmcNUzxCDb988nApPviI2PQlqXPv4t8PBzZBtiO//tx+gy57ypj1Uw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"enumerable","path":"gno.land/p/nt/grc721/enumerable/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/grc721/enumerable/v0\"\ngno = \"0.9\"\n"},{"name":"token.gno","body":"package enumerable\n\nimport (\n\t\"gno.land/p/nt/grc721/v0\"\n)\n\nconst Kind = \"enumerable\"\n\n// NewEnumerable attaches an enumerable extension and registers it as a core\n// Extension. Attach it before the first mint so every token is indexed.\nfunc NewEnumerable(coreLedger *grc721.PrivateLedger) (*Enumerable, *Ledger) {\n\tif coreLedger == nil {\n\t\tpanic(\"enumerable: nil core ledger\")\n\t}\n\n\tst := \u0026storage{}\n\tcore := coreLedger.ReadToken()\n\tled := \u0026Ledger{core: core, st: st}\n\tcoreLedger.RegisterExtension(led)\n\n\treturn \u0026Enumerable{core: core, st: st}, led\n}\n\nfunc (e *Enumerable) ExtensionKind() string {\n\treturn Kind\n}\n\nfunc (e *Enumerable) TokenID() string {\n\treturn e.core.ID()\n}\n\nfunc (e *Enumerable) TotalSupply() int64 {\n\treturn int64(len(e.st.allTokens))\n}\n\nfunc (e *Enumerable) TokenByIndex(index int64) (grc721.TokenID, error) {\n\tif index \u003c 0 || index \u003e= int64(len(e.st.allTokens)) {\n\t\treturn \"\", ErrIndexOutOfRange\n\t}\n\n\treturn e.st.allTokens[index], nil\n}\n\nfunc (e *Enumerable) TokenOfOwnerByIndex(owner address, index int64) (grc721.TokenID, error) {\n\tlist := e.ownedList(owner)\n\tif list == nil || index \u003c 0 || index \u003e= int64(len(list.ids)) {\n\t\treturn \"\", ErrIndexOutOfRange\n\t}\n\n\treturn list.ids[index], nil\n}\n\nfunc (e *Enumerable) ownedList(owner address) *tokenList {\n\tv := e.st.owned.Get(owner.String())\n\tif v == nil {\n\t\treturn nil\n\t}\n\n\treturn v.(*tokenList)\n}\n\nfunc (led *Ledger) ExtensionKind() string { return Kind }\n\nfunc (led *Ledger) OnMint(to address, tid grc721.TokenID) {\n\tled.addToAll(tid)\n\tled.addToOwner(to, tid)\n}\n\nfunc (led *Ledger) OnTransfer(from, to address, tid grc721.TokenID) {\n\tled.removeFromOwner(from, tid)\n\tled.addToOwner(to, tid)\n}\n\nfunc (led *Ledger) OnBurn(tid grc721.TokenID) {\n\tif v := led.st.owner.Get(tid.String()); v != nil {\n\t\tled.removeFromOwner(v.(address), tid)\n\t}\n\n\tled.removeFromAll(tid)\n}\n\nfunc (led *Ledger) addToAll(tid grc721.TokenID) {\n\tled.st.allIndex.Set(tid.String(), len(led.st.allTokens))\n\tled.st.allTokens = append(led.st.allTokens, tid)\n}\n\n// Swap-and-pop: move the last id into the freed slot, then truncate.\nfunc (led *Ledger) removeFromAll(tid grc721.TokenID) {\n\ttidStr := tid.String()\n\n\tv := led.st.allIndex.Get(tidStr)\n\tif v == nil {\n\t\treturn\n\t}\n\n\tidx := v.(int)\n\n\tlast := len(led.st.allTokens) - 1\n\tif idx != last {\n\t\tmoved := led.st.allTokens[last]\n\t\tled.st.allTokens[idx] = moved\n\t\tled.st.allIndex.Set(moved.String(), idx)\n\t}\n\n\tled.st.allTokens = led.st.allTokens[:last]\n\n\tled.st.allIndex.Remove(tidStr)\n}\n\nfunc (led *Ledger) addToOwner(owner address, tid grc721.TokenID) {\n\townerStr := owner.String()\n\n\tvar list *tokenList\n\tif v := led.st.owned.Get(ownerStr); v != nil {\n\t\tlist = v.(*tokenList)\n\t} else {\n\t\tlist = \u0026tokenList{}\n\t\tled.st.owned.Set(ownerStr, list)\n\t}\n\n\tled.st.ownedIndex.Set(tid.String(), len(list.ids))\n\tlist.ids = append(list.ids, tid)\n\tled.st.owner.Set(tid.String(), owner)\n}\n\n// Swap-and-pop within the owner list; drops the owner entry when it empties.\nfunc (led *Ledger) removeFromOwner(owner address, tid grc721.TokenID) {\n\townerStr := owner.String()\n\n\tv := led.st.owned.Get(ownerStr)\n\tif v == nil {\n\t\treturn\n\t}\n\n\tlist := v.(*tokenList)\n\ttidStr := tid.String()\n\n\tiv := led.st.ownedIndex.Get(tidStr)\n\tif iv == nil {\n\t\treturn\n\t}\n\n\tidx := iv.(int)\n\n\tlast := len(list.ids) - 1\n\tif idx != last {\n\t\tmoved := list.ids[last]\n\t\tlist.ids[idx] = moved\n\t\tled.st.ownedIndex.Set(moved.String(), idx)\n\t}\n\n\tlist.ids = list.ids[:last]\n\n\tled.st.ownedIndex.Remove(tidStr)\n\n\tif len(list.ids) == 0 {\n\t\tled.st.owned.Remove(ownerStr)\n\t}\n\n\tled.st.owner.Remove(tidStr)\n}\n"},{"name":"token_test.gno","body":"package enumerable\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/grc721/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\n// Same-realm cross: NewToken requires rlm.IsCurrent().\nfunc newCore(name, symbol string, id seqid.ID, rlm realm) (tok *grc721.Token, led *grc721.PrivateLedger) {\n\tfunc(cur realm) {\n\t\ttok, led = grc721.NewToken(name, symbol, id, cur)\n\t}(cross(rlm))\n\n\treturn\n}\n\nfunc newEnum(cur realm) (enum *Enumerable, core *grc721.PrivateLedger, led *Ledger) {\n\t_, coreLedger := newCore(\"Foo\", \"FOO\", 0, cur)\n\tenum, led = NewEnumerable(coreLedger)\n\treturn enum, coreLedger, led\n}\n\nfunc globalOrder(t *testing.T, enum *Enumerable) []string {\n\tt.Helper()\n\tout := []string{}\n\tfor i := int64(0); i \u003c enum.TotalSupply(); i++ {\n\t\ttid, err := enum.TokenByIndex(i)\n\t\turequire.NoError(t, err)\n\t\tout = append(out, tid.String())\n\t}\n\treturn out\n}\n\nfunc ownerOrder(t *testing.T, enum *Enumerable, owner address) []string {\n\tt.Helper()\n\tout := []string{}\n\tfor i := int64(0); ; i++ {\n\t\ttid, err := enum.TokenOfOwnerByIndex(owner, i)\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\t\tout = append(out, tid.String())\n\t}\n\treturn out\n}\n\nfunc eqStrings(t *testing.T, name string, got, want []string) {\n\tt.Helper()\n\tif len(got) != len(want) {\n\t\tt.Errorf(\"%s: length got %d want %d (%v vs %v)\", name, len(got), len(want), got, want)\n\t\treturn\n\t}\n\tfor i := range want {\n\t\tuassert.Equal(t, want[i], got[i])\n\t}\n}\n\nfunc TestNewEnumerable(cur realm, t *testing.T) {\n\tuassert.PanicsWithMessage(t, cur, \"enumerable: nil core ledger\", func() {\n\t\tNewEnumerable(nil)\n\t})\n\n\ttok, coreLedger := newCore(\"Foo\", \"FOO\", 0, cur)\n\tenum, led := NewEnumerable(coreLedger)\n\n\turequire.True(t, enum != nil, \"enum built\")\n\turequire.True(t, led != nil, \"ledger built\")\n\n\ttests := []struct {\n\t\tname string\n\t\tgot  string\n\t\twant string\n\t}{\n\t\t{\"ExtensionKind is enumerable\", enum.ExtensionKind(), Kind},\n\t\t{\"ExtensionKind constant value\", Kind, \"enumerable\"},\n\t\t{\"TokenID matches core token id\", enum.TokenID(), tok.ID()},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tuassert.Equal(t, tt.want, tt.got)\n\t\t})\n\t}\n\n\tuassert.Equal(t, int64(0), enum.TotalSupply())\n}\n\nfunc TestReadIndices(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\tcarol := testutils.TestAddress(\"carol\")\n\n\tenum, coreLedger, _ := newEnum(cur)\n\turequire.NoError(t, coreLedger.Mint(alice, \"1\"))\n\turequire.NoError(t, coreLedger.Mint(alice, \"2\"))\n\turequire.NoError(t, coreLedger.Mint(bob, \"3\"))\n\n\tuassert.Equal(t, int64(3), enum.TotalSupply())\n\n\ttests := []struct {\n\t\tname string\n\t\trun  func(t *testing.T)\n\t}{\n\t\t{\"TokenByIndex 0 returns first minted\", func(t *testing.T) {\n\t\t\ttid, err := enum.TokenByIndex(0)\n\t\t\turequire.NoError(t, err)\n\t\t\tuassert.Equal(t, \"1\", tid.String())\n\t\t}},\n\t\t{\"TokenByIndex last returns last minted\", func(t *testing.T) {\n\t\t\ttid, err := enum.TokenByIndex(2)\n\t\t\turequire.NoError(t, err)\n\t\t\tuassert.Equal(t, \"3\", tid.String())\n\t\t}},\n\t\t{\"TokenByIndex negative returns ErrIndexOutOfRange\", func(t *testing.T) {\n\t\t\t_, err := enum.TokenByIndex(-1)\n\t\t\tuassert.ErrorIs(t, err, ErrIndexOutOfRange)\n\t\t}},\n\t\t{\"TokenByIndex out of range returns ErrIndexOutOfRange\", func(t *testing.T) {\n\t\t\t_, err := enum.TokenByIndex(3)\n\t\t\tuassert.ErrorIs(t, err, ErrIndexOutOfRange)\n\t\t}},\n\t\t{\"TokenOfOwnerByIndex returns owner's token\", func(t *testing.T) {\n\t\t\ttid, err := enum.TokenOfOwnerByIndex(alice, 1)\n\t\t\turequire.NoError(t, err)\n\t\t\tuassert.Equal(t, \"2\", tid.String())\n\t\t}},\n\t\t{\"TokenOfOwnerByIndex unknown owner returns ErrIndexOutOfRange\", func(t *testing.T) {\n\t\t\t_, err := enum.TokenOfOwnerByIndex(carol, 0)\n\t\t\tuassert.ErrorIs(t, err, ErrIndexOutOfRange)\n\t\t}},\n\t\t{\"TokenOfOwnerByIndex negative index returns ErrIndexOutOfRange\", func(t *testing.T) {\n\t\t\t_, err := enum.TokenOfOwnerByIndex(alice, -1)\n\t\t\tuassert.ErrorIs(t, err, ErrIndexOutOfRange)\n\t\t}},\n\t\t{\"TokenOfOwnerByIndex past end returns ErrIndexOutOfRange\", func(t *testing.T) {\n\t\t\t_, err := enum.TokenOfOwnerByIndex(alice, 2)\n\t\t\tuassert.ErrorIs(t, err, ErrIndexOutOfRange)\n\t\t}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, tt.run)\n\t}\n\n\teqStrings(t, \"global order\", globalOrder(t, enum), []string{\"1\", \"2\", \"3\"})\n\teqStrings(t, \"alice order\", ownerOrder(t, enum, alice), []string{\"1\", \"2\"})\n\teqStrings(t, \"bob order\", ownerOrder(t, enum, bob), []string{\"3\"})\n}\n\nfunc TestBurnGlobalSwapAndPop(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\n\ttests := []struct {\n\t\tname      string\n\t\tburn      grc721.TokenID\n\t\twantOrder []string\n\t}{\n\t\t{\"burn last token truncates global list (idx == last)\", \"3\", []string{\"1\", \"2\"}},\n\t\t{\"burn middle token swap-and-pops the global list (idx != last)\", \"2\", []string{\"1\", \"3\"}},\n\t\t{\"burn first token swap-and-pops the global list (idx != last)\", \"1\", []string{\"3\", \"2\"}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tenum, coreLedger, _ := newEnum(cur)\n\t\t\turequire.NoError(t, coreLedger.Mint(alice, \"1\"))\n\t\t\turequire.NoError(t, coreLedger.Mint(alice, \"2\"))\n\t\t\turequire.NoError(t, coreLedger.Mint(alice, \"3\"))\n\n\t\t\turequire.NoError(t, coreLedger.Burn(tt.burn))\n\n\t\t\tuassert.Equal(t, int64(2), enum.TotalSupply())\n\t\t\teqStrings(t, tt.name, globalOrder(t, enum), tt.wantOrder)\n\t\t})\n\t}\n}\n\nfunc TestBurnOwnerSwapAndPop(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\n\ttests := []struct {\n\t\tname       string\n\t\tmint       []grc721.TokenID\n\t\tburn       grc721.TokenID\n\t\twantOwner  []string\n\t\townerEmpty bool\n\t}{\n\t\t{\n\t\t\tname:      \"burn first of owner swap-and-pops per-owner list (idx != last)\",\n\t\t\tmint:      []grc721.TokenID{\"1\", \"2\", \"3\"},\n\t\t\tburn:      \"1\",\n\t\t\twantOwner: []string{\"3\", \"2\"},\n\t\t},\n\t\t{\n\t\t\tname:      \"burn middle of owner swap-and-pops per-owner list (idx != last)\",\n\t\t\tmint:      []grc721.TokenID{\"1\", \"2\", \"3\"},\n\t\t\tburn:      \"2\",\n\t\t\twantOwner: []string{\"1\", \"3\"},\n\t\t},\n\t\t{\n\t\t\tname:      \"burn last of owner truncates per-owner list (idx == last)\",\n\t\t\tmint:      []grc721.TokenID{\"1\", \"2\", \"3\"},\n\t\t\tburn:      \"3\",\n\t\t\twantOwner: []string{\"1\", \"2\"},\n\t\t},\n\t\t{\n\t\t\tname:       \"burn only token empties per-owner list (owned.Remove)\",\n\t\t\tmint:       []grc721.TokenID{\"1\"},\n\t\t\tburn:       \"1\",\n\t\t\townerEmpty: true,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tenum, coreLedger, _ := newEnum(cur)\n\t\t\tfor _, tid := range tt.mint {\n\t\t\t\turequire.NoError(t, coreLedger.Mint(alice, tid))\n\t\t\t}\n\n\t\t\turequire.NoError(t, coreLedger.Burn(tt.burn))\n\n\t\t\tif tt.ownerEmpty {\n\t\t\t\t_, err := enum.TokenOfOwnerByIndex(alice, 0)\n\t\t\t\tuassert.ErrorIs(t, err, ErrIndexOutOfRange)\n\t\t\t\treturn\n\t\t\t}\n\t\t\teqStrings(t, tt.name, ownerOrder(t, enum, alice), tt.wantOwner)\n\t\t})\n\t}\n}\n\nfunc TestTransfer(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\n\tenum, coreLedger, _ := newEnum(cur)\n\turequire.NoError(t, coreLedger.Mint(alice, \"1\"))\n\turequire.NoError(t, coreLedger.Mint(alice, \"2\"))\n\turequire.NoError(t, coreLedger.Mint(bob, \"3\"))\n\n\ttests := []struct {\n\t\tname string\n\t\trun  func(t *testing.T)\n\t}{\n\t\t{\"transfer 1 alice-\u003ebob swap-and-pops alice list\", func(t *testing.T) {\n\t\t\turequire.NoError(t, coreLedger.TransferFrom(alice, alice, bob, \"1\"))\n\t\t\teqStrings(t, \"alice after transfer\", ownerOrder(t, enum, alice), []string{\"2\"})\n\t\t\teqStrings(t, \"bob after transfer\", ownerOrder(t, enum, bob), []string{\"3\", \"1\"})\n\t\t}},\n\t\t{\"transfer 2 alice-\u003ebob empties alice list\", func(t *testing.T) {\n\t\t\turequire.NoError(t, coreLedger.TransferFrom(alice, alice, bob, \"2\"))\n\t\t\t_, err := enum.TokenOfOwnerByIndex(alice, 0)\n\t\t\tuassert.ErrorIs(t, err, ErrIndexOutOfRange)\n\t\t\teqStrings(t, \"bob owns all\", ownerOrder(t, enum, bob), []string{\"3\", \"1\", \"2\"})\n\t\t}},\n\t\t{\"global supply unchanged by transfers\", func(t *testing.T) {\n\t\t\tuassert.Equal(t, int64(3), enum.TotalSupply())\n\t\t}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, tt.run)\n\t}\n}\n\nfunc TestDefensiveNoOps(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\n\tenum, coreLedger, led := newEnum(cur)\n\turequire.NoError(t, coreLedger.Mint(alice, \"1\"))\n\n\ttests := []struct {\n\t\tname string\n\t\trun  func(t *testing.T)\n\t}{\n\t\t{\"removeFromAll unknown id is a no-op\", func(t *testing.T) {\n\t\t\tled.removeFromAll(\"999\")\n\t\t\tuassert.Equal(t, int64(1), enum.TotalSupply())\n\t\t}},\n\t\t{\"removeFromOwner unknown owner is a no-op\", func(t *testing.T) {\n\t\t\tled.removeFromOwner(bob, \"1\")\n\t\t\teqStrings(t, \"alice untouched\", ownerOrder(t, enum, alice), []string{\"1\"})\n\t\t}},\n\t\t{\"removeFromOwner known owner unknown id is a no-op\", func(t *testing.T) {\n\t\t\tled.removeFromOwner(alice, \"999\")\n\t\t\teqStrings(t, \"alice untouched\", ownerOrder(t, enum, alice), []string{\"1\"})\n\t\t}},\n\t\t{\"OnBurn of never-minted id is a no-op\", func(t *testing.T) {\n\t\t\tled.OnBurn(\"neverminted\")\n\t\t\tuassert.Equal(t, int64(1), enum.TotalSupply())\n\t\t\teqStrings(t, \"alice untouched\", ownerOrder(t, enum, alice), []string{\"1\"})\n\t\t}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, tt.run)\n\t}\n}\n"},{"name":"types.gno","body":"// Package enumerable is a stackable GRC721 extension implementing\n// EIP-721Enumerable. Attach it via NewEnumerable before the first mint.\npackage enumerable\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/grc721/v0\"\n)\n\nvar ErrIndexOutOfRange = errors.New(\"index out of range\")\n\n// Stored by pointer so in-place swap-and-pop updates persist.\ntype tokenList struct {\n\tids []grc721.TokenID\n}\n\ntype storage struct {\n\tallTokens  []grc721.TokenID\n\tallIndex   avl.Tree // grc721.TokenID -\u003e int (position in allTokens)\n\towned      avl.Tree // owner address -\u003e *tokenList\n\townedIndex avl.Tree // grc721.TokenID -\u003e int (position within its owner list)\n\towner      avl.Tree // grc721.TokenID -\u003e owner address\n}\n\ntype Enumerable struct {\n\tcore *grc721.Token\n\tst   *storage\n}\n\ntype Ledger struct {\n\tcore *grc721.Token\n\tst   *storage\n}\n\nvar _ grc721.Extension = (*Ledger)(nil)\n\nvar _ grc721.ExtensionView = (*Enumerable)(nil)\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"WBqA9mj5qQ/RGe+7kPQXGu43zWJzYB2B6qWLM+weBYkh3+H2E0NxsSC+CXDpn1Uv2h+JmwllZJ3CtgrYkNShyw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"metadata","path":"gno.land/p/nt/grc721/metadata/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/grc721/metadata/v0\"\ngno = \"0.9\"\n"},{"name":"token.gno","body":"package metadata\n\nimport (\n\t\"chain\"\n\n\t\"gno.land/p/nt/grc721/v0\"\n)\n\nconst Kind = \"metadata\"\n\n// NewMetadata attaches a metadata extension and registers it as a core Extension.\n// Attach it before the first mint so every token is indexed.\nfunc NewMetadata(coreLedger *grc721.PrivateLedger) (*Metadata, *Ledger) {\n\tif coreLedger == nil {\n\t\tpanic(\"metadata: nil core ledger\")\n\t}\n\n\tst := \u0026storage{}\n\tcore := coreLedger.ReadToken()\n\tled := \u0026Ledger{core: core, st: st}\n\tcoreLedger.RegisterExtension(led)\n\n\treturn \u0026Metadata{core: core, st: st}, led\n}\n\nfunc (m *Metadata) ExtensionKind() string { return Kind }\n\nfunc (m *Metadata) TokenID() string { return m.core.ID() }\n\nfunc (m *Metadata) TokenURI(tid grc721.TokenID) (string, error) {\n\tv := m.st.uris.Get(tid.String())\n\tif v == nil {\n\t\treturn \"\", ErrNoTokenURI\n\t}\n\n\treturn v.(string), nil\n}\n\nfunc (m *Metadata) TokenMetadata(tid grc721.TokenID) (Data, error) {\n\tv := m.st.data.Get(tid.String())\n\tif v == nil {\n\t\treturn Data{}, ErrNoMetadata\n\t}\n\n\treturn v.(Data), nil\n}\n\nfunc (m *Metadata) HasTokenURI(tid grc721.TokenID) bool {\n\treturn m.st.uris.Has(tid.String())\n}\n\nfunc (m *Metadata) HasMetadata(tid grc721.TokenID) bool {\n\treturn m.st.data.Has(tid.String())\n}\n\n// SetTokenURI sets the EIP-721 tokenURI of tid; issuer-only so a holder cannot\n// rewrite the URI after acquisition.\nfunc (led *Ledger) SetTokenURI(tid grc721.TokenID, uri string) error {\n\tif _, err := led.core.OwnerOf(tid); err != nil {\n\t\treturn ErrTokenNotMinted\n\t}\n\n\tled.st.uris.Set(tid.String(), uri)\n\n\tchain.Emit(\n\t\tTokenURIUpdateEvent,\n\t\t\"token\", led.core.ID(),\n\t\t\"tokenId\", tid.String(),\n\t\t\"uri\", uri,\n\t)\n\n\treturn nil\n}\n\n// SetTokenMetadata sets the OpenSea on-chain metadata of tid; issuer-only.\nfunc (led *Ledger) SetTokenMetadata(tid grc721.TokenID, data Data) error {\n\tif _, err := led.core.OwnerOf(tid); err != nil {\n\t\treturn ErrTokenNotMinted\n\t}\n\n\tled.st.data.Set(tid.String(), data)\n\n\tchain.Emit(\n\t\tMetadataUpdateEvent,\n\t\t\"token\", led.core.ID(),\n\t\t\"tokenId\", tid.String(),\n\t)\n\n\treturn nil\n}\n\nfunc (led *Ledger) ExtensionKind() string { return Kind }\n\nfunc (led *Ledger) OnMint(to address, tid grc721.TokenID)           {}\nfunc (led *Ledger) OnTransfer(from, to address, tid grc721.TokenID) {}\n\n// OnBurn clears per-token state so it cannot resurface on re-mint.\nfunc (led *Ledger) OnBurn(tid grc721.TokenID) {\n\ttidStr := tid.String()\n\tled.st.uris.Remove(tidStr)\n\tled.st.data.Remove(tidStr)\n}\n"},{"name":"token_test.gno","body":"package metadata\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/grc721/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\n// newToken builds via same-realm cross (NewToken requires rlm.IsCurrent()).\nfunc newToken(name, symbol string, id seqid.ID, rlm realm) (tok *grc721.Token, led *grc721.PrivateLedger) {\n\tfunc(cur realm) {\n\t\ttok, led = grc721.NewToken(name, symbol, id, cur)\n\t}(cross(rlm))\n\treturn\n}\n\nfunc newMeta(id seqid.ID, rlm realm) (*grc721.PrivateLedger, *Metadata, *Ledger) {\n\t_, tokenLedger := newToken(\"Foo\", \"FOO\", id, rlm)\n\tmeta, metaLedger := NewMetadata(tokenLedger)\n\treturn tokenLedger, meta, metaLedger\n}\n\nfunc TestNewMetadata(cur realm, t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tnilLedger bool\n\t}{\n\t\t{\"nil token ledger panics\", true},\n\t\t{\"valid token ledger constructs read view and ledger\", false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.nilLedger {\n\t\t\t\tuassert.PanicsWithMessage(t, cur, \"metadata: nil core ledger\", func() {\n\t\t\t\t\tNewMetadata(nil)\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t_, tokenLedger := newToken(\"Foo\", \"FOO\", 0, cur)\n\t\t\tmeta, metaLedger := NewMetadata(tokenLedger)\n\n\t\t\tuassert.True(t, meta != nil)\n\t\t\tuassert.True(t, metaLedger != nil)\n\t\t\tuassert.Equal(t, Kind, meta.ExtensionKind())\n\t\t\tuassert.Equal(t, \"metadata\", meta.ExtensionKind())\n\t\t\tuassert.Equal(t, tokenLedger.ReadToken().ID(), meta.TokenID())\n\t\t})\n\t}\n}\n\nfunc TestSetTokenURI(cur realm, t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tmint    bool\n\t\ttid     grc721.TokenID\n\t\turi     string\n\t\twantErr error\n\t}{\n\t\t{\"unminted token returns ErrTokenNotMinted\", false, \"404\", \"ipfs://x\", ErrTokenNotMinted},\n\t\t{\"minted token sets uri\", true, \"1\", \"ipfs://uri-1\", nil},\n\t\t{\"empty uri on minted token is allowed\", true, \"1\", \"\", nil},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttokenLedger, meta, metaLedger := newMeta(0, cur)\n\t\t\tif tt.mint {\n\t\t\t\turequire.NoError(t, tokenLedger.Mint(testutils.TestAddress(\"alice\"), tt.tid))\n\t\t\t}\n\n\t\t\terr := metaLedger.SetTokenURI(tt.tid, tt.uri)\n\t\t\tif tt.wantErr != nil {\n\t\t\t\tuassert.ErrorIs(t, err, tt.wantErr)\n\t\t\t\tuassert.False(t, meta.HasTokenURI(tt.tid))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.NoError(t, err)\n\t\t\tuassert.True(t, meta.HasTokenURI(tt.tid))\n\t\t\tgot, err := meta.TokenURI(tt.tid)\n\t\t\turequire.NoError(t, err)\n\t\t\tuassert.Equal(t, tt.uri, got)\n\t\t})\n\t}\n}\n\nfunc TestSetTokenURIOverwrite(cur realm, t *testing.T) {\n\ttokenLedger, meta, metaLedger := newMeta(0, cur)\n\turequire.NoError(t, tokenLedger.Mint(testutils.TestAddress(\"alice\"), \"1\"))\n\n\turequire.NoError(t, metaLedger.SetTokenURI(\"1\", \"ipfs://first\"))\n\turequire.NoError(t, metaLedger.SetTokenURI(\"1\", \"ipfs://second\"))\n\n\tgot, err := meta.TokenURI(\"1\")\n\turequire.NoError(t, err)\n\tuassert.Equal(t, \"ipfs://second\", got)\n}\n\nfunc TestSetTokenMetadata(cur realm, t *testing.T) {\n\tfull := Data{\n\t\tImage:           \"ipfs://img-1\",\n\t\tImageData:       \"\u003csvg/\u003e\",\n\t\tExternalURL:     \"https://example.com/1\",\n\t\tDescription:     \"first\",\n\t\tName:            \"Token One\",\n\t\tAttributes:      []Trait{{DisplayType: \"string\", TraitType: \"rarity\", Value: \"rare\"}},\n\t\tBackgroundColor: \"ffffff\",\n\t\tAnimationURL:    \"ipfs://anim-1\",\n\t\tYoutubeURL:      \"https://youtu.be/abc\",\n\t}\n\n\ttests := []struct {\n\t\tname    string\n\t\tmint    bool\n\t\ttid     grc721.TokenID\n\t\tdata    Data\n\t\twantErr error\n\t}{\n\t\t{\"unminted token returns ErrTokenNotMinted\", false, \"404\", Data{Name: \"x\"}, ErrTokenNotMinted},\n\t\t{\"minted token stores full metadata\", true, \"1\", full, nil},\n\t\t{\"minted token stores zero-value metadata\", true, \"2\", Data{}, nil},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttokenLedger, meta, metaLedger := newMeta(0, cur)\n\t\t\tif tt.mint {\n\t\t\t\turequire.NoError(t, tokenLedger.Mint(testutils.TestAddress(\"alice\"), tt.tid))\n\t\t\t}\n\n\t\t\terr := metaLedger.SetTokenMetadata(tt.tid, tt.data)\n\t\t\tif tt.wantErr != nil {\n\t\t\t\tuassert.ErrorIs(t, err, tt.wantErr)\n\t\t\t\tuassert.False(t, meta.HasMetadata(tt.tid))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.NoError(t, err)\n\t\t\tuassert.True(t, meta.HasMetadata(tt.tid))\n\t\t\tgot, err := meta.TokenMetadata(tt.tid)\n\t\t\turequire.NoError(t, err)\n\t\t\tuassert.Equal(t, tt.data.Name, got.Name)\n\t\t\tuassert.Equal(t, tt.data.Description, got.Description)\n\t\t\tuassert.Equal(t, tt.data.Image, got.Image)\n\t\t\tuassert.Equal(t, len(tt.data.Attributes), len(got.Attributes))\n\t\t\tif len(tt.data.Attributes) \u003e 0 {\n\t\t\t\tuassert.Equal(t, tt.data.Attributes[0].Value, got.Attributes[0].Value)\n\t\t\t\tuassert.Equal(t, tt.data.Attributes[0].TraitType, got.Attributes[0].TraitType)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetTokenMetadataOverwrite(cur realm, t *testing.T) {\n\ttokenLedger, meta, metaLedger := newMeta(0, cur)\n\turequire.NoError(t, tokenLedger.Mint(testutils.TestAddress(\"alice\"), \"1\"))\n\n\turequire.NoError(t, metaLedger.SetTokenMetadata(\"1\", Data{Name: \"first\"}))\n\turequire.NoError(t, metaLedger.SetTokenMetadata(\"1\", Data{Name: \"second\"}))\n\n\tgot, err := meta.TokenMetadata(\"1\")\n\turequire.NoError(t, err)\n\tuassert.Equal(t, \"second\", got.Name)\n}\n\nfunc TestReadsOnUnsetToken(cur realm, t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tfn   func(t *testing.T, meta *Metadata)\n\t}{\n\t\t{\"TokenURI on unset token returns ErrNoTokenURI\", func(t *testing.T, meta *Metadata) {\n\t\t\turi, err := meta.TokenURI(\"1\")\n\t\t\tuassert.Equal(t, \"\", uri)\n\t\t\tuassert.ErrorIs(t, err, ErrNoTokenURI)\n\t\t}},\n\t\t{\"TokenMetadata on unset token returns ErrNoMetadata\", func(t *testing.T, meta *Metadata) {\n\t\t\tdata, err := meta.TokenMetadata(\"1\")\n\t\t\tuassert.Equal(t, \"\", data.Name)\n\t\t\tuassert.ErrorIs(t, err, ErrNoMetadata)\n\t\t}},\n\t\t{\"HasTokenURI is false on unset token\", func(t *testing.T, meta *Metadata) {\n\t\t\tuassert.False(t, meta.HasTokenURI(\"1\"))\n\t\t}},\n\t\t{\"HasMetadata is false on unset token\", func(t *testing.T, meta *Metadata) {\n\t\t\tuassert.False(t, meta.HasMetadata(\"1\"))\n\t\t}},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t_, meta, _ := newMeta(0, cur)\n\t\t\ttt.fn(t, meta)\n\t\t})\n\t}\n}\n\nfunc TestOnBurnClearsMetadata(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\ttokenLedger, meta, metaLedger := newMeta(0, cur)\n\n\turequire.NoError(t, tokenLedger.Mint(alice, \"1\"))\n\turequire.NoError(t, metaLedger.SetTokenURI(\"1\", \"ipfs://uri-1\"))\n\turequire.NoError(t, metaLedger.SetTokenMetadata(\"1\", Data{Name: \"one\"}))\n\tuassert.True(t, meta.HasTokenURI(\"1\"))\n\tuassert.True(t, meta.HasMetadata(\"1\"))\n\n\turequire.NoError(t, tokenLedger.Burn(\"1\"))\n\tuassert.False(t, meta.HasTokenURI(\"1\"))\n\tuassert.False(t, meta.HasMetadata(\"1\"))\n\n\t_, err := meta.TokenURI(\"1\")\n\tuassert.ErrorIs(t, err, ErrNoTokenURI)\n\t_, err = meta.TokenMetadata(\"1\")\n\tuassert.ErrorIs(t, err, ErrNoMetadata)\n\n\turequire.NoError(t, tokenLedger.Mint(alice, \"1\"))\n\tuassert.False(t, meta.HasTokenURI(\"1\"))\n\tuassert.False(t, meta.HasMetadata(\"1\"))\n}\n\nfunc TestHooks(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\ttokenLedger, meta, metaLedger := newMeta(0, cur)\n\n\turequire.NoError(t, tokenLedger.Mint(alice, \"1\"))\n\turequire.NoError(t, metaLedger.SetTokenURI(\"1\", \"ipfs://uri-1\"))\n\turequire.NoError(t, metaLedger.SetTokenMetadata(\"1\", Data{Name: \"one\"}))\n\n\tmetaLedger.OnMint(alice, \"1\")\n\tmetaLedger.OnTransfer(alice, bob, \"1\")\n\tuassert.True(t, meta.HasTokenURI(\"1\"))\n\tuassert.True(t, meta.HasMetadata(\"1\"))\n\turi, err := meta.TokenURI(\"1\")\n\turequire.NoError(t, err)\n\tuassert.Equal(t, \"ipfs://uri-1\", uri)\n\n\tmetaLedger.OnBurn(\"1\")\n\tuassert.False(t, meta.HasTokenURI(\"1\"))\n\tuassert.False(t, meta.HasMetadata(\"1\"))\n}\n"},{"name":"types.gno","body":"// Package metadata is a stackable GRC721 extension for EIP-721 tokenURI and\n// OpenSea on-chain metadata. Writes are issuer-only so a holder cannot rewrite metadata after acquisition.\npackage metadata\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/grc721/v0\"\n)\n\nconst (\n\tMetadataUpdateEvent = \"MetadataUpdate\"\n\tTokenURIUpdateEvent = \"TokenURIUpdate\"\n)\n\nvar (\n\tErrTokenNotMinted = errors.New(\"token is not minted\")\n\tErrNoMetadata     = errors.New(\"token has no metadata\")\n\tErrNoTokenURI     = errors.New(\"token has no uri\")\n)\n\ntype Metadata struct {\n\tcore *grc721.Token\n\tst   *storage\n}\n\ntype Ledger struct {\n\tcore *grc721.Token\n\tst   *storage\n}\n\n// Data is the OpenSea on-chain metadata for a single token.\ntype Data struct {\n\tImage           string\n\tImageData       string\n\tExternalURL     string\n\tDescription     string\n\tName            string\n\tAttributes      []Trait\n\tBackgroundColor string\n\tAnimationURL    string\n\tYoutubeURL      string\n}\n\ntype Trait struct {\n\tDisplayType string\n\tTraitType   string\n\tValue       string\n}\n\ntype storage struct {\n\turis avl.Tree // grc721.TokenID -\u003e string\n\tdata avl.Tree // grc721.TokenID -\u003e Data\n}\n\nvar _ grc721.Extension = (*Ledger)(nil)\n\nvar _ grc721.ExtensionView = (*Metadata)(nil)\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"tDso/3GZrkmGHxsm3g3akyzK1/C7KFKSRYb0fWD1fw5JF/FYeLB6wRb7mBKjjtBE/o/PdKod6/VKYa2xmHhrNA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"royalty","path":"gno.land/p/nt/grc721/royalty/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/grc721/royalty/v0\"\ngno = \"0.9\"\n"},{"name":"token.gno","body":"package royalty\n\nimport (\n\t\"chain\"\n\t\"math\"\n\t\"strconv\"\n\n\t\"gno.land/p/nt/grc721/v0\"\n)\n\nconst Kind = \"royalty\"\n\n// NewRoyalty attaches a royalty extension; maxBps caps every rate and must be 0..10000.\nfunc NewRoyalty(coreLedger *grc721.PrivateLedger, maxBps int64) (*Royalty, *Ledger) {\n\tif coreLedger == nil {\n\t\tpanic(\"royalty: nil core ledger\")\n\t}\n\n\tif maxBps \u003c 0 || maxBps \u003e FeeDenominator {\n\t\tpanic(ErrMaxBpsRange)\n\t}\n\n\tst := \u0026storage{maxBps: maxBps}\n\tcore := coreLedger.ReadToken()\n\tled := \u0026Ledger{core: core, st: st}\n\n\tcoreLedger.RegisterExtension(led)\n\n\treturn \u0026Royalty{core: core, st: st}, led\n}\n\nfunc (r *Royalty) ExtensionKind() string { return Kind }\n\nfunc (r *Royalty) TokenID() string { return r.core.ID() }\n\nfunc (r *Royalty) MaxBps() int64 { return r.st.maxBps }\n\n// RoyaltyInfo implements EIP-2981; per-token override takes precedence over the\n// default, and no royalty returns (zeroAddress, 0) — the \"no royalty\" signal.\nfunc (r *Royalty) RoyaltyInfo(tid grc721.TokenID, salePrice int64) (address, int64, error) {\n\tif salePrice \u003c 0 {\n\t\treturn zeroAddress, 0, ErrInvalidSalePrice\n\t}\n\n\tinfo, ok := r.resolve(tid)\n\tif !ok {\n\t\treturn zeroAddress, 0, nil\n\t}\n\n\tif info.Bps != 0 \u0026\u0026 salePrice \u003e math.MaxInt64/info.Bps {\n\t\treturn zeroAddress, 0, ErrInvalidSalePrice\n\t}\n\n\tamount := salePrice * info.Bps / FeeDenominator\n\n\treturn info.Receiver, amount, nil\n}\n\nfunc (r *Royalty) DefaultRoyalty() (RoyaltyInfo, bool) {\n\tif !r.st.hasDefault {\n\t\treturn RoyaltyInfo{}, false\n\t}\n\n\treturn r.st.def, true\n}\n\nfunc (r *Royalty) TokenRoyalty(tid grc721.TokenID) (RoyaltyInfo, bool) {\n\tv := r.st.perToken.Get(tid.String())\n\tif v == nil {\n\t\treturn RoyaltyInfo{}, false\n\t}\n\n\treturn v.(RoyaltyInfo), true\n}\n\n// resolve returns the effective royalty: per-token override first, then default.\nfunc (r *Royalty) resolve(tid grc721.TokenID) (RoyaltyInfo, bool) {\n\tif v := r.st.perToken.Get(tid.String()); v != nil {\n\t\treturn v.(RoyaltyInfo), true\n\t}\n\n\tif r.st.hasDefault {\n\t\treturn r.st.def, true\n\t}\n\n\treturn RoyaltyInfo{}, false\n}\n\n// SetDefaultRoyalty sets the collection default for tokens without an override; issuer-only.\nfunc (led *Ledger) SetDefaultRoyalty(receiver address, bps int64) error {\n\tif err := led.validate(receiver, bps); err != nil {\n\t\treturn err\n\t}\n\n\tled.st.def = RoyaltyInfo{Receiver: receiver, Bps: bps}\n\tled.st.hasDefault = true\n\n\tled.emitRoyaltyUpdate(\"default\", \"\", receiver, bps)\n\treturn nil\n}\n\nfunc (led *Ledger) DeleteDefaultRoyalty() {\n\tled.st.hasDefault = false\n\tled.st.def = RoyaltyInfo{}\n\n\tled.emitRoyaltyUpdate(\"default\", \"\", zeroAddress, 0)\n}\n\n// SetTokenRoyalty sets a per-token override for tid; issuer-only.\nfunc (led *Ledger) SetTokenRoyalty(tid grc721.TokenID, receiver address, bps int64) error {\n\tif _, err := led.core.OwnerOf(tid); err != nil {\n\t\treturn ErrTokenNotMinted\n\t}\n\n\tif err := led.validate(receiver, bps); err != nil {\n\t\treturn err\n\t}\n\n\tled.st.perToken.Set(tid.String(), RoyaltyInfo{Receiver: receiver, Bps: bps})\n\n\tled.emitRoyaltyUpdate(\"token\", tid.String(), receiver, bps)\n\treturn nil\n}\n\n// DeleteTokenRoyalty clears the per-token override, falling back to the default.\nfunc (led *Ledger) DeleteTokenRoyalty(tid grc721.TokenID) {\n\tled.st.perToken.Remove(tid.String())\n\n\tled.emitRoyaltyUpdate(\"token\", tid.String(), zeroAddress, 0)\n}\n\n// emitRoyaltyUpdate signals a royalty change (EIP-2981 defines no events); a\n// cleared royalty is reported as an empty receiver and bps 0.\nfunc (led *Ledger) emitRoyaltyUpdate(scope, tokenID string, receiver address, bps int64) {\n\tchain.Emit(\n\t\tRoyaltyUpdateEvent,\n\t\t\"token\", led.core.ID(),\n\t\t\"scope\", scope,\n\t\t\"tokenId\", tokenID,\n\t\t\"receiver\", receiver.String(),\n\t\t\"bps\", strconv.FormatInt(bps, 10),\n\t)\n}\n\nfunc (led *Ledger) validate(receiver address, bps int64) error {\n\tif !receiver.IsValid() {\n\t\treturn ErrInvalidReceiver\n\t}\n\n\tif bps \u003c 0 || bps \u003e led.st.maxBps {\n\t\treturn ErrInvalidBps\n\t}\n\n\treturn nil\n}\n\nfunc (led *Ledger) ExtensionKind() string { return Kind }\n\nfunc (led *Ledger) OnMint(to address, tid grc721.TokenID)           {}\nfunc (led *Ledger) OnTransfer(from, to address, tid grc721.TokenID) {}\n\n// OnBurn clears the per-token override (cannot resurface on re-mint) but keeps\n// the default, which is collection-level policy, not per-token state.\nfunc (led *Ledger) OnBurn(tid grc721.TokenID) {\n\tled.st.perToken.Remove(tid.String())\n}\n"},{"name":"token_test.gno","body":"package royalty\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/grc721/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\n// newToken builds via same-realm cross (NewToken requires rlm.IsCurrent()).\nfunc newToken(name, symbol string, id seqid.ID, rlm realm) (tok *grc721.Token, led *grc721.PrivateLedger) {\n\tfunc(cur realm) {\n\t\ttok, led = grc721.NewToken(name, symbol, id, cur)\n\t}(cross(rlm))\n\treturn\n}\n\nfunc newRoy(maxBps int64, rlm realm) (*grc721.PrivateLedger, *Royalty, *Ledger) {\n\t_, tokenLedger := newToken(\"Foo\", \"FOO\", 0, rlm)\n\troy, royLedger := NewRoyalty(tokenLedger, maxBps)\n\treturn tokenLedger, roy, royLedger\n}\n\nfunc TestNewRoyalty(cur realm, t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tnilLedger bool\n\t\tmaxBps    int64\n\t\twantPanic string // \"\" = success\n\t}{\n\t\t{\"nil token ledger panics\", true, 500, \"royalty: nil core ledger\"},\n\t\t{\"maxBps above FeeDenominator panics\", false, 10001, ErrMaxBpsRange.Error()},\n\t\t{\"negative maxBps panics\", false, -1, ErrMaxBpsRange.Error()},\n\t\t{\"maxBps at FeeDenominator boundary constructs\", false, 10000, \"\"},\n\t\t{\"maxBps zero boundary constructs\", false, 0, \"\"},\n\t\t{\"typical maxBps constructs\", false, 1000, \"\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.nilLedger {\n\t\t\t\tuassert.PanicsWithMessage(t, cur, tt.wantPanic, func() {\n\t\t\t\t\tNewRoyalty(nil, tt.maxBps)\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t_, tokenLedger := newToken(\"Foo\", \"FOO\", 0, cur)\n\t\t\tif tt.wantPanic != \"\" {\n\t\t\t\tuassert.PanicsWithMessage(t, cur, tt.wantPanic, func() {\n\t\t\t\t\tNewRoyalty(tokenLedger, tt.maxBps)\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\troy, royLedger := NewRoyalty(tokenLedger, tt.maxBps)\n\t\t\tuassert.True(t, roy != nil)\n\t\t\tuassert.True(t, royLedger != nil)\n\t\t\tuassert.Equal(t, Kind, roy.ExtensionKind())\n\t\t\tuassert.Equal(t, \"royalty\", roy.ExtensionKind())\n\t\t\tuassert.Equal(t, tt.maxBps, roy.MaxBps())\n\t\t\tuassert.Equal(t, tokenLedger.ReadToken().ID(), roy.TokenID())\n\t\t})\n\t}\n}\n\nfunc TestRoyaltyInfo(cur realm, t *testing.T) {\n\tartist := testutils.TestAddress(\"artist\")\n\tspecial := testutils.TestAddress(\"special\")\n\n\ttests := []struct {\n\t\tname         string\n\t\tsetDefault   bool\n\t\tdefaultBps   int64\n\t\tsetToken     bool\n\t\ttokenBps     int64\n\t\tsalePrice    int64\n\t\twantReceiver address\n\t\twantAmount   int64\n\t}{\n\t\t{\n\t\t\tname:         \"no royalty configured returns zero receiver and amount\",\n\t\t\tsalePrice:    10000,\n\t\t\twantReceiver: zeroAddress,\n\t\t\twantAmount:   0,\n\t\t},\n\t\t{\n\t\t\tname:         \"default 5% yields 500 on salePrice 10000\",\n\t\t\tsetDefault:   true,\n\t\t\tdefaultBps:   500,\n\t\t\tsalePrice:    10000,\n\t\t\twantReceiver: artist,\n\t\t\twantAmount:   500,\n\t\t},\n\t\t{\n\t\t\tname:         \"per-token override takes precedence over default\",\n\t\t\tsetDefault:   true,\n\t\t\tdefaultBps:   500,\n\t\t\tsetToken:     true,\n\t\t\ttokenBps:     250,\n\t\t\tsalePrice:    10000,\n\t\t\twantReceiver: special,\n\t\t\twantAmount:   250,\n\t\t},\n\t\t{\n\t\t\tname:         \"fractional bps 250 truncates on small sale price\",\n\t\t\tsetToken:     true,\n\t\t\ttokenBps:     250,\n\t\t\tsalePrice:    100, // 100*250/10000 = 2\n\t\t\twantReceiver: special,\n\t\t\twantAmount:   2,\n\t\t},\n\t\t{\n\t\t\tname:         \"zero sale price yields zero amount\",\n\t\t\tsetDefault:   true,\n\t\t\tdefaultBps:   500,\n\t\t\tsalePrice:    0,\n\t\t\twantReceiver: artist,\n\t\t\twantAmount:   0,\n\t\t},\n\t\t{\n\t\t\tname:         \"default receiver applies when token has no override\",\n\t\t\tsetDefault:   true,\n\t\t\tdefaultBps:   1000,\n\t\t\tsalePrice:    5000,\n\t\t\twantReceiver: artist,\n\t\t\twantAmount:   500,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttokenLedger, roy, royLedger := newRoy(1000, cur)\n\t\t\turequire.NoError(t, tokenLedger.Mint(testutils.TestAddress(\"alice\"), \"1\"))\n\n\t\t\tif tt.setDefault {\n\t\t\t\turequire.NoError(t, royLedger.SetDefaultRoyalty(artist, tt.defaultBps))\n\t\t\t}\n\t\t\tif tt.setToken {\n\t\t\t\turequire.NoError(t, royLedger.SetTokenRoyalty(\"1\", special, tt.tokenBps))\n\t\t\t}\n\n\t\t\trecv, amt, err := roy.RoyaltyInfo(\"1\", tt.salePrice)\n\t\t\turequire.NoError(t, err)\n\t\t\tuassert.Equal(t, tt.wantReceiver, recv)\n\t\t\tuassert.Equal(t, tt.wantAmount, amt)\n\t\t})\n\t}\n}\n\nfunc TestDefaultRoyalty(cur realm, t *testing.T) {\n\tartist := testutils.TestAddress(\"artist\")\n\n\tt.Run(\"unset default reports not present\", func(t *testing.T) {\n\t\t_, roy, _ := newRoy(1000, cur)\n\t\tinfo, ok := roy.DefaultRoyalty()\n\t\tuassert.False(t, ok)\n\t\tuassert.Equal(t, zeroAddress, info.Receiver)\n\t\tuassert.Equal(t, int64(0), info.Bps)\n\t})\n\n\tt.Run(\"set default is reported and readable\", func(t *testing.T) {\n\t\t_, roy, royLedger := newRoy(1000, cur)\n\t\turequire.NoError(t, royLedger.SetDefaultRoyalty(artist, 500))\n\t\tinfo, ok := roy.DefaultRoyalty()\n\t\tuassert.True(t, ok)\n\t\tuassert.Equal(t, artist, info.Receiver)\n\t\tuassert.Equal(t, int64(500), info.Bps)\n\t})\n\n\tt.Run(\"delete default clears it\", func(t *testing.T) {\n\t\t_, roy, royLedger := newRoy(1000, cur)\n\t\turequire.NoError(t, royLedger.SetDefaultRoyalty(artist, 500))\n\t\troyLedger.DeleteDefaultRoyalty()\n\t\tinfo, ok := roy.DefaultRoyalty()\n\t\tuassert.False(t, ok)\n\t\tuassert.Equal(t, zeroAddress, info.Receiver)\n\n\t\trecv, amt, err := roy.RoyaltyInfo(\"1\", 10000)\n\t\turequire.NoError(t, err)\n\t\tuassert.Equal(t, zeroAddress, recv)\n\t\tuassert.Equal(t, int64(0), amt)\n\t})\n}\n\nfunc TestTokenRoyalty(cur realm, t *testing.T) {\n\tartist := testutils.TestAddress(\"artist\")\n\tspecial := testutils.TestAddress(\"special\")\n\n\tt.Run(\"unset per-token override reports not present\", func(t *testing.T) {\n\t\t_, roy, _ := newRoy(1000, cur)\n\t\tinfo, ok := roy.TokenRoyalty(\"1\")\n\t\tuassert.False(t, ok)\n\t\tuassert.Equal(t, zeroAddress, info.Receiver)\n\t})\n\n\tt.Run(\"set per-token override is reported and readable\", func(t *testing.T) {\n\t\ttokenLedger, roy, royLedger := newRoy(1000, cur)\n\t\turequire.NoError(t, tokenLedger.Mint(testutils.TestAddress(\"alice\"), \"1\"))\n\t\turequire.NoError(t, royLedger.SetTokenRoyalty(\"1\", special, 250))\n\t\tinfo, ok := roy.TokenRoyalty(\"1\")\n\t\tuassert.True(t, ok)\n\t\tuassert.Equal(t, special, info.Receiver)\n\t\tuassert.Equal(t, int64(250), info.Bps)\n\t})\n\n\tt.Run(\"delete per-token override falls back to default\", func(t *testing.T) {\n\t\ttokenLedger, roy, royLedger := newRoy(1000, cur)\n\t\turequire.NoError(t, tokenLedger.Mint(testutils.TestAddress(\"alice\"), \"1\"))\n\t\turequire.NoError(t, royLedger.SetDefaultRoyalty(artist, 500))\n\t\turequire.NoError(t, royLedger.SetTokenRoyalty(\"1\", special, 250))\n\n\t\troyLedger.DeleteTokenRoyalty(\"1\")\n\t\t_, ok := roy.TokenRoyalty(\"1\")\n\t\tuassert.False(t, ok)\n\n\t\trecv, amt, err := roy.RoyaltyInfo(\"1\", 10000)\n\t\turequire.NoError(t, err)\n\t\tuassert.Equal(t, artist, recv)\n\t\tuassert.Equal(t, int64(500), amt)\n\t})\n}\n\nfunc TestSetDefaultRoyaltyValidation(cur realm, t *testing.T) {\n\tartist := testutils.TestAddress(\"artist\")\n\n\ttests := []struct {\n\t\tname     string\n\t\treceiver address\n\t\tbps      int64\n\t\twantErr  error\n\t}{\n\t\t{\"bps above maxBps returns ErrInvalidBps\", artist, 1001, ErrInvalidBps},\n\t\t{\"negative bps returns ErrInvalidBps\", artist, -1, ErrInvalidBps},\n\t\t{\"invalid (zero) receiver returns ErrInvalidReceiver\", zeroAddress, 100, ErrInvalidReceiver},\n\t\t{\"bps at maxBps boundary succeeds\", artist, 1000, nil},\n\t\t{\"zero bps succeeds\", artist, 0, nil},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t_, roy, royLedger := newRoy(1000, cur)\n\t\t\terr := royLedger.SetDefaultRoyalty(tt.receiver, tt.bps)\n\t\t\tif tt.wantErr != nil {\n\t\t\t\tuassert.ErrorIs(t, err, tt.wantErr)\n\t\t\t\t_, ok := roy.DefaultRoyalty()\n\t\t\t\tuassert.False(t, ok)\n\t\t\t\treturn\n\t\t\t}\n\t\t\turequire.NoError(t, err)\n\t\t\tinfo, ok := roy.DefaultRoyalty()\n\t\t\tuassert.True(t, ok)\n\t\t\tuassert.Equal(t, tt.bps, info.Bps)\n\t\t})\n\t}\n}\n\nfunc TestSetTokenRoyaltyValidation(cur realm, t *testing.T) {\n\tartist := testutils.TestAddress(\"artist\")\n\n\ttests := []struct {\n\t\tname     string\n\t\tmint     bool\n\t\ttid      grc721.TokenID\n\t\treceiver address\n\t\tbps      int64\n\t\twantErr  error\n\t}{\n\t\t{\"unminted token returns ErrTokenNotMinted\", false, \"404\", artist, 100, ErrTokenNotMinted},\n\t\t{\"minted token with bps above maxBps returns ErrInvalidBps\", true, \"1\", artist, 1001, ErrInvalidBps},\n\t\t{\"minted token with negative bps returns ErrInvalidBps\", true, \"1\", artist, -1, ErrInvalidBps},\n\t\t{\"minted token with invalid receiver returns ErrInvalidReceiver\", true, \"1\", zeroAddress, 100, ErrInvalidReceiver},\n\t\t{\"minted token at maxBps boundary succeeds\", true, \"1\", artist, 1000, nil},\n\t\t{\"minted token with valid override succeeds\", true, \"1\", artist, 250, nil},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttokenLedger, roy, royLedger := newRoy(1000, cur)\n\t\t\tif tt.mint {\n\t\t\t\turequire.NoError(t, tokenLedger.Mint(testutils.TestAddress(\"alice\"), tt.tid))\n\t\t\t}\n\n\t\t\terr := royLedger.SetTokenRoyalty(tt.tid, tt.receiver, tt.bps)\n\t\t\tif tt.wantErr != nil {\n\t\t\t\tuassert.ErrorIs(t, err, tt.wantErr)\n\t\t\t\t_, ok := roy.TokenRoyalty(tt.tid)\n\t\t\t\tuassert.False(t, ok)\n\t\t\t\treturn\n\t\t\t}\n\t\t\turequire.NoError(t, err)\n\t\t\tinfo, ok := roy.TokenRoyalty(tt.tid)\n\t\t\tuassert.True(t, ok)\n\t\t\tuassert.Equal(t, tt.bps, info.Bps)\n\t\t})\n\t}\n}\n\nfunc TestOnBurnClearsTokenRoyaltyKeepsDefault(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tartist := testutils.TestAddress(\"artist\")\n\tspecial := testutils.TestAddress(\"special\")\n\ttokenLedger, roy, royLedger := newRoy(1000, cur)\n\n\turequire.NoError(t, tokenLedger.Mint(alice, \"1\"))\n\turequire.NoError(t, royLedger.SetDefaultRoyalty(artist, 500))\n\turequire.NoError(t, royLedger.SetTokenRoyalty(\"1\", special, 250))\n\n\t_, ok := roy.TokenRoyalty(\"1\")\n\tuassert.True(t, ok)\n\n\turequire.NoError(t, tokenLedger.Burn(\"1\"))\n\t_, ok = roy.TokenRoyalty(\"1\")\n\tuassert.False(t, ok)\n\n\tinfo, ok := roy.DefaultRoyalty()\n\tuassert.True(t, ok)\n\tuassert.Equal(t, artist, info.Receiver)\n\n\turequire.NoError(t, tokenLedger.Mint(alice, \"1\"))\n\t_, ok = roy.TokenRoyalty(\"1\")\n\tuassert.False(t, ok)\n\trecv, amt, err := roy.RoyaltyInfo(\"1\", 10000)\n\turequire.NoError(t, err)\n\tuassert.Equal(t, artist, recv)\n\tuassert.Equal(t, int64(500), amt)\n}\n\nfunc TestHooks(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\tartist := testutils.TestAddress(\"artist\")\n\tspecial := testutils.TestAddress(\"special\")\n\ttokenLedger, roy, royLedger := newRoy(1000, cur)\n\n\turequire.NoError(t, tokenLedger.Mint(alice, \"1\"))\n\turequire.NoError(t, royLedger.SetDefaultRoyalty(artist, 500))\n\turequire.NoError(t, royLedger.SetTokenRoyalty(\"1\", special, 250))\n\n\troyLedger.OnMint(alice, \"1\")\n\troyLedger.OnTransfer(alice, bob, \"1\")\n\tinfo, ok := roy.TokenRoyalty(\"1\")\n\tuassert.True(t, ok)\n\tuassert.Equal(t, special, info.Receiver)\n\t_, ok = roy.DefaultRoyalty()\n\tuassert.True(t, ok)\n\n\troyLedger.OnBurn(\"1\")\n\t_, ok = roy.TokenRoyalty(\"1\")\n\tuassert.False(t, ok)\n\t_, ok = roy.DefaultRoyalty()\n\tuassert.True(t, ok)\n}\n"},{"name":"types.gno","body":"// Package royalty is a stackable GRC721 extension implementing EIP-2981 in basis\n// points. Writes are issuer-only so a holder cannot zero out a royalty on a\n// token they just received.\npackage royalty\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/grc721/v0\"\n)\n\n// FeeDenominator is the EIP-2981 basis-point denominator: bps/10000, 10000 == 100%.\nconst FeeDenominator = 10000\n\nconst RoyaltyUpdateEvent = \"RoyaltyUpdate\"\n\nvar (\n\tErrInvalidReceiver  = errors.New(\"invalid royalty receiver\")\n\tErrInvalidBps       = errors.New(\"royalty bps out of range\")\n\tErrTokenNotMinted   = errors.New(\"token is not minted\")\n\tErrMaxBpsRange      = errors.New(\"maxBps out of range (0..10000)\")\n\tErrInvalidSalePrice = errors.New(\"invalid sale price (negative or would overflow)\")\n)\n\nvar zeroAddress = address(\"\")\n\n// RoyaltyInfo is a royalty policy: send Bps/10000 of a sale to Receiver.\ntype RoyaltyInfo struct {\n\tReceiver address\n\tBps      int64\n}\n\ntype storage struct {\n\tmaxBps     int64\n\thasDefault bool\n\tdef        RoyaltyInfo\n\tperToken   avl.Tree // grc721.TokenID -\u003e RoyaltyInfo\n}\n\ntype Royalty struct {\n\tcore *grc721.Token\n\tst   *storage\n}\n\ntype Ledger struct {\n\tcore *grc721.Token\n\tst   *storage\n}\n\nvar _ grc721.Extension = (*Ledger)(nil)\n\nvar _ grc721.ExtensionView = (*Royalty)(nil)\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"9KYTdlRGz86jUNLTWFkqtMuIvM5VjyOTQhSzJZxSERkabXaxBfhVAs+qeVyKnECsk+gti2y7fwa5Hx71VxUWow=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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 trimTagLine 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":"foreign_test.gno","body":"package foreign\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestForeign_BasicWrap(t *testing.T) {\n\tgot := Foreign(\"hello\")\n\twant := \"\\n\\n\u003cgno-foreign\u003e\\nhello\\n\u003c/gno-foreign\u003e\\n\\n\"\n\tif got != want {\n\t\tt.Errorf(\"got %q, want %q\", got, want)\n\t}\n}\n\nfunc TestForeign_LeadingTrailingBlankLines(t *testing.T) {\n\t// Parser requires a blank line before the opener (CM §4.6). Verify\n\t// the helper emits exactly that.\n\tgot := Foreign(\"body\")\n\tif !strings.HasPrefix(got, \"\\n\\n\u003cgno-foreign\u003e\\n\") {\n\t\tt.Errorf(\"missing blank line before opener: %q\", got)\n\t}\n\tif !strings.HasSuffix(got, \"\\n\u003c/gno-foreign\u003e\\n\\n\") {\n\t\tt.Errorf(\"missing blank line after closer: %q\", got)\n\t}\n}\n\nfunc TestForeign_NormalizesCRLF(t *testing.T) {\n\tgot := Foreign(\"line1\\r\\nline2\\r\\nline3\")\n\tif strings.Contains(got, \"\\r\") {\n\t\tt.Errorf(\"CRLF not normalized: %q\", got)\n\t}\n\tif !strings.Contains(got, \"line1\\nline2\\nline3\") {\n\t\tt.Errorf(\"body content lost: %q\", got)\n\t}\n}\n\nfunc TestForeign_NormalizesBareCR(t *testing.T) {\n\tgot := Foreign(\"line1\\rline2\")\n\tif strings.Contains(got, \"\\r\") {\n\t\tt.Errorf(\"bare CR not normalized: %q\", got)\n\t}\n\tif !strings.Contains(got, \"line1\\nline2\") {\n\t\tt.Errorf(\"body content lost: %q\", got)\n\t}\n}\n\nfunc TestForeign_EscapesSentinelClose(t *testing.T) {\n\t// Unbalanced \u003c/gno-foreign\u003e in body would terminate the outer\n\t// block early. Verify it's escaped to literal text — only the\n\t// leading \"\u003c\" is replaced with \"\u0026lt;\"; the trailing \"\u003e\" stays\n\t// literal (rendering as visible \"\u003e\" in HTML).\n\tgot := Foreign(\"foo\\n\u003c/gno-foreign\u003e\\nbar\")\n\tif strings.Contains(got, \"\\n\u003c/gno-foreign\u003e\\nbar\") {\n\t\tt.Errorf(\"unbalanced close not escaped: %q\", got)\n\t}\n\tif !strings.Contains(got, \"\u0026lt;/gno-foreign\u003e\") {\n\t\tt.Errorf(\"expected escaped close: %q\", got)\n\t}\n}\n\nfunc TestForeign_EscapesSentinelOpen(t *testing.T) {\n\t// A bare opener inside body (without matching close in body)\n\t// would be picked up as a nested inner foreign. Escape it.\n\tgot := Foreign(\"foo\\n\u003cgno-foreign\u003e\\nbar\")\n\tif !strings.Contains(got, \"\\n\u0026lt;gno-foreign\u003e\\n\") {\n\t\tt.Errorf(\"expected escaped inner opener: %q\", got)\n\t}\n}\n\nfunc TestForeign_EscapesIndentedSentinel(t *testing.T) {\n\t// Parser strips 0-3 leading spaces before matching; helper\n\t// must mirror that to catch indented sentinels.\n\tgot := Foreign(\"foo\\n   \u003c/gno-foreign\u003e\\nbar\")\n\tif !strings.Contains(got, \"\u0026lt;/gno-foreign\u003e\") {\n\t\tt.Errorf(\"indented sentinel not escaped: %q\", got)\n\t}\n}\n\nfunc TestForeign_EscapesAttributeBearingOpener(t *testing.T) {\n\t// `\u003cgno-foreign label=\"x\"\u003e` IS recognized by the parser as a\n\t// nested opener. If left unmangled, the parser bumps framingDepth\n\t// when it sees this line in body bytes, and then consumes the\n\t// helper's own outer-close as a fake \"inner close\" — capturing\n\t// realm content that follows the helper's output INTO the\n\t// sandbox. Verify the helper escapes the leading \"\u003c\".\n\tgot := Foreign(`\u003cgno-foreign label=\"x\"\u003e`)\n\tif strings.Contains(got, `\u003cgno-foreign label=\"x\"\u003e`) {\n\t\tt.Errorf(\"attribute-bearing opener was NOT mangled — escape vector:\\n%q\", got)\n\t}\n\tif !strings.Contains(got, `\u0026lt;gno-foreign label=\"x\"\u003e`) {\n\t\tt.Errorf(\"expected leading '\u003c' escaped: %q\", got)\n\t}\n}\n\nfunc TestForeign_EscapesAttributeBearingCloser(t *testing.T) {\n\t// golang.org/x/net/html zeroes the Attr slice on end tags, so the\n\t// parser recognizes ANY `\u003c/gno-foreign attr…\u003e` form as a sentinel\n\t// close. Leaving such a line unmangled in body bytes would close\n\t// the outer block early (framingDepth=0 case in the parser's\n\t// Continue), letting body content that follows render OUTSIDE\n\t// the sandbox at top level. Verify the helper escapes it.\n\tgot := Foreign(`\u003c/gno-foreign label=\"x\"\u003e`)\n\tif strings.Contains(got, `\u003e \u003c/gno-foreign label=\"x\"\u003e \u003c/gno-foreign\u003e`) {\n\t\tt.Errorf(\"attribute-bearing closer was NOT mangled — escape vector:\\n%q\", got)\n\t}\n\tif !strings.Contains(got, `\u0026lt;/gno-foreign label=\"x\"\u003e`) {\n\t\tt.Errorf(\"expected leading '\u003c' escaped: %q\", got)\n\t}\n}\n\nfunc TestForeign_EscapesGnoForeignWithExtraContent(t *testing.T) {\n\t// `\u003cgno-foreign\u003e extra` would not be recognized as a sentinel by\n\t// the parser (tokenizer yields 2 tokens). But the helper is\n\t// over-inclusive on purpose: anything that looks like a\n\t// gno-foreign tag opener or closer gets mangled. Predictable\n\t// behavior beats narrow matching that diverges from the parser\n\t// on attribute-bearing forms.\n\tgot := Foreign(\"\u003cgno-foreign\u003e extra text\")\n\tif !strings.Contains(got, \"\u0026lt;gno-foreign\u003e extra text\") {\n\t\tt.Errorf(\"expected over-inclusive mangle of `\u003cgno-foreign\u003e extra`: %q\", got)\n\t}\n}\n\nfunc TestForeign_EscapesCaseVariantClose(t *testing.T) {\n\t// The renderer-side parser recognizes tags case-insensitively\n\t// (goldmark's tokenizer lowercases tag names). A case-variant close\n\t// left unescaped would terminate the outer block early and let\n\t// trailing body render OUTSIDE the sandbox. The helper must escape\n\t// `\u003c/GNO-FOREIGN\u003e` just like `\u003c/gno-foreign\u003e`.\n\tgot := Foreign(\"foo\\n\u003c/GNO-FOREIGN\u003e\\nbar\")\n\tif strings.Contains(got, \"\\n\u003c/GNO-FOREIGN\u003e\\nbar\") {\n\t\tt.Errorf(\"case-variant close not escaped — sandbox-escape vector: %q\", got)\n\t}\n\tif !strings.Contains(got, \"\u0026lt;/GNO-FOREIGN\u003e\") {\n\t\tt.Errorf(\"expected escaped case-variant close: %q\", got)\n\t}\n}\n\nfunc TestForeign_EscapesCaseVariantOpen(t *testing.T) {\n\t// A case-variant opener left unescaped would be picked up by the\n\t// parser as a nested inner foreign (framingDepth++), causing the\n\t// helper's own outer-close to be consumed as a fake inner close and\n\t// capturing trailing realm content into the sandbox.\n\tgot := Foreign(\"foo\\n\u003cGNO-FOREIGN\u003e\\nbar\")\n\tif !strings.Contains(got, \"\\n\u0026lt;GNO-FOREIGN\u003e\\n\") {\n\t\tt.Errorf(\"case-variant opener not escaped — capture vector: %q\", got)\n\t}\n}\n\nfunc TestForeign_EscapesMixedCaseSentinel(t *testing.T) {\n\t// Mixed-case forms (the most likely hand-crafted evasion) must also\n\t// be caught, including the attribute-bearing mixed-case opener.\n\tfor _, in := range []string{\n\t\t\"\u003cGno-Foreign\u003e\",\n\t\t\"\u003c/Gno-Foreign\u003e\",\n\t\t`\u003cGNO-foreign label=\"x\"\u003e`,\n\t\t\"\u003c/gNo-fOrEiGn\u003e\",\n\t} {\n\t\tgot := Foreign(in)\n\t\tif !strings.Contains(got, \"\u0026lt;\") {\n\t\t\tt.Errorf(\"mixed-case sentinel %q not escaped: %q\", in, got)\n\t\t}\n\t}\n}\n\nfunc TestForeignWithLabel_StripsBidiAndZeroWidth(t *testing.T) {\n\t// A bidi-override (U+202E) or zero-width (U+200C) run in the label\n\t// is invisible-spoof payload; it must be stripped before the label\n\t// is spliced into the opener attribute.\n\tgot := ForeignWithLabel(\"a\\u202eb\\u200cc\", \"body\")\n\tif !strings.Contains(got, `label=\"abc\"`) {\n\t\tt.Errorf(\"expected bidi/zero-width-stripped label: %q\", got)\n\t}\n}\n\nfunc TestForeignWithLabel_UnicodeSeparatorsBecomeSpaces(t *testing.T) {\n\t// U+2028 / U+2029 / U+0085 must not survive in the single-line\n\t// opener attribute; they fold to spaces like ASCII line breaks.\n\tgot := ForeignWithLabel(\"a\\u2028b\\u2029c\\u0085d\", \"body\")\n\tif !strings.Contains(got, `label=\"a b c d\"`) {\n\t\tt.Errorf(\"expected space-folded label: %q\", got)\n\t}\n}\n\nfunc TestForeign_EscapesUnrelatedForeignPrefix(t *testing.T) {\n\t// `\u003cgno-foreignx\u003e` is a different tag the parser would NOT treat as\n\t// a sentinel, but the escaper is deliberately over-inclusive (it\n\t// matches the `\u003cgno-foreign` prefix alone), so it is escaped to\n\t// visible literal text. Harmless, and the safe side of the trade.\n\tgot := Foreign(\"\u003cgno-foreignx\u003e\")\n\tif !strings.Contains(got, \"\u0026lt;gno-foreignx\u003e\") {\n\t\tt.Errorf(\"expected over-inclusive escape of \u003cgno-foreignx\u003e: %q\", got)\n\t}\n}\n\nfunc TestForeign_EscapesSlashAndFormFeedClosers(t *testing.T) {\n\t// REGRESSION: goldmark's tokenizer ends a tag name at `/` and at\n\t// form-feed (both HTML tag-name terminators), so `\u003c/gno-foreign/\u003e`\n\t// and `\u003c/gno-foreign\\f\u003e` are recognized as CLOSERS by the parser. A\n\t// body line carrying one of these, if it reached the parser\n\t// unescaped, would close the outer block early — a sandbox escape.\n\t// The over-inclusive escaper must neutralize every such variant.\n\tfor _, in := range []string{\n\t\t\"foo\\n\u003c/gno-foreign/\u003e\\nbar\",\n\t\t\"foo\\n\u003c/gno-foreign/ \u003e\\nbar\",\n\t\t\"foo\\n\u003c/gno-foreign\\f\u003e\\nbar\",\n\t\t\"foo\\n\u003cgno-foreign/\u003e\\nbar\",\n\t} {\n\t\tgot := Foreign(in)\n\t\tif !strings.Contains(got, \"\u0026lt;\") {\n\t\t\tt.Errorf(\"variant not escaped (sandbox-escape vector): %q -\u003e %q\", in, got)\n\t\t}\n\t}\n}\n\nfunc TestForeign_EmptyBody(t *testing.T) {\n\tgot := Foreign(\"\")\n\twant := \"\\n\\n\u003cgno-foreign\u003e\\n\\n\u003c/gno-foreign\u003e\\n\\n\"\n\tif got != want {\n\t\tt.Errorf(\"got %q, want %q\", got, want)\n\t}\n}\n\nfunc TestForeign_PreservesInternalBlankLines(t *testing.T) {\n\tgot := Foreign(\"para1\\n\\npara2\")\n\tif !strings.Contains(got, \"para1\\n\\npara2\") {\n\t\tt.Errorf(\"internal blank line lost: %q\", got)\n\t}\n}\n\nfunc TestForeignWithLabel_BasicWrap(t *testing.T) {\n\tgot := ForeignWithLabel(\"My Label\", \"body\")\n\twant := \"\\n\\n\u003cgno-foreign label=\\\"My Label\\\"\u003e\\nbody\\n\u003c/gno-foreign\u003e\\n\\n\"\n\tif got != want {\n\t\tt.Errorf(\"got %q, want %q\", got, want)\n\t}\n}\n\nfunc TestForeignWithLabel_EmptyLabelFallsBack(t *testing.T) {\n\t// An empty label (or one that becomes empty after sanitization)\n\t// should be identical to Foreign — no attribute on the opener.\n\tgot := ForeignWithLabel(\"\", \"body\")\n\tif strings.Contains(got, \"label=\") {\n\t\tt.Errorf(\"empty label should not emit attribute: %q\", got)\n\t}\n\tif got != Foreign(\"body\") {\n\t\tt.Errorf(\"empty label should match Foreign: got %q vs %q\", got, Foreign(\"body\"))\n\t}\n}\n\nfunc TestForeignWithLabel_WhitespaceOnlyLabelFallsBack(t *testing.T) {\n\tgot := ForeignWithLabel(\"   \", \"body\")\n\tif strings.Contains(got, \"label=\") {\n\t\tt.Errorf(\"whitespace label should not emit attribute: %q\", got)\n\t}\n}\n\nfunc TestForeignWithLabel_EscapesAmpAndQuote(t *testing.T) {\n\tgot := ForeignWithLabel(`Tom \u0026 \"Jerry\"`, \"body\")\n\tif !strings.Contains(got, `label=\"Tom \u0026amp; \u0026quot;Jerry\u0026quot;\"`) {\n\t\tt.Errorf(\"expected \u0026 and \\\" escaped: %q\", got)\n\t}\n}\n\nfunc TestForeignWithLabel_EscapesAngleBrackets(t *testing.T) {\n\t// An unescaped `\u003e` would close the opener tag early. Must be\n\t// escaped.\n\tgot := ForeignWithLabel(`a\u003cb\u003ec`, \"body\")\n\tif !strings.Contains(got, `label=\"a\u0026lt;b\u0026gt;c\"`) {\n\t\tt.Errorf(\"expected \u003c and \u003e escaped: %q\", got)\n\t}\n}\n\nfunc TestForeignWithLabel_StripsNUL(t *testing.T) {\n\tgot := ForeignWithLabel(\"a\\x00b\\x00c\", \"body\")\n\tif strings.Contains(got, \"\\x00\") {\n\t\tt.Errorf(\"NUL not stripped: %q\", got)\n\t}\n\tif !strings.Contains(got, `label=\"abc\"`) {\n\t\tt.Errorf(\"expected NUL-stripped label: %q\", got)\n\t}\n}\n\nfunc TestForeignWithLabel_ControlCharsBecomeSpaces(t *testing.T) {\n\t// Newlines and tabs would break the single-line opener required\n\t// by the parser. They must become spaces.\n\tgot := ForeignWithLabel(\"line1\\nline2\\ttab\", \"body\")\n\tif strings.Contains(got, \"\\nline2\") || strings.Contains(got, \"\\t\") {\n\t\tt.Errorf(\"control chars not converted: %q\", got)\n\t}\n\tif !strings.Contains(got, \"line1 line2 tab\") {\n\t\tt.Errorf(\"expected space-separated label: %q\", got)\n\t}\n}\n\nfunc TestForeignWithLabel_TrimsLeadingTrailingSpace(t *testing.T) {\n\tgot := ForeignWithLabel(\"  hello  \", \"body\")\n\tif !strings.Contains(got, `label=\"hello\"`) {\n\t\tt.Errorf(\"expected trimmed label: %q\", got)\n\t}\n}\n\nfunc TestForeignWithLabel_PreservesBodyNormalization(t *testing.T) {\n\t// Body normalization (CRLF, sentinel escaping) must still apply\n\t// in the labeled variant.\n\tgot := ForeignWithLabel(\"L\", \"a\\r\\n\u003c/gno-foreign\u003e\\r\\nb\")\n\tif strings.Contains(got, \"\\r\") {\n\t\tt.Errorf(\"CRLF not normalized in labeled variant: %q\", got)\n\t}\n\tif !strings.Contains(got, \"\u0026lt;/gno-foreign\u003e\") {\n\t\tt.Errorf(\"sentinel not escaped in labeled variant: %q\", got)\n\t}\n}\n\nfunc TestMaxBlocksPerRender(t *testing.T) {\n\t// Surfaces gnoweb's per-render foreign-block cap to realms via the\n\t// chain/markdown native. Pin the contract value; bump here if the\n\t// renderer cap changes.\n\tif got := MaxBlocksPerRender(); got != 256 {\n\t\tt.Errorf(\"MaxBlocksPerRender() = %d, want 256\", got)\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/markdown/foreign/v0\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"JYpr6x3n8Bgv2jEAzEpHRc/Z55aljbprcBBDL9mNaJFgh12R1Uf5eZYHawaPXlKwFO36GPmi+k+QLcXTJSYgjA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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"},{"name":"mdalert_test.gno","body":"package mdalert_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/mdalert/v0\"\n)\n\nfunc TestAlertString(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\texpected string\n\t\talert    mdalert.Alert\n\t}{\n\t\t{\n\t\t\tname:     \"alert\",\n\t\t\texpected: \"\u003e [!INFO] Title\\n\u003e Message\\n\",\n\t\t\talert:    mdalert.New(mdalert.TypeInfo, \"Title\", \"Message\", false),\n\t\t},\n\t\t{\n\t\t\tname:     \"alert with empty title\",\n\t\t\texpected: \"\u003e [!INFO]\\n\u003e Message\\n\",\n\t\t\talert:    mdalert.New(mdalert.TypeInfo, \"\", \"Message\", false),\n\t\t},\n\t\t{\n\t\t\tname:     \"alert multiline\",\n\t\t\texpected: \"\u003e [!INFO]\\n\u003e Line1\\n\u003e Line2\\n\",\n\t\t\talert:    mdalert.New(mdalert.TypeInfo, \"\", \"Line1\\nLine2\", false),\n\t\t},\n\t\t{\n\t\t\tname:     \"folded alert\",\n\t\t\texpected: \"\u003e [!INFO]- Title\\n\u003e Message\\n\",\n\t\t\talert:    mdalert.New(mdalert.TypeInfo, \"Title\", \"Message\", true),\n\t\t},\n\t\t{\n\t\t\tname: \"empty alert\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := tt.alert.String()\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"Got:  %q\\nWant: %q\", got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHelpers(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\texpected string\n\t\tfn       func() string\n\t}{\n\t\t// CAUTION\n\t\t{\"caution\", \"\u003e [!CAUTION] Title\\n\u003e Message\\n\", func() string {\n\t\t\treturn mdalert.Caution(\"Title\", \"Message\")\n\t\t}},\n\t\t{\"caution with empty title\", \"\u003e [!CAUTION]\\n\u003e Message\\n\", func() string {\n\t\t\treturn mdalert.Caution(\"\", \"Message\")\n\t\t}},\n\t\t{\"caution multiline\", \"\u003e [!CAUTION] Title\\n\u003e Line1\\n\u003e Line2\\n\", func() string {\n\t\t\treturn mdalert.Caution(\"Title\", \"Line1\\nLine2\")\n\t\t}},\n\t\t{\"caution formatted\", \"\u003e [!CAUTION] Title\\n\u003e Message\\n\", func() string {\n\t\t\treturn mdalert.Cautionf(\"Title\", \"%s\", \"Message\")\n\t\t}},\n\t\t{\"caution formatted with empty title\", \"\u003e [!CAUTION]\\n\u003e Message\\n\", func() string {\n\t\t\treturn mdalert.Cautionf(\"\", \"%s\", \"Message\")\n\t\t}},\n\t\t{\"caution formatted multiline\", \"\u003e [!CAUTION] Title\\n\u003e Line1\\n\u003e Line2\\n\", func() string {\n\t\t\treturn mdalert.Cautionf(\"Title\", \"%s\\n%s\", \"Line1\", \"Line2\")\n\t\t}},\n\n\t\t// INFO\n\t\t{\"info\", \"\u003e [!INFO] Title\\n\u003e Message\\n\", func() string {\n\t\t\treturn mdalert.Info(\"Title\", \"Message\")\n\t\t}},\n\t\t{\"info with empty title\", \"\u003e [!INFO]\\n\u003e Message\\n\", func() string {\n\t\t\treturn mdalert.Info(\"\", \"Message\")\n\t\t}},\n\t\t{\"info multiline\", \"\u003e [!INFO] Title\\n\u003e Line1\\n\u003e Line2\\n\", func() string {\n\t\t\treturn mdalert.Info(\"Title\", \"Line1\\nLine2\")\n\t\t}},\n\t\t{\"info formatted\", \"\u003e [!INFO] Title\\n\u003e Message\\n\", func() string {\n\t\t\treturn mdalert.Infof(\"Title\", \"%s\", \"Message\")\n\t\t}},\n\t\t{\"info formatted with empty title\", \"\u003e [!INFO]\\n\u003e Message\\n\", func() string {\n\t\t\treturn mdalert.Infof(\"\", \"%s\", \"Message\")\n\t\t}},\n\t\t{\"info formatted multiline\", \"\u003e [!INFO] Title\\n\u003e Line1\\n\u003e Line2\\n\", func() string {\n\t\t\treturn mdalert.Infof(\"Title\", \"%s\\n%s\", \"Line1\", \"Line2\")\n\t\t}},\n\n\t\t// NOTE\n\t\t{\"note\", \"\u003e [!NOTE] Title\\n\u003e Message\\n\", func() string {\n\t\t\treturn mdalert.Note(\"Title\", \"Message\")\n\t\t}},\n\t\t{\"note with empty title\", \"\u003e [!NOTE]\\n\u003e Message\\n\", func() string {\n\t\t\treturn mdalert.Note(\"\", \"Message\")\n\t\t}},\n\t\t{\"note multiline\", \"\u003e [!NOTE] Title\\n\u003e Line1\\n\u003e Line2\\n\", func() string {\n\t\t\treturn mdalert.Note(\"Title\", \"Line1\\nLine2\")\n\t\t}},\n\t\t{\"note formatted\", \"\u003e [!NOTE] Title\\n\u003e Message\\n\", func() string {\n\t\t\treturn mdalert.Notef(\"Title\", \"%s\", \"Message\")\n\t\t}},\n\t\t{\"note formatted with empty title\", \"\u003e [!NOTE]\\n\u003e Message\\n\", func() string {\n\t\t\treturn mdalert.Notef(\"\", \"%s\", \"Message\")\n\t\t}},\n\t\t{\"note formatted multiline\", \"\u003e [!NOTE] Title\\n\u003e Line1\\n\u003e Line2\\n\", func() string {\n\t\t\treturn mdalert.Notef(\"Title\", \"%s\\n%s\", \"Line1\", \"Line2\")\n\t\t}},\n\n\t\t// SUCCESS\n\t\t{\"success\", \"\u003e [!SUCCESS] Title\\n\u003e Message\\n\", func() string {\n\t\t\treturn mdalert.Success(\"Title\", \"Message\")\n\t\t}},\n\t\t{\"success with empty title\", \"\u003e [!SUCCESS]\\n\u003e Message\\n\", func() string {\n\t\t\treturn mdalert.Success(\"\", \"Message\")\n\t\t}},\n\t\t{\"success multiline\", \"\u003e [!SUCCESS] Title\\n\u003e Line1\\n\u003e Line2\\n\", func() string {\n\t\t\treturn mdalert.Success(\"Title\", \"Line1\\nLine2\")\n\t\t}},\n\t\t{\"success formatted\", \"\u003e [!SUCCESS] Title\\n\u003e Message\\n\", func() string {\n\t\t\treturn mdalert.Successf(\"Title\", \"%s\", \"Message\")\n\t\t}},\n\t\t{\"success formatted with empty title\", \"\u003e [!SUCCESS]\\n\u003e Message\\n\", func() string {\n\t\t\treturn mdalert.Successf(\"\", \"%s\", \"Message\")\n\t\t}},\n\t\t{\"success formatted multiline\", \"\u003e [!SUCCESS] Title\\n\u003e Line1\\n\u003e Line2\\n\", func() string {\n\t\t\treturn mdalert.Successf(\"Title\", \"%s\\n%s\", \"Line1\", \"Line2\")\n\t\t}},\n\n\t\t// TIP\n\t\t{\"tip\", \"\u003e [!TIP] Title\\n\u003e Message\\n\", func() string {\n\t\t\treturn mdalert.Tip(\"Title\", \"Message\")\n\t\t}},\n\t\t{\"tip with empty title\", \"\u003e [!TIP]\\n\u003e Message\\n\", func() string {\n\t\t\treturn mdalert.Tip(\"\", \"Message\")\n\t\t}},\n\t\t{\"tip multiline\", \"\u003e [!TIP] Title\\n\u003e Line1\\n\u003e Line2\\n\", func() string {\n\t\t\treturn mdalert.Tip(\"Title\", \"Line1\\nLine2\")\n\t\t}},\n\t\t{\"tip formatted\", \"\u003e [!TIP] Title\\n\u003e Message\\n\", func() string {\n\t\t\treturn mdalert.Tipf(\"Title\", \"%s\", \"Message\")\n\t\t}},\n\t\t{\"tip formatted with empty title\", \"\u003e [!TIP]\\n\u003e Message\\n\", func() string {\n\t\t\treturn mdalert.Tipf(\"\", \"%s\", \"Message\")\n\t\t}},\n\t\t{\"tip formatted multiline\", \"\u003e [!TIP] Title\\n\u003e Line1\\n\u003e Line2\\n\", func() string {\n\t\t\treturn mdalert.Tipf(\"Title\", \"%s\\n%s\", \"Line1\", \"Line2\")\n\t\t}},\n\n\t\t// WARNING\n\t\t{\"warning\", \"\u003e [!WARNING] Title\\n\u003e Message\\n\", func() string {\n\t\t\treturn mdalert.Warning(\"Title\", \"Message\")\n\t\t}},\n\t\t{\"warning with empty title\", \"\u003e [!WARNING]\\n\u003e Message\\n\", func() string {\n\t\t\treturn mdalert.Warning(\"\", \"Message\")\n\t\t}},\n\t\t{\"warning multiline\", \"\u003e [!WARNING] Title\\n\u003e Line1\\n\u003e Line2\\n\", func() string {\n\t\t\treturn mdalert.Warning(\"Title\", \"Line1\\nLine2\")\n\t\t}},\n\t\t{\"warning formatted\", \"\u003e [!WARNING] Title\\n\u003e Message\\n\", func() string {\n\t\t\treturn mdalert.Warningf(\"Title\", \"%s\", \"Message\")\n\t\t}},\n\t\t{\"warning formatted with empty title\", \"\u003e [!WARNING]\\n\u003e Message\\n\", func() string {\n\t\t\treturn mdalert.Warningf(\"\", \"%s\", \"Message\")\n\t\t}},\n\t\t{\"warning formatted multiline\", \"\u003e [!WARNING] Title\\n\u003e Line1\\n\u003e Line2\\n\", func() string {\n\t\t\treturn mdalert.Warningf(\"Title\", \"%s\\n%s\", \"Line1\", \"Line2\")\n\t\t}},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := tt.fn()\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"Got:  %q\\nWant: %q\", got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"h0HMxJykHeosANC3WMsyw+jMkRjAyoW7hhhIF0yoRmlhKFUqDWoX1L/d48YuHFjOKlDuKWcFlQcxLhfKt9zfFA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"authorizable","path":"gno.land/p/nt/ownable/exts/authorizable/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# `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/exts/authorizable/v0\"\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":"authorizable_test.gno","body":"package authorizable\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/ownable/v0\"\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\n// cur is a zero-value realm used as a placeholder when forwarding to\n// uassert/urequire dispatch helpers that gained an `rlm realm` param.\n// These tests pass `func()` callbacks (no crossing inside the callback),\n// so rlm is ignored — a nil realm here is safe.\nvar cur realm\nvar (\n\talice   = testutils.TestAddress(\"alice\")\n\tbob     = testutils.TestAddress(\"bob\")\n\tcharlie = testutils.TestAddress(\"charlie\")\n)\n\nfunc TestNew(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\n\ta := New(ownable.NewWithAddress(alice))\n\tgot := a.Owner()\n\n\tif alice != got {\n\t\tt.Fatalf(\"Expected %s, got: %s\", alice, got)\n\t}\n}\n\nfunc TestOnAuthList(cur realm, t *testing.T) {\n\ta := New(ownable.NewWithAddress(alice))\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\n\t// After SetRealm, cur is the test-frame HIV with addr=alice;\n\t// OnAuthList reports on rlm.Address() (i.e. cur.Address()).\n\tif err := a.OnAuthList(0, cur); err == ErrNotInAuthList {\n\t\tt.Fatalf(\"expected alice to be on the list\")\n\t}\n}\n\nfunc TestNotOnAuthList(cur realm, t *testing.T) {\n\ta := New(ownable.NewWithAddress(alice))\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\n\tif err := a.OnAuthList(0, cur); err == nil {\n\t\tt.Fatalf(\"expected bob to not be on the list\")\n\t}\n}\n\nfunc TestAddToAuthList(cur realm, t *testing.T) {\n\ta := New(ownable.NewWithAddress(alice))\n\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\tvar err error\n\tfunc(cur realm) { err = a.AddToAuthList(0, cur, bob) }(cross(cur))\n\tif err != nil {\n\t\tt.Fatalf(\"Expected no error, got %v\", err)\n\t}\n\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\tfunc(cur realm) { err = a.AddToAuthList(0, cur, bob) }(cross(cur))\n\tif err == nil {\n\t\tt.Fatalf(\"Expected AddToAuth to error while bob called it, but it didn't\")\n\t}\n}\n\nfunc TestDeleteFromList(cur realm, t *testing.T) {\n\ta := New(ownable.NewWithAddress(alice))\n\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\tvar err error\n\tfunc(cur realm) { err = a.AddToAuthList(0, cur, bob) }(cross(cur))\n\tif err != nil {\n\t\tt.Fatalf(\"Expected no error, got %v\", err)\n\t}\n\n\tfunc(cur realm) { err = a.AddToAuthList(0, cur, charlie) }(cross(cur))\n\tif err != nil {\n\t\tt.Fatalf(\"Expected no error, got %v\", err)\n\t}\n\n\t// Try an unauthorized deletion (bob is not the superuser).\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\tfunc(cur realm) { err = a.DeleteFromAuthList(0, cur, alice) }(cross(cur))\n\tif err == nil {\n\t\tt.Fatalf(\"Expected DelFromAuth to error with %v\", err)\n\t}\n\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\tfunc(cur realm) { err = a.DeleteFromAuthList(0, cur, charlie) }(cross(cur))\n\tif err != nil {\n\t\tt.Fatalf(\"Expected no error, got %v\", err)\n\t}\n}\n\nfunc TestAssertOnList(cur realm, t *testing.T) {\n\ta := New(ownable.NewWithAddress(alice))\n\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\n\tuassert.PanicsWithMessage(t, cur, ErrNotInAuthList.Error(), func() {\n\t\ta.AssertOnAuthList(0, cur)\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/exts/authorizable/v0\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"1dwUIBY415g+37PENJk9WTzEdl1hwOwYAnzqmhz1IKM9zIlM+zHqBCNoP+zDb1AtXaNe1HxnHIEIoFPS8Z7ZfA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l","package":{"name":"validators","path":"gno.land/p/sys/validators/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/sys/validators/v0\"\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"aq7/HEqmvWKUZwFSwEmoa0R7CSTnbGVKe3vnHrNBTWIigQsltQpvQIdGZPTldUHlrNCHl0qv0dW0dHZ6MRHE7A=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g15ge0ae9077eh40erwrn2eq0xw6wupwqthpv34l","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/v0`), 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/v0\"\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/v0`.\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/v0\"\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/v0\"\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"},{"name":"poa_test.gno","body":"package poa\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n\t\"gno.land/p/sys/validators/v0\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// generateTestValidators generates a dummy validator set\nfunc generateTestValidators(count int) []validators.Validator {\n\tvals := make([]validators.Validator, 0, count)\n\n\tfor i := 0; i \u003c count; i++ {\n\t\tval := validators.Validator{\n\t\t\tAddress:     testutils.TestAddress(ufmt.Sprintf(\"%d\", i)),\n\t\t\tPubKey:      \"public-key\",\n\t\t\tVotingPower: 1,\n\t\t}\n\n\t\tvals = append(vals, val)\n\t}\n\n\treturn vals\n}\n\nfunc TestPoA_AddValidator_Invalid(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"validator already in set\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tproposalAddress = testutils.TestAddress(\"caller\")\n\t\t\tproposalKey     = \"public-key\"\n\n\t\t\tinitialSet = generateTestValidators(1)\n\t\t)\n\n\t\tinitialSet[0].Address = proposalAddress\n\t\tinitialSet[0].PubKey = proposalKey\n\n\t\t// Create the protocol with an initial set\n\t\tp := NewPoA(WithInitialSet(initialSet))\n\n\t\t// Attempt to add the validator\n\t\t_, err := p.AddValidator(proposalAddress, proposalKey, 1)\n\t\tuassert.ErrorIs(t, err, validators.ErrValidatorExists)\n\t})\n\n\tt.Run(\"invalid voting power\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tproposalAddress = testutils.TestAddress(\"caller\")\n\t\t\tproposalKey     = \"public-key\"\n\t\t)\n\n\t\t// Create the protocol with no initial set\n\t\tp := NewPoA()\n\n\t\t// Attempt to add the validator\n\t\t_, err := p.AddValidator(proposalAddress, proposalKey, 0)\n\t\tuassert.ErrorIs(t, err, ErrInvalidVotingPower)\n\t})\n}\n\nfunc TestPoA_AddValidator(t *testing.T) {\n\tt.Parallel()\n\n\tvar (\n\t\tproposalAddress = testutils.TestAddress(\"caller\")\n\t\tproposalKey     = \"public-key\"\n\t)\n\n\t// Create the protocol with no initial set\n\tp := NewPoA()\n\n\t// Attempt to add the validator\n\t_, err := p.AddValidator(proposalAddress, proposalKey, 1)\n\tuassert.NoError(t, err)\n\n\t// Make sure the validator is added\n\tif !p.IsValidator(proposalAddress) || p.validators.Size() != 1 {\n\t\tt.Fatal(\"address is not validator\")\n\t}\n}\n\nfunc TestPoA_RemoveValidator_Invalid(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"proposed removal not in set\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tproposalAddress = testutils.TestAddress(\"caller\")\n\t\t\tinitialSet      = generateTestValidators(1)\n\t\t)\n\n\t\tinitialSet[0].Address = proposalAddress\n\n\t\t// Create the protocol with an initial set\n\t\tp := NewPoA(WithInitialSet(initialSet))\n\n\t\t// Attempt to remove the validator\n\t\t_, err := p.RemoveValidator(testutils.TestAddress(\"totally random\"))\n\t\tuassert.ErrorIs(t, err, validators.ErrValidatorMissing)\n\t})\n}\n\nfunc TestPoA_RemoveValidator(t *testing.T) {\n\tt.Parallel()\n\n\tvar (\n\t\tproposalAddress = testutils.TestAddress(\"caller\")\n\t\tinitialSet      = generateTestValidators(1)\n\t)\n\n\tinitialSet[0].Address = proposalAddress\n\n\t// Create the protocol with an initial set\n\tp := NewPoA(WithInitialSet(initialSet))\n\n\t// Attempt to remove the validator\n\t_, err := p.RemoveValidator(proposalAddress)\n\turequire.NoError(t, err)\n\n\t// Make sure the validator is removed\n\tif p.IsValidator(proposalAddress) || p.validators.Size() != 0 {\n\t\tt.Fatal(\"address is validator\")\n\t}\n}\n\nfunc TestPoA_GetValidator(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"validator not in set\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create the protocol with no initial set\n\t\tp := NewPoA()\n\n\t\t// Attempt to get the voting power\n\t\t_, err := p.GetValidator(testutils.TestAddress(\"caller\"))\n\t\tuassert.ErrorIs(t, err, validators.ErrValidatorMissing)\n\t})\n\n\tt.Run(\"validator fetched\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\taddress_XXX = testutils.TestAddress(\"caller\")\n\t\t\tpubKey      = \"public-key\"\n\t\t\tvotingPower = uint64(10)\n\n\t\t\tinitialSet = generateTestValidators(1)\n\t\t)\n\n\t\tinitialSet[0].Address = address_XXX\n\t\tinitialSet[0].PubKey = pubKey\n\t\tinitialSet[0].VotingPower = votingPower\n\n\t\t// Create the protocol with an initial set\n\t\tp := NewPoA(WithInitialSet(initialSet))\n\n\t\t// Get the validator\n\t\tval, err := p.GetValidator(address_XXX)\n\t\turequire.NoError(t, err)\n\n\t\t// Validate the address\n\t\tif val.Address != address_XXX {\n\t\t\tt.Fatal(\"invalid address\")\n\t\t}\n\n\t\t// Validate the voting power\n\t\tif val.VotingPower != votingPower {\n\t\t\tt.Fatal(\"invalid voting power\")\n\t\t}\n\n\t\t// Validate the public key\n\t\tif val.PubKey != pubKey {\n\t\t\tt.Fatal(\"invalid public key\")\n\t\t}\n\t})\n}\n\nfunc TestPoA_GetValidators(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"empty set\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create the protocol with no initial set\n\t\tp := NewPoA()\n\n\t\t// Attempt to get the voting power\n\t\tvals := p.GetValidators()\n\n\t\tif len(vals) != 0 {\n\t\t\tt.Fatal(\"validator set is not empty\")\n\t\t}\n\t})\n\n\tt.Run(\"validator set fetched\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tinitialSet := generateTestValidators(10)\n\n\t\t// Create the protocol with an initial set\n\t\tp := NewPoA(WithInitialSet(initialSet))\n\n\t\t// Get the validator set\n\t\tvals := p.GetValidators()\n\n\t\tif len(vals) != len(initialSet) {\n\t\t\tt.Fatal(\"returned validator set mismatch\")\n\t\t}\n\n\t\tfor _, val := range vals {\n\t\t\tfor _, initialVal := range initialSet {\n\t\t\t\tif val.Address != initialVal.Address {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// Validate the voting power\n\t\t\t\tuassert.Equal(t, val.VotingPower, initialVal.VotingPower)\n\n\t\t\t\t// Validate the public key\n\t\t\t\tuassert.Equal(t, val.PubKey, initialVal.PubKey)\n\t\t\t}\n\t\t}\n\t})\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"CFhq7uImi4mprEgdNmyBFfZjd9WwuzESXjIfht65MrpMJsUQYofyaNsg6S/LE7XmOw0M5+4ii/PEb0U0qcLIdQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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/nt/grc20/v0\"\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_canonical_filetest.gno","body":"// PKGPATH: gno.land/r/treasury/canonicaltest\n\npackage canonicaltest\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\n\t\"gno.land/p/nt/treasury/v0\"\n)\n\n// evilBanker embeds *treasury.CoinsBanker so it inherits ID/Send/Balances/\n// Address via promotion. If treasury used a sealed-interface marker, this\n// type would satisfy the interface and bypass the gate. The canonical-impl\n// allowlist (IsCanonicalBanker / treasury.New's type switch) rejects it\n// because type assertions are nominal: *evilBanker is not *CoinsBanker.\ntype evilBanker struct {\n\t*treasury.CoinsBanker\n}\n\nfunc main(cur realm) {\n\townerAddr := chain.PackageAddress(\"gno.land/r/treasury/canonicaltest\")\n\n\tinner := banker.NewBanker(banker.BankerTypeRealmSend, cur)\n\tlegit, err := treasury.NewCoinsBankerWithOwner(ownerAddr, inner)\n\tif err != nil {\n\t\tpanic(\"failed to construct canonical banker: \" + err.Error())\n\t}\n\n\t// Verify the helper accepts the canonical impl.\n\tif !treasury.IsCanonicalBanker(legit) {\n\t\tpanic(\"canonical *CoinsBanker must pass IsCanonicalBanker\")\n\t}\n\n\t// Verify the helper rejects an embedded-impl bypass attempt.\n\tevil := \u0026evilBanker{CoinsBanker: legit}\n\tif treasury.IsCanonicalBanker(evil) {\n\t\tpanic(\"embedded-impl wrapper must NOT pass IsCanonicalBanker\")\n\t}\n\n\t// Verify treasury.New rejects the same bypass attempt.\n\t_, err = treasury.New([]treasury.Banker{evil}, \"\")\n\tif err != treasury.ErrNonCanonicalBankerImpl {\n\t\tpanic(\"expected ErrNonCanonicalBankerImpl from treasury.New; got: \" + err.Error())\n\t}\n\n\t// And confirms the canonical banker still works.\n\t_, err = treasury.New([]treasury.Banker{legit}, \"\")\n\tif err != nil {\n\t\tpanic(\"canonical banker must be accepted: \" + err.Error())\n\t}\n\n\tprintln(\"canonical allowlist OK\")\n}\n\n// Output:\n// canonical allowlist OK\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/v0\"\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.\n//\n// The coins are copied, so the caller cannot change the amount after the\n// payment exists, and sorted and joined, so String() renders the set that will\n// actually be paid. A non-positive amount is rejected here rather than by the\n// bank keeper at execution, after a vote. A malformed denom is still only\n// caught by the keeper.\nfunc NewCoinsPayment(coins chain.Coins, toAddress address) Payment {\n\tcz := chain.Coins{}.Add(coins)\n\n\tfor _, c := range cz {\n\t\tif !c.IsPositive() {\n\t\t\tpanic(\"coins payment amounts must be positive, got \" + c.String())\n\t\t}\n\t}\n\n\treturn coinsPayment{\n\t\tcoins:     cz,\n\t\ttoAddress: toAddress,\n\t}\n}\n"},{"name":"banker_coins_filetest.gno","body":"// PKGPATH: gno.land/r/treasury/main\n\npackage main\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/treasury/v0\"\n)\n\nfunc main(cur realm) {\n\t// Define addresses for the sender (owner) and destination.\n\townerAddr := chain.PackageAddress(\"gno.land/r/treasury/main\")\n\tdestAddr := chain.PackageAddress(\"gno.land/r/dest/main\")\n\n\t// Create a CoinsBanker.\n\tbanker_ := banker.NewBanker(banker.BankerTypeRealmSend, cur)\n\tcbanker, err := treasury.NewCoinsBankerWithOwner(ownerAddr, banker_)\n\tif err != nil {\n\t\tpanic(\"failed to create CoinsBanker: \" + err.Error())\n\t}\n\n\tprintln(\"CoinsBanker ID:\", cbanker.ID())\n\tprintln(\"CoinsBanker Address:\", cbanker.Address())\n\n\t// Check if the CoinsBanker address matches the owner address.\n\tif cbanker.Address() != ownerAddr.String() {\n\t\tpanic(\"CoinsBanker address does not match current realm address\")\n\t}\n\n\tprintln(\"CoinsBanker Balances count:\", len(cbanker.Balances()))\n\n\t// Issue some coins to the owner address.\n\ttesting.IssueCoins(ownerAddr, chain.NewCoins(chain.NewCoin(\"ugnot\", 42)))\n\n\tprintln(\"CoinsBanker Balances count:\", len(cbanker.Balances()))\n\tprintln(\"Ugnot balance:\", cbanker.Balances()[0].Amount)\n\n\t// Send a valid payment.\n\tvalidPayment := treasury.NewCoinsPayment(\n\t\tchain.NewCoins(chain.NewCoin(\"ugnot\", 10)),\n\t\tdestAddr,\n\t)\n\terr = cbanker.Send(0, cur, validPayment)\n\tprintln(\"Valid payment error:\", err)\n\tif err != nil {\n\t\tpanic(\"failed to send valid payment: \" + err.Error())\n\t}\n\n\tprintln(\"Ugnot balance:\", cbanker.Balances()[0].Amount)\n\n\t// Send a payment with an invalid type.\n\tinvalidPaymentType := treasury.NewGRC20Payment(\"\", 0, destAddr)\n\terr = cbanker.Send(0, cur, invalidPaymentType)\n\tprintln(\"Invalid payment type error:\", err)\n\tif err == nil {\n\t\tpanic(\"expected error for invalid payment type, but got none\")\n\t}\n\n\t// Issue another coin to the owner address to test the Balances method.\n\ttesting.IssueCoins(ownerAddr, chain.NewCoins(chain.NewCoin(\"anothercoin\", 1337)))\n\n\tprintln(\"CoinsBanker Balances count:\", len(cbanker.Balances()))\n}\n\n// Output:\n// CoinsBanker ID: Coins\n// CoinsBanker Address: g1ynsdz5zaxhn9gnqtr6t40m5k4fueeutq7xy224\n// CoinsBanker Balances count: 0\n// CoinsBanker Balances count: 1\n// Ugnot balance: 42\n// Valid payment error: undefined\n// Ugnot balance: 32\n// Invalid payment type error: invalid payment type\n// CoinsBanker Balances count: 2\n"},{"name":"banker_coins_payment_canonical_filetest.gno","body":"// PKGPATH: gno.land/r/treasury/main\n\npackage main\n\nimport (\n\t\"chain\"\n\n\t\"gno.land/p/nt/treasury/v0\"\n)\n\n// foreign satisfies Payment without being one of treasury's own impls: a\n// BankerID that names a registered banker, and a String() saying whatever\n// its realm likes. No Banker will process it, because Banker.Send\n// type-asserts on the concrete type.\ntype foreign struct{}\n\nfunc (foreign) BankerID() string { return \"Coins\" }\n\nfunc (foreign) String() string { return \"42ugnot to g1nobody\" }\n\nvar destAddr = chain.PackageAddress(\"gno.land/r/dest/main\")\n\nfunc main(cur realm) {\n\t// A hand-built set with duplicate denoms is joined, so String() renders\n\t// the amount the bank keeper will actually debit rather than \"5ugnot,10ugnot\".\n\tdup := chain.Coins{\n\t\tchain.Coin{Denom: \"ugnot\", Amount: 5},\n\t\tchain.Coin{Denom: \"ugnot\", Amount: 10},\n\t}\n\tprintln(\"duplicate denoms joined:\", treasury.NewCoinsPayment(dup, destAddr).String())\n\n\t// An unsorted set is sorted, for the same reason: std.Coins.validate\n\t// rejects \"coins not sorted\" at execution, after the vote.\n\tunsorted := chain.Coins{\n\t\tchain.Coin{Denom: \"zzz\", Amount: 1},\n\t\tchain.Coin{Denom: \"aaa\", Amount: 2},\n\t}\n\tprintln(\"unsorted set sorted:\", treasury.NewCoinsPayment(unsorted, destAddr).String())\n\n\t// A non-positive amount is refused at construction, not at execution.\n\tprintln(\"negative amount:\", mustPanic(func() {\n\t\ttreasury.NewCoinsPayment(chain.Coins{chain.Coin{Denom: \"ugnot\", Amount: -5}}, destAddr)\n\t}))\n\n\t// Joining can produce a non-positive amount the caller never wrote.\n\tprintln(\"cancels to negative:\", mustPanic(func() {\n\t\ttreasury.NewCoinsPayment(chain.Coins{\n\t\t\tchain.Coin{Denom: \"ugnot\", Amount: 5},\n\t\t\tchain.Coin{Denom: \"ugnot\", Amount: -10},\n\t\t}, destAddr)\n\t}))\n\n\t// The canonical impls are recognised; a foreign one is not.\n\tprintln(\"coins payment canonical:\", treasury.IsCanonicalPayment(\n\t\ttreasury.NewCoinsPayment(chain.NewCoins(chain.NewCoin(\"ugnot\", 1)), destAddr)))\n\tprintln(\"grc20 payment canonical:\", treasury.IsCanonicalPayment(\n\t\ttreasury.NewGRC20Payment(\"TOK\", 1, destAddr)))\n\tprintln(\"foreign payment canonical:\", treasury.IsCanonicalPayment(foreign{}))\n}\n\nfunc mustPanic(fn func()) (msg string) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tif s, ok := r.(string); ok {\n\t\t\t\tmsg = s\n\t\t\t\treturn\n\t\t\t}\n\t\t\tmsg = \"rejected\"\n\t\t}\n\t}()\n\tfn()\n\treturn \"NOT REJECTED\"\n}\n\n// Output:\n// duplicate denoms joined: 15ugnot to g1fs92ep7z5vtrp26d767ag3zztsd6wqn9rew4ve\n// unsorted set sorted: 2aaa,1zzz to g1fs92ep7z5vtrp26d767ag3zztsd6wqn9rew4ve\n// negative amount: coins payment amounts must be positive, got -5ugnot\n// cancels to negative: coins payment amounts must be positive, got -5ugnot\n// coins payment canonical: true\n// grc20 payment canonical: true\n// foreign payment canonical: false\n"},{"name":"banker_coins_payment_copy_filetest.gno","body":"// PKGPATH: gno.land/r/treasury/main\n\npackage main\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/treasury/v0\"\n)\n\n// coins is this realm's own slice. It is handed to NewCoinsPayment and then\n// rewritten, which is what a realm supplying the coins for a governance\n// payment proposal can do between the vote and the execution.\nvar coins = chain.Coins{chain.Coin{Denom: \"ugnot\", Amount: 10}}\n\nfunc main(cur realm) {\n\townerAddr := chain.PackageAddress(\"gno.land/r/treasury/main\")\n\tdestAddr := chain.PackageAddress(\"gno.land/r/dest/main\")\n\n\tbanker_ := banker.NewBanker(banker.BankerTypeRealmSend, cur)\n\tcbanker, err := treasury.NewCoinsBankerWithOwner(ownerAddr, banker_)\n\tif err != nil {\n\t\tpanic(\"failed to create CoinsBanker: \" + err.Error())\n\t}\n\ttesting.IssueCoins(ownerAddr, chain.NewCoins(chain.NewCoin(\"ugnot\", 1000)))\n\n\tpayment := treasury.NewCoinsPayment(coins, destAddr)\n\tprintln(\"payment as created:\", payment.String())\n\n\t// The supplying realm rewrites its own array. The payment must not follow.\n\tcoins[0].Amount = 900\n\tprintln(\"payment after the rewrite:\", payment.String())\n\n\tif err := cbanker.Send(0, cur, payment); err != nil {\n\t\tpanic(\"failed to send payment: \" + err.Error())\n\t}\n\tprintln(\"owner balance after send:\", cbanker.Balances()[0].Amount)\n}\n\n// Output:\n// payment as created: 10ugnot to g1fs92ep7z5vtrp26d767ag3zztsd6wqn9rew4ve\n// payment after the rewrite: 10ugnot to g1fs92ep7z5vtrp26d767ag3zztsd6wqn9rew4ve\n// owner balance after send: 990\n"},{"name":"banker_grc20.gno","body":"package treasury\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n\n\t\"gno.land/p/nt/grc20/v0\"\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":"banker_grc20_filetest.gno","body":"// PKGPATH: gno.land/r/treasury/main\n\npackage main\n\nimport (\n\t\"chain\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/grc20/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/p/nt/treasury/v0\"\n)\n\nconst amount = int64(1000)\n\nvar nextTokenID seqid.ID\n\nfunc createToken(_ int, rlm realm, name string, toMint address) *grc20.Token {\n\t// Create the token.\n\tsymbol := strings.ToUpper(name)\n\ttoken, ledger := grc20.NewToken(name, symbol, 0, nextTokenID.Next(), rlm)\n\n\t// Mint the requested amount.\n\tledger.Mint(toMint, amount)\n\n\treturn token\n}\n\nfunc main(cur realm) {\n\t// Define addresses for the sender (owner) and destination.\n\townerAddr := chain.PackageAddress(\"gno.land/r/treasury/main\")\n\tdestAddr := chain.PackageAddress(\"gno.land/r/dest/main\")\n\n\t// Try to create a GRC20Banker using a nil lister.\n\tgbanker, err := treasury.NewGRC20BankerWithOwner(ownerAddr, nil)\n\tif err == nil {\n\t\tpanic(\"expected error when creating GRC20Banker with nil lister\")\n\t}\n\n\t// Define a list of token and the associated lister.\n\ttokens := []*grc20.Token{\n\t\tcreateToken(0, cur, \"TestToken0\", ownerAddr),\n\t\tcreateToken(0, cur, \"TestToken1\", ownerAddr),\n\t\tcreateToken(0, cur, \"TestToken2\", ownerAddr),\n\t}\n\n\tgrc20Lister := func() map[string]*grc20.Token {\n\t\ttokensMap := make(map[string]*grc20.Token, len(tokens))\n\n\t\tfor _, token := range tokens {\n\t\t\ttokensMap[token.GetSymbol()] = token\n\t\t}\n\n\t\treturn tokensMap\n\t}\n\n\t// Create a GRC20Banker.\n\tgbanker, err = treasury.NewGRC20BankerWithOwner(ownerAddr, grc20Lister)\n\tif err != nil {\n\t\tpanic(\"failed to create GRC20Banker: \" + err.Error())\n\t}\n\n\tprintln(\"GRC20Banker ID:\", gbanker.ID())\n\tprintln(\"GRC20Banker Address:\", gbanker.Address())\n\n\t// Check if the GRC20Banker address matches the owner address.\n\tif gbanker.Address() != ownerAddr.String() {\n\t\tpanic(\"GRC20Banker address does not match current realm address\")\n\t}\n\n\t// Check the balances of the GRC20Banker.\n\tprintln(\"GRC20Banker Balances count:\", len(gbanker.Balances()))\n\tfor _, balance := range gbanker.Balances() {\n\t\tif balance.Amount != amount {\n\t\t\tpanic(\"GRC20Banker balance does not match expected amount\")\n\t\t}\n\t}\n\n\t// Send a valid payment.\n\ttoken := tokens[len(tokens)-1]\n\tvalidPayment := treasury.NewGRC20Payment(\n\t\ttoken.GetSymbol(),\n\t\t100,\n\t\tdestAddr,\n\t)\n\terr = gbanker.Send(0, cur, validPayment)\n\tprintln(\"Valid payment error:\", err)\n\tif err != nil {\n\t\tpanic(\"failed to send valid payment: \" + err.Error())\n\t}\n\n\tprintln(\"Owner balance:\", token.BalanceOf(ownerAddr))\n\tprintln(\"Dest balance:\", token.BalanceOf(destAddr))\n\n\t// Send an unknown token payment.\n\tunknownPayment := treasury.NewGRC20Payment(\n\t\t\"unknown\",\n\t\t100,\n\t\tdestAddr,\n\t)\n\terr = gbanker.Send(0, cur, unknownPayment)\n\tprintln(\"Unknown token payment error:\", err)\n\tif err == nil {\n\t\tpanic(\"expected error for unknown token, but got none\")\n\t}\n\n\t// Send an unsufficient funds payment.\n\tunsufficientPayment := treasury.NewGRC20Payment(\n\t\ttokens[0].GetSymbol(),\n\t\tamount+1,\n\t\tdestAddr,\n\t)\n\terr = gbanker.Send(0, cur, unsufficientPayment)\n\tprintln(\"Unsufficient funds payment error:\", err)\n\tif err == nil {\n\t\tpanic(\"expected error for insufficient funds, but got none\")\n\t}\n\n\t// Send a payment with an invalid type.\n\tinvalidPaymentType := treasury.NewCoinsPayment(chain.Coins{}, destAddr)\n\terr = gbanker.Send(0, cur, invalidPaymentType)\n\tprintln(\"Invalid payment type error:\", err)\n\tif err == nil {\n\t\tpanic(\"expected error for invalid payment type, but got none\")\n\t}\n}\n\n// Output:\n// GRC20Banker ID: GRC20\n// GRC20Banker Address: g1ynsdz5zaxhn9gnqtr6t40m5k4fueeutq7xy224\n// GRC20Banker Balances count: 3\n// Valid payment error: undefined\n// Owner balance: 900\n// Dest balance: 100\n// Unknown token payment error: GRC20 token not found: unknown\n// Unsufficient funds payment error: insufficient balance\n// Invalid payment type error: invalid payment type\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/v0\"\n\t\"gno.land/p/moul/mdtable/v0\"\n\t\"gno.land/p/nt/bptree/pager/v0\"\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: \" + md.EscapeText(bankerID))\n\t}\n\tbanker := br.(*bankerRecord).banker\n\n\t// Render banker title.\n\tout := md.H2(md.EscapeText(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: \" + md.EscapeText(bankerID))\n\t}\n\thistory := br.(*bankerRecord).history\n\n\t// Render banker history title.\n\tout := md.H2(md.EscapeText(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: \" + md.EscapeText(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/pager/v0\"\n\t\"gno.land/p/nt/bptree/v0\"\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":"treasury_filetest.gno","body":"// PKGPATH: gno.land/r/treasury/main\n\npackage main\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/grc20/v0\"\n\t\"gno.land/p/nt/treasury/v0\"\n)\n\nfunc checkBalanceAndHistory(t *treasury.Treasury, bankerIDs []string) {\n\tfor _, bankerID := range bankerIDs {\n\t\tbalances, err := t.Balances(bankerID)\n\t\tif err != nil {\n\t\t\tpanic(\"failed to get banker balances: \" + err.Error())\n\t\t}\n\t\tprintln(\"Banker\", bankerID, \"Balance:\", balances[0].Amount)\n\n\t\thistory, err := t.History(bankerID, 1, 10)\n\t\tif err != nil {\n\t\t\tpanic(\"failed to get banker history: \" + err.Error())\n\t\t}\n\t\tprintln(\"Banker\", bankerID, \"History count:\", len(history))\n\t}\n}\n\nfunc main(cur realm) {\n\t// Define addresses for the sender (owner) and destination.\n\townerAddr := chain.PackageAddress(\"gno.land/r/treasury/main\")\n\tdestAddr := chain.PackageAddress(\"gno.land/r/dest/main\")\n\n\t// Try to create a Treasury instance with no bankers.\n\t_, err := treasury.New(nil, \"\")\n\tif err != treasury.ErrNoBankerProvided {\n\t\tpanic(\"expected error when creating Treasury with no bankers\")\n\t}\n\n\t// Define a token and the associated lister.\n\tconst amount = int64(1000)\n\ttoken, ledger := grc20.NewToken(\"TestToken\", \"TEST\", 0, 0, cur)\n\tledger.Mint(ownerAddr, amount)\n\n\tgrc20Lister := func() map[string]*grc20.Token {\n\t\treturn map[string]*grc20.Token{\n\t\t\t\"TEST\": token,\n\t\t}\n\t}\n\n\t// Try to create a Treasury instance with a duplicate banker.\n\tvar (\n\t\tbanker_           = banker.NewBanker(banker.BankerTypeRealmSend, cur)\n\t\tcoinsBanker, _    = treasury.NewCoinsBankerWithOwner(ownerAddr, banker_)\n\t\tgrc20Banker, _    = treasury.NewGRC20BankerWithOwner(ownerAddr, grc20Lister)\n\t\tgrc20BankerDup, _ = treasury.NewGRC20BankerWithOwner(ownerAddr, grc20Lister)\n\t)\n\n\t_, err = treasury.New([]treasury.Banker{coinsBanker, grc20Banker, grc20BankerDup}, \"\")\n\tif !strings.Contains(err.Error(), treasury.ErrDuplicateBanker.Error()) {\n\t\tpanic(\"expected error when creating Treasury with duplicate banker\")\n\t}\n\n\t// Create a Treasury instance with valid bankers.\n\tbankers := []treasury.Banker{coinsBanker, grc20Banker}\n\tt, err := treasury.New(bankers, \"\")\n\tif err != nil {\n\t\tpanic(\"failed to create Treasury: \" + err.Error())\n\t}\n\n\t// Test if the Treasury instance has the expected bankers.\n\tprintln(\"Treasury banker IDs:\", t.ListBankerIDs())\n\n\tconst unknownBankerID = \"unknown-banker-id\"\n\n\tif t.HasBanker(unknownBankerID) {\n\t\tpanic(\"expected banker not to be found\")\n\t}\n\n\t// Check if the addresses of the bankers matches the owner address.\n\tfor _, banker_ := range bankers {\n\t\taddr, err := t.Address(banker_.ID())\n\t\tif err != nil {\n\t\t\tpanic(\"failed to get banker address: \" + err.Error())\n\t\t}\n\t\tprintln(\"Banker\", banker_.ID(), \"Address:\", addr)\n\t}\n\n\t// Check if the balances and history of the bankers match the expected values.\n\ttesting.IssueCoins(ownerAddr, chain.NewCoins(chain.NewCoin(\"ugnot\", amount)))\n\tbankerIDs := []string{coinsBanker.ID(), grc20Banker.ID()}\n\tcheckBalanceAndHistory(t, bankerIDs)\n\n\t// Send 3 valid payments using the CoinsBanker.\n\tvalidCoinsPayment := treasury.NewCoinsPayment(\n\t\tchain.NewCoins(chain.NewCoin(\"ugnot\", 100)),\n\t\tdestAddr,\n\t)\n\tfor i := 0; i \u003c 3; i++ {\n\t\terr = t.Send(0, cur, validCoinsPayment)\n\t\tif err != nil {\n\t\t\tpanic(\"failed to send valid Coins payment: \" + err.Error())\n\t\t}\n\t}\n\n\t// Send 3 valid payments using the GRC20Banker.\n\tvalidGRC20Payment := treasury.NewGRC20Payment(\n\t\ttoken.GetSymbol(),\n\t\t100,\n\t\tdestAddr,\n\t)\n\tfor i := 0; i \u003c 3; i++ {\n\t\terr = t.Send(0, cur, validGRC20Payment)\n\t\tif err != nil {\n\t\t\tpanic(\"failed to send valid GRC20 payment: \" + err.Error())\n\t\t}\n\t}\n\n\t// Check if the balances and history of the bankers match the expected values.\n\tcheckBalanceAndHistory(t, bankerIDs)\n}\n\n// Output:\n// Treasury banker IDs: slice[(\"Coins\" string),(\"GRC20\" string)]\n// Banker Coins Address: g1ynsdz5zaxhn9gnqtr6t40m5k4fueeutq7xy224\n// Banker GRC20 Address: g1ynsdz5zaxhn9gnqtr6t40m5k4fueeutq7xy224\n// Banker Coins Balance: 1000\n// Banker Coins History count: 0\n// Banker GRC20 Balance: 1000\n// Banker GRC20 History count: 0\n// Banker Coins Balance: 700\n// Banker Coins History count: 3\n// Banker GRC20 Balance: 700\n// Banker GRC20 History count: 3\n"},{"name":"types.gno","body":"package treasury\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/bptree/list/v0\"\n\t\"gno.land/p/nt/bptree/v0\"\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// IsCanonicalPayment reports whether p is one of treasury's canonical concrete\n// Payment impls. Use this at any public entry point that accepts a Payment\n// from an external caller and shows it to a human before it is processed.\n//\n// The immutability the constructors provide is a property of these two types,\n// not of the interface: a foreign impl's String() says whatever its realm\n// likes, and no Banker will process it, because Banker.Send type-asserts and\n// so fails only at execution — after a vote has been spent on it.\n//\n// Only the value types are listed, deliberately. Banker.Send asserting\n// p.(coinsPayment) is what makes this check meaningful, so accepting\n// *coinsPayment would pass a payment that still fails at execution.\n//\n// To add a new canonical type: extend the switch below AND add a regression\n// test (under filetests/ in this package) that a foreign impl is rejected.\n//\n// Mirrors IsCanonicalBanker above.\nfunc IsCanonicalPayment(p Payment) bool {\n\tswitch p.(type) {\n\tcase coinsPayment, grc20Payment:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"M4E7LMqUt9wTVmvMMZG26WHT/J6CoHZ3Nsmz23CHvb4tingrEDw20SvIgKBU6SL86Q9bxpYRMG3hqcrEVZXL+g=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"treasury","path":"gno.land/p/nt/treasury/v1","files":[{"name":"README.md","body":"\u003e **v1 - 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` v1 - Coin and GRC20 treasury management\n\n## Differences from v0\n\n- `Balance.Amount` is a `uint256` (v0: `int64`).\n- `NewGRC20Payment` takes a `uint256` amount, and `GRC20Banker` handles\n  `gno.land/p/nt/grc20/v1` tokens (`TokenListerFunc` returns v1 tokens).\n- `NewCoinsPayment` takes `[]Balance` (uint256 amounts) instead of\n  `chain.Coins`. Chain coins stay `int64`, so it sums the amounts per denom\n  with a checked add and range-checks each total down to `int64`: an amount\n  of zero or above `math.MaxInt64` panics when the payment is built, before\n  anyone votes on it.\n- `CoinsBanker.Balances` widens the `int64` chain coin amounts to `uint256`.\n\nv0 stays as it is for the realms already built on it (`r/gov/dao/treasury/v0`);\n`r/gov/dao/treasury/v1` is the GovDAO treasury built on v1.\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/banker\"\n    \"chain/runtime\"\n\n    \"gno.land/p/nt/grc20/v1\"\n    \"gno.land/p/nt/treasury/v1\"\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 uint256) {\n    p := treasury.NewCoinsPayment([]treasury.Balance{{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 uint256\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\n// Amounts are joined per denom and must each total [1, math.MaxInt64]; it\n// panics otherwise.\nfunc NewCoinsPayment(amounts []Balance, 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 uint256, 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\nErrCoinAmountOutOfRange   // a coins payment amount outside [1, MaxInt64]\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_canonical_filetest.gno","body":"// PKGPATH: gno.land/r/treasury/canonicaltest\n\npackage canonicaltest\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\n\t\"gno.land/p/nt/treasury/v1\"\n)\n\n// evilBanker embeds *treasury.CoinsBanker so it inherits ID/Send/Balances/\n// Address via promotion. If treasury used a sealed-interface marker, this\n// type would satisfy the interface and bypass the gate. The canonical-impl\n// allowlist (IsCanonicalBanker / treasury.New's type switch) rejects it\n// because type assertions are nominal: *evilBanker is not *CoinsBanker.\ntype evilBanker struct {\n\t*treasury.CoinsBanker\n}\n\nfunc main(cur realm) {\n\townerAddr := chain.PackageAddress(\"gno.land/r/treasury/canonicaltest\")\n\n\tinner := banker.NewBanker(banker.BankerTypeRealmSend, cur)\n\tlegit, err := treasury.NewCoinsBankerWithOwner(ownerAddr, inner)\n\tif err != nil {\n\t\tpanic(\"failed to construct canonical banker: \" + err.Error())\n\t}\n\n\t// Verify the helper accepts the canonical impl.\n\tif !treasury.IsCanonicalBanker(legit) {\n\t\tpanic(\"canonical *CoinsBanker must pass IsCanonicalBanker\")\n\t}\n\n\t// Verify the helper rejects an embedded-impl bypass attempt.\n\tevil := \u0026evilBanker{CoinsBanker: legit}\n\tif treasury.IsCanonicalBanker(evil) {\n\t\tpanic(\"embedded-impl wrapper must NOT pass IsCanonicalBanker\")\n\t}\n\n\t// Verify treasury.New rejects the same bypass attempt.\n\t_, err = treasury.New([]treasury.Banker{evil}, \"\")\n\tif err != treasury.ErrNonCanonicalBankerImpl {\n\t\tpanic(\"expected ErrNonCanonicalBankerImpl from treasury.New; got: \" + err.Error())\n\t}\n\n\t// And confirms the canonical banker still works.\n\t_, err = treasury.New([]treasury.Banker{legit}, \"\")\n\tif err != nil {\n\t\tpanic(\"canonical banker must be accepted: \" + err.Error())\n\t}\n\n\tprintln(\"canonical allowlist OK\")\n}\n\n// Output:\n// canonical allowlist OK\n"},{"name":"banker_coins.gno","body":"package treasury\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"errors\"\n\t\"math\"\n\t\"math/overflow\"\n\t\"strconv\"\n\n\t\"gno.land/p/aeddi/panictoerr/v0\"\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. A held coin amount is never\n\t// negative, so widening it to uint256 is exact.\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: uint256(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 from uint256 amounts.\n//\n// Chain coins are int64, so each amount is range-checked down here: an amount\n// of zero or above math.MaxInt64 is refused (it panics) when the payment is\n// built, rather than by the bank keeper at execution, after a vote. Amounts\n// of the same denom are summed with a checked add first, so the check applies\n// to what will actually be paid.\n//\n// The payment holds its own sorted, joined chain.Coins, so the caller cannot\n// change the amount after the payment exists, and String() renders the set\n// that will actually be paid. A malformed denom is still only caught by the\n// keeper.\nfunc NewCoinsPayment(amounts []Balance, toAddress address) Payment {\n\t// Join amounts by denom, in uint256 so that the sum cannot wrap.\n\ttotals := map[string]uint256{}\n\tdenoms := []string{}\n\tfor _, a := range amounts {\n\t\tprev, seen := totals[a.Denom]\n\t\tif !seen {\n\t\t\tdenoms = append(denoms, a.Denom)\n\t\t}\n\t\tsum, ok := overflow.Addu256(prev, a.Amount)\n\t\tif !ok {\n\t\t\tpanic(\"coins payment amount overflows uint256 for denom \" + a.Denom)\n\t\t}\n\t\ttotals[a.Denom] = sum\n\t}\n\n\tcz := chain.Coins{}\n\tfor _, denom := range denoms {\n\t\tamount, err := coinAmount(totals[denom])\n\t\tif err != nil {\n\t\t\tpanic(err.Error() + \", got \" + strconv.FormatUint256(totals[denom], 10) + denom)\n\t\t}\n\t\tcz = cz.Add(chain.Coins{chain.NewCoin(denom, amount)})\n\t}\n\n\treturn coinsPayment{\n\t\tcoins:     cz,\n\t\ttoAddress: toAddress,\n\t}\n}\n\n// ErrCoinAmountOutOfRange is returned when a uint256 amount cannot be paid in\n// chain coins, which are positive int64 values.\nvar ErrCoinAmountOutOfRange = errors.New(\"coins payment amounts must be in [1, MaxInt64]\")\n\n// coinAmount range-checks a uint256 amount down to a positive int64 chain\n// coin amount.\nfunc coinAmount(amount uint256) (int64, error) {\n\tif amount == 0 || amount \u003e math.MaxInt64 {\n\t\treturn 0, ErrCoinAmountOutOfRange\n\t}\n\treturn int64(amount), nil\n}\n"},{"name":"banker_coins_filetest.gno","body":"// PKGPATH: gno.land/r/treasury/main\n\npackage main\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/treasury/v1\"\n)\n\nfunc main(cur realm) {\n\t// Define addresses for the sender (owner) and destination.\n\townerAddr := chain.PackageAddress(\"gno.land/r/treasury/main\")\n\tdestAddr := chain.PackageAddress(\"gno.land/r/dest/main\")\n\n\t// Create a CoinsBanker.\n\tbanker_ := banker.NewBanker(banker.BankerTypeRealmSend, cur)\n\tcbanker, err := treasury.NewCoinsBankerWithOwner(ownerAddr, banker_)\n\tif err != nil {\n\t\tpanic(\"failed to create CoinsBanker: \" + err.Error())\n\t}\n\n\tprintln(\"CoinsBanker ID:\", cbanker.ID())\n\tprintln(\"CoinsBanker Address:\", cbanker.Address())\n\n\t// Check if the CoinsBanker address matches the owner address.\n\tif cbanker.Address() != ownerAddr.String() {\n\t\tpanic(\"CoinsBanker address does not match current realm address\")\n\t}\n\n\tprintln(\"CoinsBanker Balances count:\", len(cbanker.Balances()))\n\n\t// Issue some coins to the owner address.\n\ttesting.IssueCoins(ownerAddr, chain.NewCoins(chain.NewCoin(\"ugnot\", 42)))\n\n\tprintln(\"CoinsBanker Balances count:\", len(cbanker.Balances()))\n\tprintln(\"Ugnot balance:\", cbanker.Balances()[0].Amount)\n\n\t// Send a valid payment.\n\tvalidPayment := treasury.NewCoinsPayment(\n\t\t[]treasury.Balance{{Denom: \"ugnot\", Amount: 10}},\n\t\tdestAddr,\n\t)\n\terr = cbanker.Send(0, cur, validPayment)\n\tprintln(\"Valid payment error:\", err)\n\tif err != nil {\n\t\tpanic(\"failed to send valid payment: \" + err.Error())\n\t}\n\n\tprintln(\"Ugnot balance:\", cbanker.Balances()[0].Amount)\n\n\t// Send a payment with an invalid type.\n\tinvalidPaymentType := treasury.NewGRC20Payment(\"\", 0, destAddr)\n\terr = cbanker.Send(0, cur, invalidPaymentType)\n\tprintln(\"Invalid payment type error:\", err)\n\tif err == nil {\n\t\tpanic(\"expected error for invalid payment type, but got none\")\n\t}\n\n\t// Issue another coin to the owner address to test the Balances method.\n\ttesting.IssueCoins(ownerAddr, chain.NewCoins(chain.NewCoin(\"anothercoin\", 1337)))\n\n\tprintln(\"CoinsBanker Balances count:\", len(cbanker.Balances()))\n}\n\n// Output:\n// CoinsBanker ID: Coins\n// CoinsBanker Address: g1ynsdz5zaxhn9gnqtr6t40m5k4fueeutq7xy224\n// CoinsBanker Balances count: 0\n// CoinsBanker Balances count: 1\n// Ugnot balance: 42\n// Valid payment error: undefined\n// Ugnot balance: 32\n// Invalid payment type error: invalid payment type\n// CoinsBanker Balances count: 2\n"},{"name":"banker_coins_payment_canonical_filetest.gno","body":"// PKGPATH: gno.land/r/treasury/main\n\npackage main\n\nimport (\n\t\"chain\"\n\t\"math\"\n\n\t\"gno.land/p/nt/treasury/v1\"\n)\n\n// foreign satisfies Payment without being one of treasury's own impls: a\n// BankerID that names a registered banker, and a String() saying whatever\n// its realm likes. No Banker will process it, because Banker.Send\n// type-asserts on the concrete type.\ntype foreign struct{}\n\nfunc (foreign) BankerID() string { return \"Coins\" }\n\nfunc (foreign) String() string { return \"42ugnot to g1nobody\" }\n\nvar destAddr = chain.PackageAddress(\"gno.land/r/dest/main\")\n\nfunc main(cur realm) {\n\t// A hand-built set with duplicate denoms is joined, so String() renders\n\t// the amount the bank keeper will actually debit rather than \"5ugnot,10ugnot\".\n\tdup := []treasury.Balance{\n\t\t{Denom: \"ugnot\", Amount: 5},\n\t\t{Denom: \"ugnot\", Amount: 10},\n\t}\n\tprintln(\"duplicate denoms joined:\", treasury.NewCoinsPayment(dup, destAddr).String())\n\n\t// An unsorted set is sorted, for the same reason: std.Coins.validate\n\t// rejects \"coins not sorted\" at execution, after the vote.\n\tunsorted := []treasury.Balance{\n\t\t{Denom: \"zzz\", Amount: 1},\n\t\t{Denom: \"aaa\", Amount: 2},\n\t}\n\tprintln(\"unsorted set sorted:\", treasury.NewCoinsPayment(unsorted, destAddr).String())\n\n\t// A zero amount is refused at construction, not at execution.\n\tprintln(\"zero amount:\", mustPanic(func() {\n\t\ttreasury.NewCoinsPayment([]treasury.Balance{{Denom: \"ugnot\", Amount: 0}}, destAddr)\n\t}))\n\n\t// Chain coins are int64: the largest payable amount is MaxInt64 ...\n\tprintln(\"max int64:\", treasury.NewCoinsPayment(\n\t\t[]treasury.Balance{{Denom: \"ugnot\", Amount: math.MaxInt64}}, destAddr).String())\n\n\t// ... and one more is refused rather than truncated.\n\tprintln(\"above max int64:\", mustPanic(func() {\n\t\ttreasury.NewCoinsPayment([]treasury.Balance{{Denom: \"ugnot\", Amount: math.MaxInt64 + 1}}, destAddr)\n\t}))\n\n\t// Joining can take a sum out of range although each part is in range.\n\tprintln(\"sum above max int64:\", mustPanic(func() {\n\t\ttreasury.NewCoinsPayment([]treasury.Balance{\n\t\t\t{Denom: \"ugnot\", Amount: math.MaxInt64},\n\t\t\t{Denom: \"ugnot\", Amount: 1},\n\t\t}, destAddr)\n\t}))\n\n\t// A sum that would wrap uint256 is refused too, not wrapped to a small\n\t// payable amount.\n\tprintln(\"sum wraps uint256:\", mustPanic(func() {\n\t\ttreasury.NewCoinsPayment([]treasury.Balance{\n\t\t\t{Denom: \"ugnot\", Amount: math.MaxUint256},\n\t\t\t{Denom: \"ugnot\", Amount: 2},\n\t\t}, destAddr)\n\t}))\n\n\t// The canonical impls are recognised; a foreign one is not.\n\tprintln(\"coins payment canonical:\", treasury.IsCanonicalPayment(\n\t\ttreasury.NewCoinsPayment([]treasury.Balance{{Denom: \"ugnot\", Amount: 1}}, destAddr)))\n\tprintln(\"grc20 payment canonical:\", treasury.IsCanonicalPayment(\n\t\ttreasury.NewGRC20Payment(\"TOK\", 1, destAddr)))\n\tprintln(\"foreign payment canonical:\", treasury.IsCanonicalPayment(foreign{}))\n}\n\nfunc mustPanic(fn func()) (msg string) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tif s, ok := r.(string); ok {\n\t\t\t\tmsg = s\n\t\t\t\treturn\n\t\t\t}\n\t\t\tmsg = \"rejected\"\n\t\t}\n\t}()\n\tfn()\n\treturn \"NOT REJECTED\"\n}\n\n// Output:\n// duplicate denoms joined: 15ugnot to g1fs92ep7z5vtrp26d767ag3zztsd6wqn9rew4ve\n// unsorted set sorted: 2aaa,1zzz to g1fs92ep7z5vtrp26d767ag3zztsd6wqn9rew4ve\n// zero amount: coins payment amounts must be in [1, MaxInt64], got 0ugnot\n// max int64: 9223372036854775807ugnot to g1fs92ep7z5vtrp26d767ag3zztsd6wqn9rew4ve\n// above max int64: coins payment amounts must be in [1, MaxInt64], got 9223372036854775808ugnot\n// sum above max int64: coins payment amounts must be in [1, MaxInt64], got 9223372036854775808ugnot\n// sum wraps uint256: coins payment amount overflows uint256 for denom ugnot\n// coins payment canonical: true\n// grc20 payment canonical: true\n// foreign payment canonical: false\n"},{"name":"banker_coins_payment_copy_filetest.gno","body":"// PKGPATH: gno.land/r/treasury/main\n\npackage main\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/treasury/v1\"\n)\n\n// coins is this realm's own slice. It is handed to NewCoinsPayment and then\n// rewritten, which is what a realm supplying the coins for a governance\n// payment proposal can do between the vote and the execution.\nvar coins = []treasury.Balance{{Denom: \"ugnot\", Amount: 10}}\n\nfunc main(cur realm) {\n\townerAddr := chain.PackageAddress(\"gno.land/r/treasury/main\")\n\tdestAddr := chain.PackageAddress(\"gno.land/r/dest/main\")\n\n\tbanker_ := banker.NewBanker(banker.BankerTypeRealmSend, cur)\n\tcbanker, err := treasury.NewCoinsBankerWithOwner(ownerAddr, banker_)\n\tif err != nil {\n\t\tpanic(\"failed to create CoinsBanker: \" + err.Error())\n\t}\n\ttesting.IssueCoins(ownerAddr, chain.NewCoins(chain.NewCoin(\"ugnot\", 1000)))\n\n\tpayment := treasury.NewCoinsPayment(coins, destAddr)\n\tprintln(\"payment as created:\", payment.String())\n\n\t// The supplying realm rewrites its own array. The payment must not follow.\n\tcoins[0].Amount = 900\n\tprintln(\"payment after the rewrite:\", payment.String())\n\n\tif err := cbanker.Send(0, cur, payment); err != nil {\n\t\tpanic(\"failed to send payment: \" + err.Error())\n\t}\n\tprintln(\"owner balance after send:\", cbanker.Balances()[0].Amount)\n}\n\n// Output:\n// payment as created: 10ugnot to g1fs92ep7z5vtrp26d767ag3zztsd6wqn9rew4ve\n// payment after the rewrite: 10ugnot to g1fs92ep7z5vtrp26d767ag3zztsd6wqn9rew4ve\n// owner balance after send: 990\n"},{"name":"banker_grc20.gno","body":"package treasury\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n\n\t\"gno.land/p/nt/grc20/v1\"\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    uint256 // 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.FormatUint256(gp.amount, 10)\n\treturn amount + gp.tokenKey + \" to \" + gp.toAddress.String()\n}\n\n// NewGRC20Payment creates a new grc20Payment.\nfunc NewGRC20Payment(tokenKey string, amount uint256, toAddress address) Payment {\n\treturn grc20Payment{\n\t\ttokenKey:  tokenKey,\n\t\tamount:    amount,\n\t\ttoAddress: toAddress,\n\t}\n}\n"},{"name":"banker_grc20_filetest.gno","body":"// PKGPATH: gno.land/r/treasury/main\n\npackage main\n\nimport (\n\t\"chain\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/grc20/v1\"\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/p/nt/treasury/v1\"\n)\n\nconst amount = uint256(1000)\n\nvar nextTokenID seqid.ID\n\nfunc createToken(_ int, rlm realm, name string, toMint address) *grc20.Token {\n\t// Create the token.\n\tsymbol := strings.ToUpper(name)\n\ttoken, ledger := grc20.NewToken(name, symbol, 0, nextTokenID.Next(), rlm)\n\n\t// Mint the requested amount.\n\tledger.Mint(toMint, amount)\n\n\treturn token\n}\n\nfunc main(cur realm) {\n\t// Define addresses for the sender (owner) and destination.\n\townerAddr := chain.PackageAddress(\"gno.land/r/treasury/main\")\n\tdestAddr := chain.PackageAddress(\"gno.land/r/dest/main\")\n\n\t// Try to create a GRC20Banker using a nil lister.\n\tgbanker, err := treasury.NewGRC20BankerWithOwner(ownerAddr, nil)\n\tif err == nil {\n\t\tpanic(\"expected error when creating GRC20Banker with nil lister\")\n\t}\n\n\t// Define a list of token and the associated lister.\n\ttokens := []*grc20.Token{\n\t\tcreateToken(0, cur, \"TestToken0\", ownerAddr),\n\t\tcreateToken(0, cur, \"TestToken1\", ownerAddr),\n\t\tcreateToken(0, cur, \"TestToken2\", ownerAddr),\n\t}\n\n\tgrc20Lister := func() map[string]*grc20.Token {\n\t\ttokensMap := make(map[string]*grc20.Token, len(tokens))\n\n\t\tfor _, token := range tokens {\n\t\t\ttokensMap[token.GetSymbol()] = token\n\t\t}\n\n\t\treturn tokensMap\n\t}\n\n\t// Create a GRC20Banker.\n\tgbanker, err = treasury.NewGRC20BankerWithOwner(ownerAddr, grc20Lister)\n\tif err != nil {\n\t\tpanic(\"failed to create GRC20Banker: \" + err.Error())\n\t}\n\n\tprintln(\"GRC20Banker ID:\", gbanker.ID())\n\tprintln(\"GRC20Banker Address:\", gbanker.Address())\n\n\t// Check if the GRC20Banker address matches the owner address.\n\tif gbanker.Address() != ownerAddr.String() {\n\t\tpanic(\"GRC20Banker address does not match current realm address\")\n\t}\n\n\t// Check the balances of the GRC20Banker.\n\tprintln(\"GRC20Banker Balances count:\", len(gbanker.Balances()))\n\tfor _, balance := range gbanker.Balances() {\n\t\tif balance.Amount != amount {\n\t\t\tpanic(\"GRC20Banker balance does not match expected amount\")\n\t\t}\n\t}\n\n\t// Send a valid payment.\n\ttoken := tokens[len(tokens)-1]\n\tvalidPayment := treasury.NewGRC20Payment(\n\t\ttoken.GetSymbol(),\n\t\t100,\n\t\tdestAddr,\n\t)\n\terr = gbanker.Send(0, cur, validPayment)\n\tprintln(\"Valid payment error:\", err)\n\tif err != nil {\n\t\tpanic(\"failed to send valid payment: \" + err.Error())\n\t}\n\n\tprintln(\"Owner balance:\", token.BalanceOf(ownerAddr))\n\tprintln(\"Dest balance:\", token.BalanceOf(destAddr))\n\n\t// Send an unknown token payment.\n\tunknownPayment := treasury.NewGRC20Payment(\n\t\t\"unknown\",\n\t\t100,\n\t\tdestAddr,\n\t)\n\terr = gbanker.Send(0, cur, unknownPayment)\n\tprintln(\"Unknown token payment error:\", err)\n\tif err == nil {\n\t\tpanic(\"expected error for unknown token, but got none\")\n\t}\n\n\t// Send an unsufficient funds payment.\n\tunsufficientPayment := treasury.NewGRC20Payment(\n\t\ttokens[0].GetSymbol(),\n\t\tamount+1,\n\t\tdestAddr,\n\t)\n\terr = gbanker.Send(0, cur, unsufficientPayment)\n\tprintln(\"Unsufficient funds payment error:\", err)\n\tif err == nil {\n\t\tpanic(\"expected error for insufficient funds, but got none\")\n\t}\n\n\t// Send a payment with an invalid type.\n\tinvalidPaymentType := treasury.NewCoinsPayment(nil, destAddr)\n\terr = gbanker.Send(0, cur, invalidPaymentType)\n\tprintln(\"Invalid payment type error:\", err)\n\tif err == nil {\n\t\tpanic(\"expected error for invalid payment type, but got none\")\n\t}\n}\n\n// Output:\n// GRC20Banker ID: GRC20\n// GRC20Banker Address: g1ynsdz5zaxhn9gnqtr6t40m5k4fueeutq7xy224\n// GRC20Banker Balances count: 3\n// Valid payment error: undefined\n// Owner balance: 900\n// Dest balance: 100\n// Unknown token payment error: GRC20 token not found: unknown\n// Unsufficient funds payment error: insufficient balance\n// Invalid payment type error: invalid payment type\n"},{"name":"banker_grc20_uint256_filetest.gno","body":"// PKGPATH: gno.land/r/treasury/main\n\n// A GRC20 treasury holds and pays amounts beyond the int64 range, and renders\n// them in full.\npackage main\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"math\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/grc20/v1\"\n\t\"gno.land/p/nt/treasury/v1\"\n)\n\nfunc main(cur realm) {\n\townerAddr := chain.PackageAddress(\"gno.land/r/treasury/main\")\n\tdestAddr := chain.PackageAddress(\"gno.land/r/dest/main\")\n\n\ttoken, ledger := grc20.NewToken(\"Big\", \"BIG\", 18, 0, cur)\n\tif err := ledger.Mint(ownerAddr, math.MaxUint256); err != nil {\n\t\tpanic(err)\n\t}\n\tlister := func() map[string]*grc20.Token { return map[string]*grc20.Token{\"BIG\": token} }\n\n\tgbanker, err := treasury.NewGRC20BankerWithOwner(ownerAddr, lister)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tcbanker, err := treasury.NewCoinsBankerWithOwner(ownerAddr, banker.NewBanker(banker.BankerTypeRealmSend, cur))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\ttesting.IssueCoins(ownerAddr, chain.NewCoins(chain.NewCoin(\"ugnot\", math.MaxInt64)))\n\n\tt, err := treasury.New([]treasury.Banker{cbanker, gbanker}, \"\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// 2^200 is far beyond int64.\n\tpayment := treasury.NewGRC20Payment(\"BIG\", 1\u003c\u003c200, destAddr)\n\tprintln(payment.String())\n\tif err := t.Send(0, cur, payment); err != nil {\n\t\tpanic(err)\n\t}\n\tprintln(\"dest:\", token.BalanceOf(destAddr))\n\tprintln(\"owner:\", token.BalanceOf(ownerAddr) == math.MaxUint256-1\u003c\u003c200)\n\n\t// More than the treasury holds is refused.\n\terr = t.Send(0, cur, treasury.NewGRC20Payment(\"BIG\", math.MaxUint256, destAddr))\n\tprintln(\"overdraft:\", err)\n\n\t// Coins balances are the int64 chain amounts, widened.\n\tcoins, _ := t.Balances(\"Coins\")\n\tprintln(\"coins:\", coins[0].Amount == math.MaxInt64)\n\n\tprintln(t.Render(\"GRC20?history_size=0\"))\n}\n\n// Output:\n// 1606938044258990275541962092341162602522202993782792835301376BIG to g1fs92ep7z5vtrp26d767ag3zztsd6wqn9rew4ve\n// dest: 1606938044258990275541962092341162602522202993782792835301376\n// owner: true\n// overdraft: failed to send payment: insufficient balance\n// coins: true\n// ## GRC20 Banker\n// ### Address\n// g1ynsdz5zaxhn9gnqtr6t40m5k4fueeutq7xy224\n//\n// ### Balances\n// | Denom | Amount |\n// | --- | --- |\n// | BIG | 115792089237316193816632940749697632311307892324477961517254590225120294338559 |\n"},{"name":"doc.gno","body":"// v1 - 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.\n//\n// v1 reports every Balance and takes every payment amount as a uint256, and\n// its GRC20Banker handles gno.land/p/nt/grc20/v1 tokens. Chain coins stay\n// int64: CoinsBanker widens them for reporting, and NewCoinsPayment\n// range-checks each amount back down to int64, refusing anything outside\n// [1, MaxInt64].\npackage treasury\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/treasury/v1\"\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/v0\"\n\t\"gno.land/p/moul/mdtable/v0\"\n\t\"gno.land/p/nt/bptree/pager/v0\"\n\t\"gno.land/p/nt/markdown/sanitize/v0\"\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: \" + md.EscapeText(bankerID))\n\t}\n\tbanker := br.(*bankerRecord).banker\n\n\t// Render banker title.\n\tout := md.H2(md.EscapeText(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{sanitize.TableCell(balance.Denom), strconv.FormatUint256(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(sanitize.InlineText(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: \" + md.EscapeText(bankerID))\n\t}\n\thistory := br.(*bankerRecord).history\n\n\t// Render banker history title.\n\tout := md.H2(md.EscapeText(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: \" + md.EscapeText(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(sanitize.InlineText(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":"render_escape_filetest.gno","body":"// PKGPATH: gno.land/r/treasury/main\n\n// Token keys (lister map keys) and payment strings are rendered by the\n// treasury pages; markdown in them must stay inert.\npackage main\n\nimport (\n\t\"chain\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/grc20/v1\"\n\t\"gno.land/p/nt/treasury/v1\"\n)\n\nconst evilKey = \"[Claim](https://evil.example)\"\n\nfunc main(cur realm) {\n\towner := chain.PackageAddress(\"gno.land/r/treasury/main\")\n\tdest := chain.PackageAddress(\"gno.land/r/dest/main\")\n\n\ttoken, ledger := grc20.NewToken(\"Tok\", \"TOK\", 0, 0, cur)\n\tledger.Mint(owner, 10)\n\tlister := func() map[string]*grc20.Token { return map[string]*grc20.Token{evilKey: token} }\n\tgb, err := treasury.NewGRC20BankerWithOwner(owner, lister)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tt, err := treasury.New([]treasury.Banker{gb}, \"gno.land/r/treasury/main\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif err := t.Send(0, cur, treasury.NewGRC20Payment(evilKey, 1, dest)); err != nil {\n\t\tpanic(err)\n\t}\n\n\tfor _, path := range []string{\"GRC20\", \"GRC20/history\"} {\n\t\tout := t.Render(path)\n\t\tprintln(path, \"live link:\", strings.Contains(out, evilKey))\n\t\tprintln(path, \"escaped:\", strings.Contains(out, `\\[Claim\\]`))\n\t}\n}\n\n// Output:\n// GRC20 live link: false\n// GRC20 escaped: true\n// GRC20/history live link: false\n// GRC20/history escaped: true\n"},{"name":"treasury.gno","body":"package treasury\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/bptree/pager/v0\"\n\t\"gno.land/p/nt/bptree/v0\"\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":"treasury_filetest.gno","body":"// PKGPATH: gno.land/r/treasury/main\n\npackage main\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/grc20/v1\"\n\t\"gno.land/p/nt/treasury/v1\"\n)\n\nfunc checkBalanceAndHistory(t *treasury.Treasury, bankerIDs []string) {\n\tfor _, bankerID := range bankerIDs {\n\t\tbalances, err := t.Balances(bankerID)\n\t\tif err != nil {\n\t\t\tpanic(\"failed to get banker balances: \" + err.Error())\n\t\t}\n\t\tprintln(\"Banker\", bankerID, \"Balance:\", balances[0].Amount)\n\n\t\thistory, err := t.History(bankerID, 1, 10)\n\t\tif err != nil {\n\t\t\tpanic(\"failed to get banker history: \" + err.Error())\n\t\t}\n\t\tprintln(\"Banker\", bankerID, \"History count:\", len(history))\n\t}\n}\n\nfunc main(cur realm) {\n\t// Define addresses for the sender (owner) and destination.\n\townerAddr := chain.PackageAddress(\"gno.land/r/treasury/main\")\n\tdestAddr := chain.PackageAddress(\"gno.land/r/dest/main\")\n\n\t// Try to create a Treasury instance with no bankers.\n\t_, err := treasury.New(nil, \"\")\n\tif err != treasury.ErrNoBankerProvided {\n\t\tpanic(\"expected error when creating Treasury with no bankers\")\n\t}\n\n\t// Define a token and the associated lister.\n\tconst amount = 1000\n\ttoken, ledger := grc20.NewToken(\"TestToken\", \"TEST\", 0, 0, cur)\n\tledger.Mint(ownerAddr, amount)\n\n\tgrc20Lister := func() map[string]*grc20.Token {\n\t\treturn map[string]*grc20.Token{\n\t\t\t\"TEST\": token,\n\t\t}\n\t}\n\n\t// Try to create a Treasury instance with a duplicate banker.\n\tvar (\n\t\tbanker_           = banker.NewBanker(banker.BankerTypeRealmSend, cur)\n\t\tcoinsBanker, _    = treasury.NewCoinsBankerWithOwner(ownerAddr, banker_)\n\t\tgrc20Banker, _    = treasury.NewGRC20BankerWithOwner(ownerAddr, grc20Lister)\n\t\tgrc20BankerDup, _ = treasury.NewGRC20BankerWithOwner(ownerAddr, grc20Lister)\n\t)\n\n\t_, err = treasury.New([]treasury.Banker{coinsBanker, grc20Banker, grc20BankerDup}, \"\")\n\tif !strings.Contains(err.Error(), treasury.ErrDuplicateBanker.Error()) {\n\t\tpanic(\"expected error when creating Treasury with duplicate banker\")\n\t}\n\n\t// Create a Treasury instance with valid bankers.\n\tbankers := []treasury.Banker{coinsBanker, grc20Banker}\n\tt, err := treasury.New(bankers, \"\")\n\tif err != nil {\n\t\tpanic(\"failed to create Treasury: \" + err.Error())\n\t}\n\n\t// Test if the Treasury instance has the expected bankers.\n\tprintln(\"Treasury banker IDs:\", t.ListBankerIDs())\n\n\tconst unknownBankerID = \"unknown-banker-id\"\n\n\tif t.HasBanker(unknownBankerID) {\n\t\tpanic(\"expected banker not to be found\")\n\t}\n\n\t// Check if the addresses of the bankers matches the owner address.\n\tfor _, banker_ := range bankers {\n\t\taddr, err := t.Address(banker_.ID())\n\t\tif err != nil {\n\t\t\tpanic(\"failed to get banker address: \" + err.Error())\n\t\t}\n\t\tprintln(\"Banker\", banker_.ID(), \"Address:\", addr)\n\t}\n\n\t// Check if the balances and history of the bankers match the expected values.\n\ttesting.IssueCoins(ownerAddr, chain.NewCoins(chain.NewCoin(\"ugnot\", amount)))\n\tbankerIDs := []string{coinsBanker.ID(), grc20Banker.ID()}\n\tcheckBalanceAndHistory(t, bankerIDs)\n\n\t// Send 3 valid payments using the CoinsBanker.\n\tvalidCoinsPayment := treasury.NewCoinsPayment(\n\t\t[]treasury.Balance{{Denom: \"ugnot\", Amount: 100}},\n\t\tdestAddr,\n\t)\n\tfor i := 0; i \u003c 3; i++ {\n\t\terr = t.Send(0, cur, validCoinsPayment)\n\t\tif err != nil {\n\t\t\tpanic(\"failed to send valid Coins payment: \" + err.Error())\n\t\t}\n\t}\n\n\t// Send 3 valid payments using the GRC20Banker.\n\tvalidGRC20Payment := treasury.NewGRC20Payment(\n\t\ttoken.GetSymbol(),\n\t\t100,\n\t\tdestAddr,\n\t)\n\tfor i := 0; i \u003c 3; i++ {\n\t\terr = t.Send(0, cur, validGRC20Payment)\n\t\tif err != nil {\n\t\t\tpanic(\"failed to send valid GRC20 payment: \" + err.Error())\n\t\t}\n\t}\n\n\t// Check if the balances and history of the bankers match the expected values.\n\tcheckBalanceAndHistory(t, bankerIDs)\n}\n\n// Output:\n// Treasury banker IDs: slice[(\"Coins\" string),(\"GRC20\" string)]\n// Banker Coins Address: g1ynsdz5zaxhn9gnqtr6t40m5k4fueeutq7xy224\n// Banker GRC20 Address: g1ynsdz5zaxhn9gnqtr6t40m5k4fueeutq7xy224\n// Banker Coins Balance: 1000\n// Banker Coins History count: 0\n// Banker GRC20 Balance: 1000\n// Banker GRC20 History count: 0\n// Banker Coins Balance: 700\n// Banker Coins History count: 3\n// Banker GRC20 Balance: 700\n// Banker GRC20 History count: 3\n"},{"name":"types.gno","body":"package treasury\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/bptree/list/v0\"\n\t\"gno.land/p/nt/bptree/v0\"\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// IsCanonicalPayment reports whether p is one of treasury's canonical concrete\n// Payment impls. Use this at any public entry point that accepts a Payment\n// from an external caller and shows it to a human before it is processed.\n//\n// The immutability the constructors provide is a property of these two types,\n// not of the interface: a foreign impl's String() says whatever its realm\n// likes, and no Banker will process it, because Banker.Send type-asserts and\n// so fails only at execution — after a vote has been spent on it.\n//\n// Only the value types are listed, deliberately. Banker.Send asserting\n// p.(coinsPayment) is what makes this check meaningful, so accepting\n// *coinsPayment would pass a payment that still fails at execution.\n//\n// To add a new canonical type: extend the switch below AND add a regression\n// test (under filetests/ in this package) that a foreign impl is rejected.\n//\n// Mirrors IsCanonicalBanker above.\nfunc IsCanonicalPayment(p Payment) bool {\n\tswitch p.(type) {\n\tcase coinsPayment, grc20Payment:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// Balance represents the balance of an asset held by a Banker. Amount is a\n// uint256 for every banker: a CoinsBanker widens the int64 chain coin\n// amounts, a GRC20Banker reports grc20 v1 amounts as they are.\ntype Balance struct {\n\tDenom  string  // The denomination of the asset\n\tAmount uint256 // 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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"0fO5OwSnXQZf9VAg/o6Dvpir8uBUrq8JHMyZgtz/oURylMWZbxy6a/NeJd3SKNvXUGKOGaA84VClGx4deNxXqg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"json","path":"gno.land/p/onbloc/json/v0","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":"buffer_test.gno","body":"package json\n\nimport (\n\t\"testing\"\n)\n\nfunc TestBufferCurrent(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tbuffer   *buffer\n\t\texpected byte\n\t\twantErr  bool\n\t}{\n\t\t{\n\t\t\tname: \"Valid current byte\",\n\t\t\tbuffer: \u0026buffer{\n\t\t\t\tdata:   []byte(\"test\"),\n\t\t\t\tlength: 4,\n\t\t\t\tindex:  1,\n\t\t\t},\n\t\t\texpected: 'e',\n\t\t\twantErr:  false,\n\t\t},\n\t\t{\n\t\t\tname: \"EOF\",\n\t\t\tbuffer: \u0026buffer{\n\t\t\t\tdata:   []byte(\"test\"),\n\t\t\t\tlength: 4,\n\t\t\t\tindex:  4,\n\t\t\t},\n\t\t\texpected: 0,\n\t\t\twantErr:  true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.buffer.current()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"buffer.current() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"buffer.current() = %v, want %v\", got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBufferStep(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tbuffer  *buffer\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"Valid step\",\n\t\t\tbuffer:  \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"EOF error\",\n\t\t\tbuffer:  \u0026buffer{data: []byte(\"test\"), length: 4, index: 3},\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.buffer.step()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"buffer.step() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBufferNext(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tbuffer  *buffer\n\t\twant    byte\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"Valid next byte\",\n\t\t\tbuffer:  \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\twant:    'e',\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"EOF error\",\n\t\t\tbuffer:  \u0026buffer{data: []byte(\"test\"), length: 4, index: 3},\n\t\t\twant:    0,\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.buffer.next()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"buffer.next() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"buffer.next() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBufferSlice(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tbuffer  *buffer\n\t\tpos     int\n\t\twant    []byte\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"Valid slice -- 0 characters\",\n\t\t\tbuffer:  \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\tpos:     0,\n\t\t\twant:    nil,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"Valid slice -- 1 character\",\n\t\t\tbuffer:  \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\tpos:     1,\n\t\t\twant:    []byte(\"t\"),\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"Valid slice -- 2 characters\",\n\t\t\tbuffer:  \u0026buffer{data: []byte(\"test\"), length: 4, index: 1},\n\t\t\tpos:     2,\n\t\t\twant:    []byte(\"es\"),\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"Valid slice -- 3 characters\",\n\t\t\tbuffer:  \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\tpos:     3,\n\t\t\twant:    []byte(\"tes\"),\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"Valid slice -- 4 characters\",\n\t\t\tbuffer:  \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\tpos:     4,\n\t\t\twant:    []byte(\"test\"),\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"EOF error\",\n\t\t\tbuffer:  \u0026buffer{data: []byte(\"test\"), length: 4, index: 3},\n\t\t\tpos:     2,\n\t\t\twant:    nil,\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.buffer.slice(tt.pos)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"buffer.slice() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif string(got) != string(tt.want) {\n\t\t\t\tt.Errorf(\"buffer.slice() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBufferMove(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tbuffer  *buffer\n\t\tpos     int\n\t\twantErr bool\n\t\twantIdx int\n\t}{\n\t\t{\n\t\t\tname:    \"Valid move\",\n\t\t\tbuffer:  \u0026buffer{data: []byte(\"test\"), length: 4, index: 1},\n\t\t\tpos:     2,\n\t\t\twantErr: false,\n\t\t\twantIdx: 3,\n\t\t},\n\t\t{\n\t\t\tname:    \"Move beyond length\",\n\t\t\tbuffer:  \u0026buffer{data: []byte(\"test\"), length: 4, index: 1},\n\t\t\tpos:     4,\n\t\t\twantErr: true,\n\t\t\twantIdx: 1,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.buffer.move(tt.pos)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"buffer.move() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif tt.buffer.index != tt.wantIdx {\n\t\t\t\tt.Errorf(\"buffer.move() index = %v, want %v\", tt.buffer.index, tt.wantIdx)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBufferSkip(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tbuffer  *buffer\n\t\tb       byte\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"Skip byte\",\n\t\t\tbuffer:  \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\tb:       'e',\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"Skip to EOF\",\n\t\t\tbuffer:  \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\tb:       'x',\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.buffer.skip(tt.b)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"buffer.skip() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSkipToNextSignificantToken(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []byte\n\t\texpected int\n\t}{\n\t\t{\"No significant chars\", []byte(\"abc\"), 3},\n\t\t{\"One significant char at start\", []byte(\".abc\"), 0},\n\t\t{\"Significant char in middle\", []byte(\"ab.c\"), 2},\n\t\t{\"Multiple significant chars\", []byte(\"a$.c\"), 1},\n\t\t{\"Significant char at end\", []byte(\"abc$\"), 3},\n\t\t{\"Only significant chars\", []byte(\"$.\"), 0},\n\t\t{\"Empty string\", []byte(\"\"), 0},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tb := newBuffer(tt.input)\n\t\t\tb.skipToNextSignificantToken()\n\t\t\tif b.index != tt.expected {\n\t\t\t\tt.Errorf(\"after skipToNextSignificantToken(), got index = %v, want %v\", b.index, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc mockBuffer(s string) *buffer {\n\treturn newBuffer([]byte(s))\n}\n\nfunc TestSkipAndReturnIndex(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected int\n\t}{\n\t\t{\"StartOfString\", \"\", 0},\n\t\t{\"MiddleOfString\", \"abcdef\", 1},\n\t\t{\"EndOfString\", \"abc\", 1},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tbuf := mockBuffer(tt.input)\n\t\t\tgot, err := buf.skipAndReturnIndex()\n\t\t\tif err != nil \u0026\u0026 tt.input != \"\" { // Expect no error unless input is empty\n\t\t\t\tt.Errorf(\"skipAndReturnIndex() error = %v\", err)\n\t\t\t}\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"skipAndReturnIndex() = %v, want %v\", got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSkipUntil(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\ttokens   map[byte]bool\n\t\texpected int\n\t}{\n\t\t{\"SkipToToken\", \"abcdefg\", map[byte]bool{'c': true}, 2},\n\t\t{\"SkipToEnd\", \"abcdefg\", map[byte]bool{'h': true}, 7},\n\t\t{\"SkipNone\", \"abcdefg\", map[byte]bool{'a': true}, 0},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tbuf := mockBuffer(tt.input)\n\t\t\tgot, err := buf.skipUntil(tt.tokens)\n\t\t\tif err != nil \u0026\u0026 got != len(tt.input) { // Expect error only if reached end without finding token\n\t\t\t\tt.Errorf(\"skipUntil() error = %v\", err)\n\t\t\t}\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"skipUntil() = %v, want %v\", got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSliceFromIndices(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\tstart    int\n\t\tend      int\n\t\texpected string\n\t}{\n\t\t{\"FullString\", \"abcdefg\", 0, 7, \"abcdefg\"},\n\t\t{\"Substring\", \"abcdefg\", 2, 5, \"cde\"},\n\t\t{\"OutOfBounds\", \"abcdefg\", 5, 10, \"fg\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tbuf := mockBuffer(tt.input)\n\t\t\tgot := buf.sliceFromIndices(tt.start, tt.end)\n\t\t\tif string(got) != tt.expected {\n\t\t\t\tt.Errorf(\"sliceFromIndices() = %v, want %v\", string(got), tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBufferToken(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\tpath  string\n\t\tindex int\n\t\tisErr bool\n\t}{\n\t\t{\n\t\t\tname:  \"Simple valid path\",\n\t\t\tpath:  \"@.length\",\n\t\t\tindex: 8,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname:  \"Path with array expr\",\n\t\t\tpath:  \"@['foo'].0.bar\",\n\t\t\tindex: 14,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname:  \"Path with array expr and simple fomula\",\n\t\t\tpath:  \"@['foo'].[(@.length - 1)].*\",\n\t\t\tindex: 27,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname:  \"Path with filter expr\",\n\t\t\tpath:  \"@['foo'].[?(@.bar == 1 \u0026 @.baz \u003c @.length)].*\",\n\t\t\tindex: 45,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname:  \"addition of foo and bar\",\n\t\t\tpath:  \"@.foo+@.bar\",\n\t\t\tindex: 11,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname:  \"logical AND of foo and bar\",\n\t\t\tpath:  \"@.foo \u0026\u0026 @.bar\",\n\t\t\tindex: 14,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname:  \"logical OR of foo and bar\",\n\t\t\tpath:  \"@.foo || @.bar\",\n\t\t\tindex: 14,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname:  \"accessing third element of foo\",\n\t\t\tpath:  \"@.foo,3\",\n\t\t\tindex: 7,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname:  \"accessing last element of array\",\n\t\t\tpath:  \"@.length-1\",\n\t\t\tindex: 10,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname:  \"number 1\",\n\t\t\tpath:  \"1\",\n\t\t\tindex: 1,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname:  \"float\",\n\t\t\tpath:  \"3.1e4\",\n\t\t\tindex: 5,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname:  \"float with minus\",\n\t\t\tpath:  \"3.1e-4\",\n\t\t\tindex: 6,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname:  \"float with plus\",\n\t\t\tpath:  \"3.1e+4\",\n\t\t\tindex: 6,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname:  \"negative number\",\n\t\t\tpath:  \"-12345\",\n\t\t\tindex: 6,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname:  \"negative float\",\n\t\t\tpath:  \"-3.1e4\",\n\t\t\tindex: 6,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname:  \"negative float with minus\",\n\t\t\tpath:  \"-3.1e-4\",\n\t\t\tindex: 7,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname:  \"negative float with plus\",\n\t\t\tpath:  \"-3.1e+4\",\n\t\t\tindex: 7,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname:  \"string number\",\n\t\t\tpath:  \"'12345'\",\n\t\t\tindex: 7,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname:  \"string with backslash\",\n\t\t\tpath:  \"'foo \\\\'bar '\",\n\t\t\tindex: 12,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname:  \"string with inner double quotes\",\n\t\t\tpath:  \"'foo \\\"bar \\\"'\",\n\t\t\tindex: 12,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname:  \"parenthesis 1\",\n\t\t\tpath:  \"(@abc)\",\n\t\t\tindex: 6,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname:  \"parenthesis 2\",\n\t\t\tpath:  \"[()]\",\n\t\t\tindex: 4,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname:  \"parenthesis mismatch\",\n\t\t\tpath:  \"[(])\",\n\t\t\tindex: 2,\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname:  \"parenthesis mismatch 2\",\n\t\t\tpath:  \"(\",\n\t\t\tindex: 1,\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname:  \"parenthesis mismatch 3\",\n\t\t\tpath:  \"())]\",\n\t\t\tindex: 2,\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname:  \"bracket mismatch\",\n\t\t\tpath:  \"[()\",\n\t\t\tindex: 3,\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname:  \"bracket mismatch 2\",\n\t\t\tpath:  \"()]\",\n\t\t\tindex: 2,\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname:  \"path does not close bracket\",\n\t\t\tpath:  \"@.foo[)\",\n\t\t\tindex: 6,\n\t\t\tisErr: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tbuf := newBuffer([]byte(tt.path))\n\n\t\t\terr := buf.pathToken()\n\t\t\tif tt.isErr {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Errorf(\"Expected an error for path `%s`, but got none\", tt.path)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif err == nil \u0026\u0026 tt.isErr {\n\t\t\t\tt.Errorf(\"Expected an error for path `%s`, but got none\", tt.path)\n\t\t\t}\n\n\t\t\tif buf.index != tt.index {\n\t\t\t\tt.Errorf(\"Expected final index %d, got %d (token: `%s`) for path `%s`\", tt.index, buf.index, string(buf.data[buf.index]), tt.path)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBufferFirst(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tdata     []byte\n\t\texpected byte\n\t}{\n\t\t{\n\t\t\tname:     \"Valid first byte\",\n\t\t\tdata:     []byte(\"test\"),\n\t\t\texpected: 't',\n\t\t},\n\t\t{\n\t\t\tname:     \"Empty buffer\",\n\t\t\tdata:     []byte(\"\"),\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname:     \"Whitespace buffer\",\n\t\t\tdata:     []byte(\"   \"),\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname:     \"whitespace in middle\",\n\t\t\tdata:     []byte(\"hello world\"),\n\t\t\texpected: 'h',\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tb := newBuffer(tt.data)\n\n\t\t\tgot, err := b.first()\n\t\t\tif err != nil \u0026\u0026 tt.expected != 0 {\n\t\t\t\tt.Errorf(\"Unexpected error: %v\", err)\n\t\t\t}\n\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"Expected first byte to be %q, got %q\", tt.expected, got)\n\t\t\t}\n\t\t})\n\t}\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":"builder_test.gno","body":"package json\n\nimport (\n\t\"testing\"\n)\n\nfunc TestNodeBuilder(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tbuild    func() *Node\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"plain object\",\n\t\t\tbuild: func() *Node {\n\t\t\t\treturn Builder().\n\t\t\t\t\tWriteString(\"name\", \"Alice\").\n\t\t\t\t\tWriteNumber(\"age\", 30).\n\t\t\t\t\tWriteBool(\"is_student\", false).\n\t\t\t\t\tNode()\n\t\t\t},\n\t\t\texpected: `{\"name\":\"Alice\",\"age\":30,\"is_student\":false}`,\n\t\t},\n\t\t{\n\t\t\tname: \"nested object\",\n\t\t\tbuild: func() *Node {\n\t\t\t\treturn Builder().\n\t\t\t\t\tWriteString(\"name\", \"Alice\").\n\t\t\t\t\tWriteObject(\"address\", func(b *NodeBuilder) {\n\t\t\t\t\t\tb.WriteString(\"city\", \"New York\").\n\t\t\t\t\t\t\tWriteNumber(\"zipcode\", 10001)\n\t\t\t\t\t}).\n\t\t\t\t\tNode()\n\t\t\t},\n\t\t\texpected: `{\"name\":\"Alice\",\"address\":{\"city\":\"New York\",\"zipcode\":10001}}`,\n\t\t},\n\t\t{\n\t\t\tname: \"null node\",\n\t\t\tbuild: func() *Node {\n\t\t\t\treturn Builder().WriteNull(\"foo\").Node()\n\t\t\t},\n\t\t\texpected: `{\"foo\":null}`,\n\t\t},\n\t\t{\n\t\t\tname: \"array node\",\n\t\t\tbuild: func() *Node {\n\t\t\t\treturn Builder().\n\t\t\t\t\tWriteArray(\"items\", func(ab *ArrayBuilder) {\n\t\t\t\t\t\tab.WriteString(\"item1\").\n\t\t\t\t\t\t\tWriteString(\"item2\").\n\t\t\t\t\t\t\tWriteString(\"item3\")\n\t\t\t\t\t}).\n\t\t\t\t\tNode()\n\t\t\t},\n\t\t\texpected: `{\"items\":[\"item1\",\"item2\",\"item3\"]}`,\n\t\t},\n\t\t{\n\t\t\tname: \"array with objects\",\n\t\t\tbuild: func() *Node {\n\t\t\t\treturn Builder().\n\t\t\t\t\tWriteArray(\"users\", func(ab *ArrayBuilder) {\n\t\t\t\t\t\tab.WriteObject(func(b *NodeBuilder) {\n\t\t\t\t\t\t\tb.WriteString(\"name\", \"Bob\").\n\t\t\t\t\t\t\t\tWriteNumber(\"age\", 25)\n\t\t\t\t\t\t}).\n\t\t\t\t\t\t\tWriteObject(func(b *NodeBuilder) {\n\t\t\t\t\t\t\t\tb.WriteString(\"name\", \"Carol\").\n\t\t\t\t\t\t\t\t\tWriteNumber(\"age\", 27)\n\t\t\t\t\t\t\t})\n\t\t\t\t\t}).\n\t\t\t\t\tNode()\n\t\t\t},\n\t\t\texpected: `{\"users\":[{\"name\":\"Bob\",\"age\":25},{\"name\":\"Carol\",\"age\":27}]}`,\n\t\t},\n\t\t{\n\t\t\tname: \"array with various types\",\n\t\t\tbuild: func() *Node {\n\t\t\t\treturn Builder().\n\t\t\t\t\tWriteArray(\"values\", func(ab *ArrayBuilder) {\n\t\t\t\t\t\tab.WriteString(\"item1\").\n\t\t\t\t\t\t\tWriteNumber(123).\n\t\t\t\t\t\t\tWriteBool(true).\n\t\t\t\t\t\t\tWriteNull()\n\t\t\t\t\t}).\n\t\t\t\t\tNode()\n\t\t\t},\n\t\t\texpected: `{\"values\":[\"item1\",123,true,null]}`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tnode := tt.build()\n\t\t\tvalue, err := Marshal(node)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"unexpected error: %s\", err)\n\t\t\t}\n\t\t\tif string(value) != tt.expected {\n\t\t\t\tt.Errorf(\"expected %s, got %s\", tt.expected, string(value))\n\t\t\t}\n\t\t})\n\t}\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":"decode_test.gno","body":"package json\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n)\n\ntype testNode struct {\n\tname  string\n\tinput []byte\n\tvalue []byte\n\t_type ValueType\n}\n\nfunc simpleValid(test *testNode, t *testing.T) {\n\troot, err := Unmarshal(test.input)\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(%s): %s\", test.input, err.Error())\n\t} else if root == nil {\n\t\tt.Errorf(\"Error on Unmarshal(%s): root is nil\", test.name)\n\t} else if root.nodeType != test._type {\n\t\tt.Errorf(\"Error on Unmarshal(%s): wrong type\", test.name)\n\t} else if !bytes.Equal(root.source(), test.value) {\n\t\tt.Errorf(\"Error on Unmarshal(%s): %s != %s\", test.name, root.source(), test.value)\n\t}\n}\n\nfunc simpleInvalid(test *testNode, t *testing.T) {\n\troot, err := Unmarshal(test.input)\n\tif err == nil {\n\t\tt.Errorf(\"Error on Unmarshal(%s): error expected, got '%s'\", test.name, root.source())\n\t} else if root != nil {\n\t\tt.Errorf(\"Error on Unmarshal(%s): root is not nil\", test.name)\n\t}\n}\n\nfunc simpleCorrupted(name string) *testNode {\n\treturn \u0026testNode{name: name, input: []byte(name)}\n}\n\nfunc TestUnmarshal_StringSimpleSuccess(t *testing.T) {\n\ttests := []*testNode{\n\t\t{name: \"blank\", input: []byte(\"\\\"\\\"\"), _type: String, value: []byte(\"\\\"\\\"\")},\n\t\t{name: \"char\", input: []byte(\"\\\"c\\\"\"), _type: String, value: []byte(\"\\\"c\\\"\")},\n\t\t{name: \"word\", input: []byte(\"\\\"cat\\\"\"), _type: String, value: []byte(\"\\\"cat\\\"\")},\n\t\t{name: \"spaces\", input: []byte(\"  \\\"good cat or dog\\\"\\r\\n \"), _type: String, value: []byte(\"\\\"good cat or dog\\\"\")},\n\t\t{name: \"backslash\", input: []byte(\"\\\"good \\\\\\\"cat\\\\\\\"\\\"\"), _type: String, value: []byte(\"\\\"good \\\\\\\"cat\\\\\\\"\\\"\")},\n\t\t{name: \"backslash 2\", input: []byte(\"\\\"good \\\\\\\\\\\\\\\"cat\\\\\\\"\\\"\"), _type: String, value: []byte(\"\\\"good \\\\\\\\\\\\\\\"cat\\\\\\\"\\\"\")},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleValid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_NumericSimpleSuccess(t *testing.T) {\n\ttests := []*testNode{\n\t\t{name: \"1\", input: []byte(\"1\"), _type: Number, value: []byte(\"1\")},\n\t\t{name: \"-1\", input: []byte(\"-1\"), _type: Number, value: []byte(\"-1\")},\n\n\t\t{name: \"1234567890\", input: []byte(\"1234567890\"), _type: Number, value: []byte(\"1234567890\")},\n\t\t{name: \"-123\", input: []byte(\"-123\"), _type: Number, value: []byte(\"-123\")},\n\n\t\t{name: \"123.456\", input: []byte(\"123.456\"), _type: Number, value: []byte(\"123.456\")},\n\t\t{name: \"-123.456\", input: []byte(\"-123.456\"), _type: Number, value: []byte(\"-123.456\")},\n\n\t\t{name: \"1e3\", input: []byte(\"1e3\"), _type: Number, value: []byte(\"1e3\")},\n\t\t{name: \"1e+3\", input: []byte(\"1e+3\"), _type: Number, value: []byte(\"1e+3\")},\n\t\t{name: \"1e-3\", input: []byte(\"1e-3\"), _type: Number, value: []byte(\"1e-3\")},\n\t\t{name: \"-1e3\", input: []byte(\"-1e3\"), _type: Number, value: []byte(\"-1e3\")},\n\t\t{name: \"-1e-3\", input: []byte(\"-1e-3\"), _type: Number, value: []byte(\"-1e-3\")},\n\n\t\t{name: \"1.123e3456\", input: []byte(\"1.123e3456\"), _type: Number, value: []byte(\"1.123e3456\")},\n\t\t{name: \"1.123e-3456\", input: []byte(\"1.123e-3456\"), _type: Number, value: []byte(\"1.123e-3456\")},\n\t\t{name: \"-1.123e3456\", input: []byte(\"-1.123e3456\"), _type: Number, value: []byte(\"-1.123e3456\")},\n\t\t{name: \"-1.123e-3456\", input: []byte(\"-1.123e-3456\"), _type: Number, value: []byte(\"-1.123e-3456\")},\n\n\t\t{name: \"1E3\", input: []byte(\"1E3\"), _type: Number, value: []byte(\"1E3\")},\n\t\t{name: \"1E-3\", input: []byte(\"1E-3\"), _type: Number, value: []byte(\"1E-3\")},\n\t\t{name: \"-1E3\", input: []byte(\"-1E3\"), _type: Number, value: []byte(\"-1E3\")},\n\t\t{name: \"-1E-3\", input: []byte(\"-1E-3\"), _type: Number, value: []byte(\"-1E-3\")},\n\n\t\t{name: \"1.123E3456\", input: []byte(\"1.123E3456\"), _type: Number, value: []byte(\"1.123E3456\")},\n\t\t{name: \"1.123E-3456\", input: []byte(\"1.123E-3456\"), _type: Number, value: []byte(\"1.123E-3456\")},\n\t\t{name: \"-1.123E3456\", input: []byte(\"-1.123E3456\"), _type: Number, value: []byte(\"-1.123E3456\")},\n\t\t{name: \"-1.123E-3456\", input: []byte(\"-1.123E-3456\"), _type: Number, value: []byte(\"-1.123E-3456\")},\n\n\t\t{name: \"-1.123E-3456 with spaces\", input: []byte(\" \\r -1.123E-3456 \\t\\n\"), _type: Number, value: []byte(\"-1.123E-3456\")},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\troot, err := Unmarshal(test.input)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Error on Unmarshal(%s): %s\", test.name, err.Error())\n\t\t\t} else if root == nil {\n\t\t\t\tt.Errorf(\"Error on Unmarshal(%s): root is nil\", test.name)\n\t\t\t} else if root.nodeType != test._type {\n\t\t\t\tt.Errorf(\"Error on Unmarshal(%s): wrong type\", test.name)\n\t\t\t} else if !bytes.Equal(root.source(), test.value) {\n\t\t\t\tt.Errorf(\"Error on Unmarshal(%s): %s != %s\", test.name, root.source(), test.value)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_StringSimpleCorrupted(t *testing.T) {\n\ttests := []*testNode{\n\t\t{name: \"white NL\", input: []byte(\"\\\"foo\\nbar\\\"\")},\n\t\t{name: \"white R\", input: []byte(\"\\\"foo\\rbar\\\"\")},\n\t\t{name: \"white Tab\", input: []byte(\"\\\"foo\\tbar\\\"\")},\n\t\t{name: \"wrong quotes\", input: []byte(\"'cat'\")},\n\t\t{name: \"double string\", input: []byte(\"\\\"Hello\\\" \\\"World\\\"\")},\n\t\t{name: \"quotes in quotes\", input: []byte(\"\\\"good \\\"cat\\\"\\\"\")},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleInvalid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_ObjectSimpleSuccess(t *testing.T) {\n\ttests := []*testNode{\n\t\t{name: \"{}\", input: []byte(\"{}\"), _type: Object, value: []byte(\"{}\")},\n\t\t{name: `{ \\r\\n }`, input: []byte(\"{ \\r\\n }\"), _type: Object, value: []byte(\"{ \\r\\n }\")},\n\t\t{name: `{\"key\":1}`, input: []byte(`{\"key\":1}`), _type: Object, value: []byte(`{\"key\":1}`)},\n\t\t{name: `{\"key\":true}`, input: []byte(`{\"key\":true}`), _type: Object, value: []byte(`{\"key\":true}`)},\n\t\t{name: `{\"key\":\"value\"}`, input: []byte(`{\"key\":\"value\"}`), _type: Object, value: []byte(`{\"key\":\"value\"}`)},\n\t\t{name: `{\"foo\":\"bar\",\"baz\":\"foo\"}`, input: []byte(`{\"foo\":\"bar\", \"baz\":\"foo\"}`), _type: Object, value: []byte(`{\"foo\":\"bar\", \"baz\":\"foo\"}`)},\n\t\t{name: \"spaces\", input: []byte(`  {  \"foo\"  :  \"bar\"  , \"baz\"   :   \"foo\"   }    `), _type: Object, value: []byte(`{  \"foo\"  :  \"bar\"  , \"baz\"   :   \"foo\"   }`)},\n\t\t{name: \"nested\", input: []byte(`{\"foo\":{\"bar\":{\"baz\":{}}}}`), _type: Object, value: []byte(`{\"foo\":{\"bar\":{\"baz\":{}}}}`)},\n\t\t{name: \"array\", input: []byte(`{\"array\":[{},{},{\"foo\":[{\"bar\":[\"baz\"]}]}]}`), _type: Object, value: []byte(`{\"array\":[{},{},{\"foo\":[{\"bar\":[\"baz\"]}]}]}`)},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleValid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_ObjectSimpleCorrupted(t *testing.T) {\n\ttests := []*testNode{\n\t\tsimpleCorrupted(\"{{{\\\"key\\\": \\\"foo\\\"{{{{\"),\n\t\tsimpleCorrupted(\"}\"),\n\t\tsimpleCorrupted(\"{ }}}}}}}\"),\n\t\tsimpleCorrupted(\" }\"),\n\t\tsimpleCorrupted(\"{,}\"),\n\t\tsimpleCorrupted(\"{:}\"),\n\t\tsimpleCorrupted(\"{100000}\"),\n\t\tsimpleCorrupted(\"{1:1}\"),\n\t\tsimpleCorrupted(\"{'1:2,3:4'}\"),\n\t\tsimpleCorrupted(`{\"d\"}`),\n\t\tsimpleCorrupted(`{\"foo\"}`),\n\t\tsimpleCorrupted(`{\"foo\":}`),\n\t\tsimpleCorrupted(`{:\"foo\"}`),\n\t\tsimpleCorrupted(`{\"foo\":bar}`),\n\t\tsimpleCorrupted(`{\"foo\":\"bar\",}`),\n\t\tsimpleCorrupted(`{}{}`),\n\t\tsimpleCorrupted(`{},{}`),\n\t\tsimpleCorrupted(`{[},{]}`),\n\t\tsimpleCorrupted(`{[,]}`),\n\t\tsimpleCorrupted(`{[]}`),\n\t\tsimpleCorrupted(`{}1`),\n\t\tsimpleCorrupted(`1{}`),\n\t\tsimpleCorrupted(`{\"x\"::1}`),\n\t\tsimpleCorrupted(`{null:null}`),\n\t\tsimpleCorrupted(`{\"foo:\"bar\"}`),\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleInvalid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_NullSimpleCorrupted(t *testing.T) {\n\ttests := []*testNode{\n\t\t{name: \"nul\", input: []byte(\"nul\")},\n\t\t{name: \"nil\", input: []byte(\"nil\")},\n\t\t{name: \"nill\", input: []byte(\"nill\")},\n\t\t{name: \"NILL\", input: []byte(\"NILL\")},\n\t\t{name: \"Null\", input: []byte(\"Null\")},\n\t\t{name: \"NULL\", input: []byte(\"NULL\")},\n\t\t{name: \"spaces\", input: []byte(\"Nu ll\")},\n\t\t{name: \"null1\", input: []byte(\"null1\")},\n\t\t{name: \"double\", input: []byte(\"null null\")},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleInvalid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_BoolSimpleSuccess(t *testing.T) {\n\ttests := []*testNode{\n\t\t{name: \"lower true\", input: []byte(\"true\"), _type: Boolean, value: []byte(\"true\")},\n\t\t{name: \"lower false\", input: []byte(\"false\"), _type: Boolean, value: []byte(\"false\")},\n\t\t{name: \"spaces true\", input: []byte(\"  true\\r\\n \"), _type: Boolean, value: []byte(\"true\")},\n\t\t{name: \"spaces false\", input: []byte(\"  false\\r\\n \"), _type: Boolean, value: []byte(\"false\")},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleValid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_BoolSimpleCorrupted(t *testing.T) {\n\ttests := []*testNode{\n\t\tsimpleCorrupted(\"tru\"),\n\t\tsimpleCorrupted(\"fals\"),\n\t\tsimpleCorrupted(\"tre\"),\n\t\tsimpleCorrupted(\"fal se\"),\n\t\tsimpleCorrupted(\"true false\"),\n\t\tsimpleCorrupted(\"True\"),\n\t\tsimpleCorrupted(\"TRUE\"),\n\t\tsimpleCorrupted(\"False\"),\n\t\tsimpleCorrupted(\"FALSE\"),\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleInvalid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_ArraySimpleSuccess(t *testing.T) {\n\ttests := []*testNode{\n\t\t{name: \"[]\", input: []byte(\"[]\"), _type: Array, value: []byte(\"[]\")},\n\t\t{name: \"[1]\", input: []byte(\"[1]\"), _type: Array, value: []byte(\"[1]\")},\n\t\t{name: \"[1,2,3]\", input: []byte(\"[1,2,3]\"), _type: Array, value: []byte(\"[1,2,3]\")},\n\t\t{name: \"[1, 2, 3]\", input: []byte(\"[1, 2, 3]\"), _type: Array, value: []byte(\"[1, 2, 3]\")},\n\t\t{name: \"[1,[2],3]\", input: []byte(\"[1,[2],3]\"), _type: Array, value: []byte(\"[1,[2],3]\")},\n\t\t{name: \"[[],[],[]]\", input: []byte(\"[[],[],[]]\"), _type: Array, value: []byte(\"[[],[],[]]\")},\n\t\t{name: \"[[[[[]]]]]\", input: []byte(\"[[[[[]]]]]\"), _type: Array, value: []byte(\"[[[[[]]]]]\")},\n\t\t{name: \"[true,null,1,\\\"foo\\\",[]]\", input: []byte(\"[true,null,1,\\\"foo\\\",[]]\"), _type: Array, value: []byte(\"[true,null,1,\\\"foo\\\",[]]\")},\n\t\t{name: \"spaces\", input: []byte(\"\\n\\r [\\n1\\n ]\\r\\n\"), _type: Array, value: []byte(\"[\\n1\\n ]\")},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleValid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_ArraySimpleCorrupted(t *testing.T) {\n\ttests := []*testNode{\n\t\tsimpleCorrupted(\"[,]\"),\n\t\tsimpleCorrupted(\"[]\\\\\"),\n\t\tsimpleCorrupted(\"[1,]\"),\n\t\tsimpleCorrupted(\"[[]\"),\n\t\tsimpleCorrupted(\"[]]\"),\n\t\tsimpleCorrupted(\"1[]\"),\n\t\tsimpleCorrupted(\"[]1\"),\n\t\tsimpleCorrupted(\"[[]1]\"),\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleInvalid(test, t)\n\t\t})\n\t}\n}\n\n// Examples from https://json.org/example.html\nfunc TestUnmarshal(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\tvalue string\n\t}{\n\t\t{\n\t\t\tname: \"glossary\",\n\t\t\tvalue: `{\n\t\t\t\t\"glossary\": {\n\t\t\t\t\t\"title\": \"example glossary\",\n\t\t\t\t\t\"GlossDiv\": {\n\t\t\t\t\t\t\"title\": \"S\",\n\t\t\t\t\t\t\"GlossList\": {\n\t\t\t\t\t\t\t\"GlossEntry\": {\n\t\t\t\t\t\t\t\t\"ID\": \"SGML\",\n\t\t\t\t\t\t\t\t\"SortAs\": \"SGML\",\n\t\t\t\t\t\t\t\t\"GlossTerm\": \"Standard Generalized Markup Language\",\n\t\t\t\t\t\t\t\t\"Acronym\": \"SGML\",\n\t\t\t\t\t\t\t\t\"Abbrev\": \"ISO 8879:1986\",\n\t\t\t\t\t\t\t\t\"GlossDef\": {\n\t\t\t\t\t\t\t\t\t\"para\": \"A meta-markup language, used to create markup languages such as DocBook.\",\n\t\t\t\t\t\t\t\t\t\"GlossSeeAlso\": [\"GML\", \"XML\"]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"GlossSee\": \"markup\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}`,\n\t\t},\n\t\t{\n\t\t\tname: \"menu\",\n\t\t\tvalue: `{\"menu\": {\n\t\t\t\t\"id\": \"file\",\n\t\t\t\t\"value\": \"File\",\n\t\t\t\t\"popup\": {\n\t\t\t\t  \"menuitem\": [\n\t\t\t\t\t{\"value\": \"New\", \"onclick\": \"CreateNewDoc()\"},\n\t\t\t\t\t{\"value\": \"Open\", \"onclick\": \"OpenDoc()\"},\n\t\t\t\t\t{\"value\": \"Close\", \"onclick\": \"CloseDoc()\"}\n\t\t\t\t  ]\n\t\t\t\t}\n\t\t\t}}`,\n\t\t},\n\t\t{\n\t\t\tname: \"widget\",\n\t\t\tvalue: `{\"widget\": {\n\t\t\t\t\"debug\": \"on\",\n\t\t\t\t\"window\": {\n\t\t\t\t\t\"title\": \"Sample Konfabulator Widget\",\n\t\t\t\t\t\"name\": \"main_window\",\n\t\t\t\t\t\"width\": 500,\n\t\t\t\t\t\"height\": 500\n\t\t\t\t},\n\t\t\t\t\"image\": { \n\t\t\t\t\t\"src\": \"Images/Sun.png\",\n\t\t\t\t\t\"name\": \"sun1\",\n\t\t\t\t\t\"hOffset\": 250,\n\t\t\t\t\t\"vOffset\": 250,\n\t\t\t\t\t\"alignment\": \"center\"\n\t\t\t\t},\n\t\t\t\t\"text\": {\n\t\t\t\t\t\"data\": \"Click Here\",\n\t\t\t\t\t\"size\": 36,\n\t\t\t\t\t\"style\": \"bold\",\n\t\t\t\t\t\"name\": \"text1\",\n\t\t\t\t\t\"hOffset\": 250,\n\t\t\t\t\t\"vOffset\": 100,\n\t\t\t\t\t\"alignment\": \"center\",\n\t\t\t\t\t\"onMouseUp\": \"sun1.opacity = (sun1.opacity / 100) * 90;\"\n\t\t\t\t}\n\t\t\t}}    `,\n\t\t},\n\t\t{\n\t\t\tname: \"web-app\",\n\t\t\tvalue: `{\"web-app\": {\n\t\t\t\t\"servlet\": [   \n\t\t\t\t  {\n\t\t\t\t\t\"servlet-name\": \"cofaxCDS\",\n\t\t\t\t\t\"servlet-class\": \"org.cofax.cds.CDSServlet\",\n\t\t\t\t\t\"init-param\": {\n\t\t\t\t\t  \"configGlossary:installationAt\": \"Philadelphia, PA\",\n\t\t\t\t\t  \"configGlossary:adminEmail\": \"ksm@pobox.com\",\n\t\t\t\t\t  \"configGlossary:poweredBy\": \"Cofax\",\n\t\t\t\t\t  \"configGlossary:poweredByIcon\": \"/images/cofax.gif\",\n\t\t\t\t\t  \"configGlossary:staticPath\": \"/content/static\",\n\t\t\t\t\t  \"templateProcessorClass\": \"org.cofax.WysiwygTemplate\",\n\t\t\t\t\t  \"templateLoaderClass\": \"org.cofax.FilesTemplateLoader\",\n\t\t\t\t\t  \"templatePath\": \"templates\",\n\t\t\t\t\t  \"templateOverridePath\": \"\",\n\t\t\t\t\t  \"defaultListTemplate\": \"listTemplate.htm\",\n\t\t\t\t\t  \"defaultFileTemplate\": \"articleTemplate.htm\",\n\t\t\t\t\t  \"useJSP\": false,\n\t\t\t\t\t  \"jspListTemplate\": \"listTemplate.jsp\",\n\t\t\t\t\t  \"jspFileTemplate\": \"articleTemplate.jsp\",\n\t\t\t\t\t  \"cachePackageTagsTrack\": 200,\n\t\t\t\t\t  \"cachePackageTagsStore\": 200,\n\t\t\t\t\t  \"cachePackageTagsRefresh\": 60,\n\t\t\t\t\t  \"cacheTemplatesTrack\": 100,\n\t\t\t\t\t  \"cacheTemplatesStore\": 50,\n\t\t\t\t\t  \"cacheTemplatesRefresh\": 15,\n\t\t\t\t\t  \"cachePagesTrack\": 200,\n\t\t\t\t\t  \"cachePagesStore\": 100,\n\t\t\t\t\t  \"cachePagesRefresh\": 10,\n\t\t\t\t\t  \"cachePagesDirtyRead\": 10,\n\t\t\t\t\t  \"searchEngineListTemplate\": \"forSearchEnginesList.htm\",\n\t\t\t\t\t  \"searchEngineFileTemplate\": \"forSearchEngines.htm\",\n\t\t\t\t\t  \"searchEngineRobotsDb\": \"WEB-INF/robots.db\",\n\t\t\t\t\t  \"useDataStore\": true,\n\t\t\t\t\t  \"dataStoreClass\": \"org.cofax.SqlDataStore\",\n\t\t\t\t\t  \"redirectionClass\": \"org.cofax.SqlRedirection\",\n\t\t\t\t\t  \"dataStoreName\": \"cofax\",\n\t\t\t\t\t  \"dataStoreDriver\": \"com.microsoft.jdbc.sqlserver.SQLServerDriver\",\n\t\t\t\t\t  \"dataStoreUrl\": \"jdbc:microsoft:sqlserver://LOCALHOST:1433;DatabaseName=goon\",\n\t\t\t\t\t  \"dataStoreUser\": \"sa\",\n\t\t\t\t\t  \"dataStorePassword\": \"dataStoreTestQuery\",\n\t\t\t\t\t  \"dataStoreTestQuery\": \"SET NOCOUNT ON;select test='test';\",\n\t\t\t\t\t  \"dataStoreLogFile\": \"/usr/local/tomcat/logs/datastore.log\",\n\t\t\t\t\t  \"dataStoreInitConns\": 10,\n\t\t\t\t\t  \"dataStoreMaxConns\": 100,\n\t\t\t\t\t  \"dataStoreConnUsageLimit\": 100,\n\t\t\t\t\t  \"dataStoreLogLevel\": \"debug\",\n\t\t\t\t\t  \"maxUrlLength\": 500}},\n\t\t\t\t  {\n\t\t\t\t\t\"servlet-name\": \"cofaxEmail\",\n\t\t\t\t\t\"servlet-class\": \"org.cofax.cds.EmailServlet\",\n\t\t\t\t\t\"init-param\": {\n\t\t\t\t\t\"mailHost\": \"mail1\",\n\t\t\t\t\t\"mailHostOverride\": \"mail2\"}},\n\t\t\t\t  {\n\t\t\t\t\t\"servlet-name\": \"cofaxAdmin\",\n\t\t\t\t\t\"servlet-class\": \"org.cofax.cds.AdminServlet\"},\n\t\t\t   \n\t\t\t\t  {\n\t\t\t\t\t\"servlet-name\": \"fileServlet\",\n\t\t\t\t\t\"servlet-class\": \"org.cofax.cds.FileServlet\"},\n\t\t\t\t  {\n\t\t\t\t\t\"servlet-name\": \"cofaxTools\",\n\t\t\t\t\t\"servlet-class\": \"org.cofax.cms.CofaxToolsServlet\",\n\t\t\t\t\t\"init-param\": {\n\t\t\t\t\t  \"templatePath\": \"toolstemplates/\",\n\t\t\t\t\t  \"log\": 1,\n\t\t\t\t\t  \"logLocation\": \"/usr/local/tomcat/logs/CofaxTools.log\",\n\t\t\t\t\t  \"logMaxSize\": \"\",\n\t\t\t\t\t  \"dataLog\": 1,\n\t\t\t\t\t  \"dataLogLocation\": \"/usr/local/tomcat/logs/dataLog.log\",\n\t\t\t\t\t  \"dataLogMaxSize\": \"\",\n\t\t\t\t\t  \"removePageCache\": \"/content/admin/remove?cache=pages\u0026id=\",\n\t\t\t\t\t  \"removeTemplateCache\": \"/content/admin/remove?cache=templates\u0026id=\",\n\t\t\t\t\t  \"fileTransferFolder\": \"/usr/local/tomcat/webapps/content/fileTransferFolder\",\n\t\t\t\t\t  \"lookInContext\": 1,\n\t\t\t\t\t  \"adminGroupID\": 4,\n\t\t\t\t\t  \"betaServer\": true}}],\n\t\t\t\t\"servlet-mapping\": {\n\t\t\t\t  \"cofaxCDS\": \"/\",\n\t\t\t\t  \"cofaxEmail\": \"/cofaxutil/aemail/*\",\n\t\t\t\t  \"cofaxAdmin\": \"/admin/*\",\n\t\t\t\t  \"fileServlet\": \"/static/*\",\n\t\t\t\t  \"cofaxTools\": \"/tools/*\"},\n\t\t\t   \n\t\t\t\t\"taglib\": {\n\t\t\t\t  \"taglib-uri\": \"cofax.tld\",\n\t\t\t\t  \"taglib-location\": \"/WEB-INF/tlds/cofax.tld\"}}}`,\n\t\t},\n\t\t{\n\t\t\tname: \"SVG Viewer\",\n\t\t\tvalue: `{\"menu\": {\n\t\t\t\t\"header\": \"SVG Viewer\",\n\t\t\t\t\"items\": [\n\t\t\t\t\t{\"id\": \"Open\"},\n\t\t\t\t\t{\"id\": \"OpenNew\", \"label\": \"Open New\"},\n\t\t\t\t\tnull,\n\t\t\t\t\t{\"id\": \"ZoomIn\", \"label\": \"Zoom In\"},\n\t\t\t\t\t{\"id\": \"ZoomOut\", \"label\": \"Zoom Out\"},\n\t\t\t\t\t{\"id\": \"OriginalView\", \"label\": \"Original View\"},\n\t\t\t\t\tnull,\n\t\t\t\t\t{\"id\": \"Quality\"},\n\t\t\t\t\t{\"id\": \"Pause\"},\n\t\t\t\t\t{\"id\": \"Mute\"},\n\t\t\t\t\tnull,\n\t\t\t\t\t{\"id\": \"Find\", \"label\": \"Find...\"},\n\t\t\t\t\t{\"id\": \"FindAgain\", \"label\": \"Find Again\"},\n\t\t\t\t\t{\"id\": \"Copy\"},\n\t\t\t\t\t{\"id\": \"CopyAgain\", \"label\": \"Copy Again\"},\n\t\t\t\t\t{\"id\": \"CopySVG\", \"label\": \"Copy SVG\"},\n\t\t\t\t\t{\"id\": \"ViewSVG\", \"label\": \"View SVG\"},\n\t\t\t\t\t{\"id\": \"ViewSource\", \"label\": \"View Source\"},\n\t\t\t\t\t{\"id\": \"SaveAs\", \"label\": \"Save As\"},\n\t\t\t\t\tnull,\n\t\t\t\t\t{\"id\": \"Help\"},\n\t\t\t\t\t{\"id\": \"About\", \"label\": \"About Adobe CVG Viewer...\"}\n\t\t\t\t]\n\t\t\t}}`,\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\t_, err := Unmarshal([]byte(test.value))\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Error on Unmarshal: %s\", err.Error())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUnmarshalSafe(t *testing.T) {\n\tjson := []byte(`{ \"store\": {\n\t\t\"book\": [ \n\t\t  { \"category\": \"reference\",\n\t\t\t\"author\": \"Nigel Rees\",\n\t\t\t\"title\": \"Sayings of the Century\",\n\t\t\t\"price\": 8.95\n\t\t  },\n\t\t  { \"category\": \"fiction\",\n\t\t\t\"author\": \"Evelyn Waugh\",\n\t\t\t\"title\": \"Sword of Honour\",\n\t\t\t\"price\": 12.99\n\t\t  },\n\t\t  { \"category\": \"fiction\",\n\t\t\t\"author\": \"Herman Melville\",\n\t\t\t\"title\": \"Moby Dick\",\n\t\t\t\"isbn\": \"0-553-21311-3\",\n\t\t\t\"price\": 8.99\n\t\t  },\n\t\t  { \"category\": \"fiction\",\n\t\t\t\"author\": \"J. R. R. Tolkien\",\n\t\t\t\"title\": \"The Lord of the Rings\",\n\t\t\t\"isbn\": \"0-395-19395-8\",\n\t\t\t\"price\": 22.99\n\t\t  }\n\t\t],\n\t\t\"bicycle\": {\n\t\t  \"color\": \"red\",\n\t\t  \"price\": 19.95\n\t\t}\n\t  }\n\t}`)\n\tsafe, err := UnmarshalSafe(json)\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal: %s\", err.Error())\n\t} else if safe == nil {\n\t\tt.Errorf(\"Error on Unmarshal: safe is nil\")\n\t} else {\n\t\troot, err := Unmarshal(json)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Error on Unmarshal: %s\", err.Error())\n\t\t} else if root == nil {\n\t\t\tt.Errorf(\"Error on Unmarshal: root is nil\")\n\t\t} else if !bytes.Equal(root.source(), safe.source()) {\n\t\t\tt.Errorf(\"Error on UnmarshalSafe: values not same\")\n\t\t}\n\t}\n}\n\n// BenchmarkGoStdUnmarshal-8   \t   61698\t     19350 ns/op\t     288 B/op\t       6 allocs/op\n// BenchmarkUnmarshal-8        \t   45620\t     26165 ns/op\t   21889 B/op\t     367 allocs/op\n//\n// type bench struct {\n// \tName  string `json:\"name\"`\n// \tValue int    `json:\"value\"`\n// }\n\n// func BenchmarkGoStdUnmarshal(b *testing.B) {\n// \tdata := []byte(webApp)\n// \tfor i := 0; i \u003c b.N; i++ {\n// \t\terr := json.Unmarshal(data, \u0026bench{})\n// \t\tif err != nil {\n// \t\t\tb.Fatal(err)\n// \t\t}\n// \t}\n// }\n\n// func BenchmarkUnmarshal(b *testing.B) {\n// \tdata := []byte(webApp)\n// \tfor i := 0; i \u003c b.N; i++ {\n// \t\t_, err := Unmarshal(data)\n// \t\tif err != nil {\n// \t\t\tb.Fatal(err)\n// \t\t}\n// \t}\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":"encode_test.gno","body":"package json\n\nimport \"testing\"\n\nfunc TestMarshal_Primitive(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tnode *Node\n\t}{\n\t\t{\n\t\t\tname: \"null\",\n\t\t\tnode: NullNode(\"\"),\n\t\t},\n\t\t{\n\t\t\tname: \"true\",\n\t\t\tnode: BoolNode(\"\", true),\n\t\t},\n\t\t{\n\t\t\tname: \"false\",\n\t\t\tnode: BoolNode(\"\", false),\n\t\t},\n\t\t{\n\t\t\tname: `\"string\"`,\n\t\t\tnode: StringNode(\"\", \"string\"),\n\t\t},\n\t\t{\n\t\t\tname: `\"one \\\"encoded\\\" string\"`,\n\t\t\tnode: StringNode(\"\", `one \"encoded\" string`),\n\t\t},\n\t\t{\n\t\t\tname: `{\"foo\":\"bar\"}`,\n\t\t\tnode: ObjectNode(\"\", map[string]*Node{\n\t\t\t\t\"foo\": StringNode(\"foo\", \"bar\"),\n\t\t\t}),\n\t\t},\n\t\t{\n\t\t\tname: \"42\",\n\t\t\tnode: NumberNode(\"\", 42),\n\t\t},\n\t\t{\n\t\t\tname: \"3.14\",\n\t\t\tnode: NumberNode(\"\", 3.14),\n\t\t},\n\t\t{\n\t\t\tname: `[1,2,3]`,\n\t\t\tnode: ArrayNode(\"\", []*Node{\n\t\t\t\tNumberNode(\"0\", 1),\n\t\t\t\tNumberNode(\"2\", 2),\n\t\t\t\tNumberNode(\"3\", 3),\n\t\t\t}),\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tvalue, err := Marshal(test.node)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"unexpected error: %s\", err)\n\t\t\t} else if string(value) != test.name {\n\t\t\t\tt.Errorf(\"wrong result: '%s', expected '%s'\", value, test.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMarshal_Object(t *testing.T) {\n\tnode := ObjectNode(\"\", map[string]*Node{\n\t\t\"foo\": StringNode(\"foo\", \"bar\"),\n\t\t\"baz\": NumberNode(\"baz\", 100500),\n\t\t\"qux\": NullNode(\"qux\"),\n\t})\n\n\tmustKey := []string{\"foo\", \"baz\", \"qux\"}\n\n\tvalue, err := Marshal(node)\n\tif err != nil {\n\t\tt.Errorf(\"unexpected error: %s\", err)\n\t}\n\n\t// the order of keys in the map is not guaranteed\n\t// so we need to unmarshal the result and check the keys\n\tdecoded, err := Unmarshal(value)\n\tif err != nil {\n\t\tt.Errorf(\"unexpected error: %s\", err)\n\t}\n\n\tfor _, key := range mustKey {\n\t\tif node, err := decoded.GetKey(key); err != nil {\n\t\t\tt.Errorf(\"unexpected error: %s\", err)\n\t\t} else {\n\t\t\tif node == nil {\n\t\t\t\tt.Errorf(\"node is nil\")\n\t\t\t} else if node.key == nil {\n\t\t\t\tt.Errorf(\"key is nil\")\n\t\t\t} else if *node.key != key {\n\t\t\t\tt.Errorf(\"wrong key: '%s', expected '%s'\", *node.key, key)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc valueNode(prev *Node, key string, typ ValueType, val any) *Node {\n\tcurr := \u0026Node{\n\t\tprev:     prev,\n\t\tdata:     nil,\n\t\tkey:      \u0026key,\n\t\tborders:  [2]int{0, 0},\n\t\tvalue:    val,\n\t\tmodified: true,\n\t}\n\n\tif val != nil {\n\t\tcurr.nodeType = typ\n\t}\n\n\treturn curr\n}\n\nfunc TestMarshal_Errors(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tnode func() (node *Node)\n\t}{\n\t\t{\n\t\t\tname: \"nil\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\treturn\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"broken\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\tnode = Must(Unmarshal([]byte(`{}`)))\n\t\t\t\tnode.borders[1] = 0\n\t\t\t\treturn\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Numeric\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\treturn valueNode(nil, \"\", Number, false)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"String\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\treturn valueNode(nil, \"\", String, false)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Bool\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\treturn valueNode(nil, \"\", Boolean, 1)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Array_1\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\tnode = ArrayNode(\"\", nil)\n\t\t\t\tnode.next[\"1\"] = NullNode(\"1\")\n\t\t\t\treturn\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Array_2\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\treturn ArrayNode(\"\", []*Node{valueNode(nil, \"\", Boolean, 1)})\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Object\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\treturn ObjectNode(\"\", map[string]*Node{\"key\": valueNode(nil, \"key\", Boolean, 1)})\n\t\t\t},\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tvalue, err := Marshal(test.node())\n\t\t\tif err == nil {\n\t\t\t\tt.Errorf(\"expected error\")\n\t\t\t} else if len(value) != 0 {\n\t\t\t\tt.Errorf(\"wrong result\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMarshal_Nil(t *testing.T) {\n\t_, err := Marshal(nil)\n\tif err == nil {\n\t\tt.Error(\"Expected error for nil node, but got nil\")\n\t}\n}\n\nfunc TestMarshal_NotModified(t *testing.T) {\n\tnode := \u0026Node{}\n\t_, err := Marshal(node)\n\tif err == nil {\n\t\tt.Error(\"Expected error for not modified node, but got nil\")\n\t}\n}\n\nfunc TestMarshalCycleReference(t *testing.T) {\n\tnode1 := \u0026Node{\n\t\tkey:      stringPtr(\"node1\"),\n\t\tnodeType: String,\n\t\tnext: map[string]*Node{\n\t\t\t\"next\": nil,\n\t\t},\n\t}\n\n\tnode2 := \u0026Node{\n\t\tkey:      stringPtr(\"node2\"),\n\t\tnodeType: String,\n\t\tprev:     node1,\n\t}\n\n\tnode1.next[\"next\"] = node2\n\n\t_, err := Marshal(node1)\n\tif err == nil {\n\t\tt.Error(\"Expected error for cycle reference, but got nil\")\n\t}\n}\n\nfunc TestMarshalNoCycleReference(t *testing.T) {\n\tnode1 := \u0026Node{\n\t\tkey:      stringPtr(\"node1\"),\n\t\tnodeType: String,\n\t\tvalue:    \"value1\",\n\t\tmodified: true,\n\t}\n\n\tnode2 := \u0026Node{\n\t\tkey:      stringPtr(\"node2\"),\n\t\tnodeType: String,\n\t\tvalue:    \"value2\",\n\t\tmodified: true,\n\t}\n\n\t_, err := Marshal(node1)\n\tif err != nil {\n\t\tt.Errorf(\"Unexpected error: %v\", err)\n\t}\n\n\t_, err = Marshal(node2)\n\tif err != nil {\n\t\tt.Errorf(\"Unexpected error: %v\", err)\n\t}\n}\n\nfunc stringPtr(s string) *string {\n\treturn \u0026s\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":"escape_test.gno","body":"package json\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\t\"unicode/utf8\"\n)\n\nfunc TestHexToInt(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tc    byte\n\t\twant int\n\t}{\n\t\t{\"Digit 0\", '0', 0},\n\t\t{\"Digit 9\", '9', 9},\n\t\t{\"Uppercase A\", 'A', 10},\n\t\t{\"Uppercase F\", 'F', 15},\n\t\t{\"Lowercase a\", 'a', 10},\n\t\t{\"Lowercase f\", 'f', 15},\n\t\t{\"Invalid character1\", 'g', badHex},\n\t\t{\"Invalid character2\", 'G', badHex},\n\t\t{\"Invalid character3\", 'z', badHex},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := h2i(tt.c); got != tt.want {\n\t\t\t\tt.Errorf(\"h2i() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIsSurrogatePair(t *testing.T) {\n\ttestCases := []struct {\n\t\tname     string\n\t\tr        rune\n\t\texpected bool\n\t}{\n\t\t{\"high surrogate start\", 0xD800, true},\n\t\t{\"high surrogate end\", 0xDBFF, true},\n\t\t{\"low surrogate start\", 0xDC00, true},\n\t\t{\"low surrogate end\", 0xDFFF, true},\n\t\t{\"Non-surrogate\", 0x0000, false},\n\t\t{\"Non-surrogate 2\", 0xE000, false},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tif got := isSurrogatePair(tc.r); got != tc.expected {\n\t\t\t\tt.Errorf(\"isSurrogate() = %v, want %v\", got, tc.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCombineSurrogates(t *testing.T) {\n\ttestCases := []struct {\n\t\thigh, low rune\n\t\texpected  rune\n\t}{\n\t\t{0xD83D, 0xDC36, 0x1F436}, // 🐶 U+1F436 DOG FACE\n\t\t{0xD83D, 0xDE00, 0x1F600}, // 😀 U+1F600 GRINNING FACE\n\t\t{0xD83C, 0xDF03, 0x1F303}, // 🌃 U+1F303 NIGHT WITH STARS\n\t}\n\n\tfor _, tc := range testCases {\n\t\tresult := combineSurrogates(tc.high, tc.low)\n\t\tif result != tc.expected {\n\t\t\tt.Errorf(\"combineSurrogates(%U, %U) = %U; want %U\", tc.high, tc.low, result, tc.expected)\n\t\t}\n\t}\n}\n\nfunc TestDecodeSingleUnicodeEscape(t *testing.T) {\n\ttestCases := []struct {\n\t\tinput    []byte\n\t\texpected rune\n\t\tisValid  bool\n\t}{\n\t\t// valid unicode escape sequences\n\t\t{[]byte(`\\u0041`), 'A', true},\n\t\t{[]byte(`\\u03B1`), 'α', true},\n\t\t{[]byte(`\\u00E9`), 'é', true}, // valid non-English character\n\t\t{[]byte(`\\u0021`), '!', true}, // valid special character\n\t\t{[]byte(`\\uFF11`), '１', true},\n\t\t{[]byte(`\\uD83D`), 0xD83D, true},\n\t\t{[]byte(`\\uDE03`), 0xDE03, true},\n\n\t\t// invalid unicode escape sequences\n\t\t{[]byte(`\\u004`), utf8.RuneError, false},  // too short\n\t\t{[]byte(`\\uXYZW`), utf8.RuneError, false}, // invalid hex\n\t\t{[]byte(`\\u00G1`), utf8.RuneError, false}, // non-hex character\n\t}\n\n\tfor _, tc := range testCases {\n\t\tresult, isValid := decodeSingleUnicodeEscape(tc.input)\n\t\tif result != tc.expected || isValid != tc.isValid {\n\t\t\tt.Errorf(\"decodeSingleUnicodeEscape(%s) = (%U, %v); want (%U, %v)\", tc.input, result, isValid, tc.expected, tc.isValid)\n\t\t}\n\t}\n}\n\nfunc TestDecodeUnicodeEscape(t *testing.T) {\n\ttests := []struct {\n\t\tinput    []byte\n\t\texpected rune\n\t\tsize     int\n\t}{\n\t\t{[]byte(`\\u0041`), 'A', 6},\n\t\t{[]byte(`\\uD83D\\uDE00`), 0x1F600, 12}, // 😀\n\t\t{[]byte(`\\uD834\\uDD1E`), 0x1D11E, 12}, // 𝄞\n\t\t{[]byte(`\\uFFFF`), '\\uFFFF', 6},\n\t\t{[]byte(`\\uXYZW`), utf8.RuneError, -1},\n\t\t{[]byte(`\\uD800`), utf8.RuneError, -1},       // single high surrogate\n\t\t{[]byte(`\\uDC00`), utf8.RuneError, -1},       // single low surrogate\n\t\t{[]byte(`\\uD800\\uDC00`), 0x10000, 12},        // First code point above U+FFFF\n\t\t{[]byte(`\\uDBFF\\uDFFF`), 0x10FFFF, 12},       // Maximum code point\n\t\t{[]byte(`\\uD83D\\u0041`), utf8.RuneError, -1}, // invalid surrogate pair\n\t}\n\n\tfor _, tc := range tests {\n\t\tr, size := decodeUnicodeEscape(tc.input)\n\t\tif r != tc.expected || size != tc.size {\n\t\t\tt.Errorf(\"decodeUnicodeEscape(%q) = (%U, %d); want (%U, %d)\", tc.input, r, size, tc.expected, tc.size)\n\t\t}\n\t}\n}\n\nfunc TestUnescapeToUTF8(t *testing.T) {\n\ttests := []struct {\n\t\tinput       []byte\n\t\texpectedIn  int\n\t\texpectedOut int\n\t\tisError     bool\n\t}{\n\t\t// valid escape sequences\n\t\t{[]byte(`\\n`), 2, 1, false},\n\t\t{[]byte(`\\t`), 2, 1, false},\n\t\t{[]byte(`\\u0041`), 6, 1, false},\n\t\t{[]byte(`\\u03B1`), 6, 2, false},\n\t\t{[]byte(`\\uD830\\uDE03`), 12, 4, false},\n\n\t\t// invalid escape sequences\n\t\t{[]byte(`\\`), -1, -1, true},            // incomplete escape sequence\n\t\t{[]byte(`\\x`), -1, -1, true},           // invalid escape character\n\t\t{[]byte(`\\u`), -1, -1, true},           // incomplete unicode escape sequence\n\t\t{[]byte(`\\u004`), -1, -1, true},        // invalid unicode escape sequence\n\t\t{[]byte(`\\uXYZW`), -1, -1, true},       // invalid unicode escape sequence\n\t\t{[]byte(`\\uD83D\\u0041`), -1, -1, true}, // invalid unicode escape sequence\n\t}\n\n\tfor _, tc := range tests {\n\t\tinput := make([]byte, len(tc.input))\n\t\tcopy(input, tc.input)\n\t\toutput := make([]byte, utf8.UTFMax)\n\t\tinLen, outLen, err := processEscapedUTF8(input, output)\n\t\tif (err != nil) != tc.isError {\n\t\t\tt.Errorf(\"processEscapedUTF8(%q) = %v; want %v\", tc.input, err, tc.isError)\n\t\t}\n\n\t\tif inLen != tc.expectedIn || outLen != tc.expectedOut {\n\t\t\tt.Errorf(\"processEscapedUTF8(%q) = (%d, %d); want (%d, %d)\", tc.input, inLen, outLen, tc.expectedIn, tc.expectedOut)\n\t\t}\n\t}\n}\n\nfunc TestUnescape(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []byte\n\t\texpected []byte\n\t\tisError  bool\n\t}{\n\t\t{\"NoEscape\", []byte(\"hello world\"), []byte(\"hello world\"), false},\n\t\t{\"SingleEscape\", []byte(\"hello\\\\nworld\"), []byte(\"hello\\nworld\"), false},\n\t\t{\"MultipleEscapes\", []byte(\"line1\\\\nline2\\\\r\\\\nline3\"), []byte(\"line1\\nline2\\r\\nline3\"), false},\n\t\t{\"UnicodeEscape\", []byte(\"snowman:\\\\u2603\"), []byte(\"snowman:\\u2603\"), false},\n\t\t{\"SurrogatePair\", []byte(\"emoji:\\\\uD83D\\\\uDE00\"), []byte(\"emoji:😀\"), false},\n\t\t{\"InvalidEscape\", []byte(\"hello\\\\xworld\"), nil, true},\n\t\t{\"IncompleteUnicode\", []byte(\"incomplete:\\\\u123\"), nil, true},\n\t\t{\"InvalidSurrogatePair\", []byte(\"invalid:\\\\uD83D\\\\u0041\"), nil, true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\toutput := make([]byte, len(tc.input)*2) // Allocate extra space for possible expansion\n\t\t\tresult, err := Unescape(tc.input, output)\n\t\t\tif (err != nil) != tc.isError {\n\t\t\t\tt.Errorf(\"Unescape(%q) error = %v; want error = %v\", tc.input, err, tc.isError)\n\t\t\t}\n\n\t\t\tif !tc.isError \u0026\u0026 !bytes.Equal(result, tc.expected) {\n\t\t\t\tt.Errorf(\"Unescape(%q) = %q; want %q\", tc.input, result, tc.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUnquoteBytes(t *testing.T) {\n\ttests := []struct {\n\t\tinput    []byte\n\t\tborder   byte\n\t\texpected []byte\n\t\tok       bool\n\t}{\n\t\t{[]byte(\"\\\"hello\\\"\"), '\"', []byte(\"hello\"), true},\n\t\t{[]byte(\"'hello'\"), '\\'', []byte(\"hello\"), true},\n\t\t{[]byte(\"\\\"hello\"), '\"', nil, false},\n\t\t{[]byte(\"hello\\\"\"), '\"', nil, false},\n\t\t{[]byte(\"\\\"he\\\\\\\"llo\\\"\"), '\"', []byte(\"he\\\"llo\"), true},\n\t\t{[]byte(\"\\\"he\\\\nllo\\\"\"), '\"', []byte(\"he\\nllo\"), true},\n\t\t{[]byte(\"\\\"\\\"\"), '\"', []byte(\"\"), true},\n\t\t{[]byte(\"''\"), '\\'', []byte(\"\"), true},\n\t\t{[]byte(\"\\\"\\\\u0041\\\"\"), '\"', []byte(\"A\"), true},\n\t\t{[]byte(`\"Hello, 世界\"`), '\"', []byte(\"Hello, 世界\"), true},\n\t\t{[]byte(`\"Hello, \\x80\"`), '\"', nil, false},\n\t\t{[]byte(`\"invalid surrogate: \\uD83D\\u0041\"`), '\"', nil, false},\n\t}\n\n\tfor _, tc := range tests {\n\t\tresult, pass := unquoteBytes(tc.input, tc.border)\n\n\t\tif pass != tc.ok {\n\t\t\tt.Errorf(\"unquoteBytes(%q) = %v; want %v\", tc.input, pass, tc.ok)\n\t\t}\n\n\t\tif !bytes.Equal(result, tc.expected) {\n\t\t\tt.Errorf(\"unquoteBytes(%q) = %q; want %q\", tc.input, result, tc.expected)\n\t\t}\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/onbloc/json/v0\"\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":"indent_test.gno","body":"package json\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n)\n\nfunc TestIndentJSON(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []byte\n\t\tindent   string\n\t\texpected []byte\n\t}{\n\t\t{\n\t\t\tname:     \"empty object\",\n\t\t\tinput:    []byte(`{}`),\n\t\t\tindent:   \"  \",\n\t\t\texpected: []byte(`{}`),\n\t\t},\n\t\t{\n\t\t\tname:     \"empty array\",\n\t\t\tinput:    []byte(`[]`),\n\t\t\tindent:   \"  \",\n\t\t\texpected: []byte(`[]`),\n\t\t},\n\t\t{\n\t\t\tname:     \"nested object\",\n\t\t\tinput:    []byte(`{{}}`),\n\t\t\tindent:   \"\\t\",\n\t\t\texpected: []byte(\"{\\n\\t\\t{}\\n}\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"nested array\",\n\t\t\tinput:    []byte(`[[[]]]`),\n\t\t\tindent:   \"\\t\",\n\t\t\texpected: []byte(\"[[\\n\\t\\t[\\n\\t\\t\\t\\t\\n\\t\\t]\\n]]\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"top-level array\",\n\t\t\tinput:    []byte(`[\"apple\",\"banana\",\"cherry\"]`),\n\t\t\tindent:   \"\\t\",\n\t\t\texpected: []byte(`[\"apple\",\"banana\",\"cherry\"]`),\n\t\t},\n\t\t{\n\t\t\tname:     \"array of arrays\",\n\t\t\tinput:    []byte(`[\"apple\",[\"banana\",\"cherry\"],\"date\"]`),\n\t\t\tindent:   \"  \",\n\t\t\texpected: []byte(\"[\\\"apple\\\",[\\n    \\\"banana\\\",\\n    \\\"cherry\\\"\\n],\\\"date\\\"]\"),\n\t\t},\n\n\t\t{\n\t\t\tname:     \"nested array in object\",\n\t\t\tinput:    []byte(`{\"fruits\":[\"apple\",[\"banana\",\"cherry\"],\"date\"]}`),\n\t\t\tindent:   \"  \",\n\t\t\texpected: []byte(\"{\\n    \\\"fruits\\\": [\\\"apple\\\",[\\n        \\\"banana\\\",\\n        \\\"cherry\\\"\\n    ],\\\"date\\\"]\\n}\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"complex nested structure\",\n\t\t\tinput:    []byte(`{\"data\":{\"array\":[1,2,3],\"bool\":true,\"nestedArray\":[[\"a\",\"b\"],\"c\"]}}`),\n\t\t\tindent:   \"  \",\n\t\t\texpected: []byte(\"{\\n    \\\"data\\\": {\\n        \\\"array\\\": [1,2,3],\\\"bool\\\": true,\\\"nestedArray\\\": [[\\n            \\\"a\\\",\\n            \\\"b\\\"\\n        ],\\\"c\\\"]\\n    }\\n}\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"custom ident character\",\n\t\t\tinput:    []byte(`{\"fruits\":[\"apple\",[\"banana\",\"cherry\"],\"date\"]}`),\n\t\t\tindent:   \"*\",\n\t\t\texpected: []byte(\"{\\n**\\\"fruits\\\": [\\\"apple\\\",[\\n****\\\"banana\\\",\\n****\\\"cherry\\\"\\n**],\\\"date\\\"]\\n}\"),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tactual, err := Indent(tt.input, tt.indent)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"IndentJSON() error = %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !bytes.Equal(actual, tt.expected) {\n\t\t\t\tt.Errorf(\"IndentJSON() = %q, want %q\", actual, tt.expected)\n\t\t\t}\n\t\t})\n\t}\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":"node_test.gno","body":"package json\n\nimport (\n\t\"bytes\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nvar (\n\tnilKey   *string\n\tdummyKey = \"key\"\n)\n\ntype _args struct {\n\tprev *Node\n\tbuf  *buffer\n\ttyp  ValueType\n\tkey  **string\n}\n\ntype simpleNode struct {\n\tname string\n\tnode *Node\n}\n\nfunc TestNode_CreateNewNode(t *testing.T) {\n\trel := \u0026dummyKey\n\n\ttests := []struct {\n\t\tname        string\n\t\targs        _args\n\t\texpectCurr  *Node\n\t\texpectErr   bool\n\t\texpectPanic bool\n\t}{\n\t\t{\n\t\t\tname: \"child for non container type\",\n\t\t\targs: _args{\n\t\t\t\tprev: BoolNode(\"\", true),\n\t\t\t\tbuf:  newBuffer(make([]byte, 10)),\n\t\t\t\ttyp:  Boolean,\n\t\t\t\tkey:  \u0026rel,\n\t\t\t},\n\t\t\texpectCurr: nil,\n\t\t\texpectErr:  true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tif tt.expectPanic {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tt.Errorf(\"%s panic occurred when not expected: %v\", tt.name, r)\n\t\t\t\t} else if tt.expectPanic {\n\t\t\t\t\tt.Errorf(\"%s expected panic but didn't occur\", tt.name)\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tgot, err := NewNode(tt.args.prev, tt.args.buf, tt.args.typ, tt.args.key)\n\t\t\tif (err != nil) != tt.expectErr {\n\t\t\t\tt.Errorf(\"%s error = %v, expect error %v\", tt.name, err, tt.expectErr)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif tt.expectErr {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif !compareNodes(got, tt.expectCurr) {\n\t\t\t\tt.Errorf(\"%s got = %v, want %v\", tt.name, got, tt.expectCurr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_Value(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tdata        []byte\n\t\t_type       ValueType\n\t\texpected    any\n\t\terrExpected bool\n\t}{\n\t\t{name: \"null\", data: []byte(\"null\"), _type: Null, expected: nil},\n\t\t{name: \"1\", data: []byte(\"1\"), _type: Number, expected: float64(1)},\n\t\t{name: \".1\", data: []byte(\".1\"), _type: Number, expected: float64(.1)},\n\t\t{name: \"-.1e1\", data: []byte(\"-.1e1\"), _type: Number, expected: float64(-1)},\n\t\t{name: \"string\", data: []byte(\"\\\"foo\\\"\"), _type: String, expected: \"foo\"},\n\t\t{name: \"space\", data: []byte(\"\\\"foo bar\\\"\"), _type: String, expected: \"foo bar\"},\n\t\t{name: \"true\", data: []byte(\"true\"), _type: Boolean, expected: true},\n\t\t{name: \"invalid true\", data: []byte(\"tru\"), _type: Unknown, errExpected: true},\n\t\t{name: \"invalid false\", data: []byte(\"fals\"), _type: Unknown, errExpected: true},\n\t\t{name: \"false\", data: []byte(\"false\"), _type: Boolean, expected: false},\n\t\t{name: \"e1\", data: []byte(\"e1\"), _type: Unknown, errExpected: true},\n\t\t{name: \"1a\", data: []byte(\"1a\"), _type: Unknown, errExpected: true},\n\t\t{name: \"string error\", data: []byte(\"\\\"foo\\nbar\\\"\"), _type: String, errExpected: true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcurr := \u0026Node{\n\t\t\t\tdata:     tt.data,\n\t\t\t\tnodeType: tt._type,\n\t\t\t\tborders:  [2]int{0, len(tt.data)},\n\t\t\t}\n\n\t\t\tgot, err := curr.Value()\n\t\t\tif err != nil {\n\t\t\t\tif !tt.errExpected {\n\t\t\t\t\tt.Errorf(\"%s error = %v, expect error %v\", tt.name, err, tt.errExpected)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"%s got = %v, want %v\", tt.name, got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_Delete(t *testing.T) {\n\troot := Must(Unmarshal([]byte(`{\"foo\":\"bar\"}`)))\n\tif err := root.Delete(); err != nil {\n\t\tt.Errorf(\"Delete returns error: %v\", err)\n\t}\n\n\tif value, err := Marshal(root); err != nil {\n\t\tt.Errorf(\"Marshal returns error: %v\", err)\n\t} else if string(value) != `{\"foo\":\"bar\"}` {\n\t\tt.Errorf(\"Marshal returns wrong value: %s\", string(value))\n\t}\n\n\tfoo := root.MustKey(\"foo\")\n\tif err := foo.Delete(); err != nil {\n\t\tt.Errorf(\"Delete returns error while handling foo: %v\", err)\n\t}\n\n\tif value, err := Marshal(root); err != nil {\n\t\tt.Errorf(\"Marshal returns error: %v\", err)\n\t} else if string(value) != `{}` {\n\t\tt.Errorf(\"Marshal returns wrong value: %s\", string(value))\n\t}\n\n\tif value, err := Marshal(foo); err != nil {\n\t\tt.Errorf(\"Marshal returns error: %v\", err)\n\t} else if string(value) != `\"bar\"` {\n\t\tt.Errorf(\"Marshal returns wrong value: %s\", string(value))\n\t}\n\n\tif foo.prev != nil {\n\t\tt.Errorf(\"foo.prev should be nil\")\n\t}\n}\n\nfunc TestNode_ObjectNode(t *testing.T) {\n\tobjs := map[string]*Node{\n\t\t\"key1\": NullNode(\"null\"),\n\t\t\"key2\": NumberNode(\"answer\", 42),\n\t\t\"key3\": StringNode(\"string\", \"foobar\"),\n\t\t\"key4\": BoolNode(\"bool\", true),\n\t}\n\n\tnode := ObjectNode(\"test\", objs)\n\n\tif len(node.next) != len(objs) {\n\t\tt.Errorf(\"ObjectNode: want %v got %v\", len(objs), len(node.next))\n\t}\n\n\tfor k, v := range objs {\n\t\tif node.next[k] == nil {\n\t\t\tt.Errorf(\"ObjectNode: want %v got %v\", v, node.next[k])\n\t\t}\n\t}\n}\n\nfunc TestNode_AppendObject(t *testing.T) {\n\tif err := Must(Unmarshal([]byte(`{\"foo\":\"bar\",\"baz\":null}`))).AppendObject(\"biz\", NullNode(\"\")); err != nil {\n\t\tt.Errorf(\"AppendArray should return error\")\n\t}\n\n\troot := Must(Unmarshal([]byte(`{\"foo\":\"bar\"}`)))\n\tif err := root.AppendObject(\"baz\", NullNode(\"\")); err != nil {\n\t\tt.Errorf(\"AppendObject should not return error: %s\", err)\n\t}\n\n\tif value, err := Marshal(root); err != nil {\n\t\tt.Errorf(\"Marshal returns error: %v\", err)\n\t} else if isSameObject(string(value), `\"{\"foo\":\"bar\",\"baz\":null}\"`) {\n\t\tt.Errorf(\"Marshal returns wrong value: %s\", string(value))\n\t}\n\n\t// FIXME: this may fail if execute test in more than 3 times in a row.\n\tif err := root.AppendObject(\"biz\", NumberNode(\"\", 42)); err != nil {\n\t\tt.Errorf(\"AppendObject returns error: %v\", err)\n\t}\n\n\tval, err := Marshal(root)\n\tif err != nil {\n\t\tt.Errorf(\"Marshal returns error: %v\", err)\n\t}\n\n\t// FIXME: this may fail if execute test in more than 3 times in a row.\n\tif isSameObject(string(val), `\"{\"foo\":\"bar\",\"baz\":null,\"biz\":42}\"`) {\n\t\tt.Errorf(\"Marshal returns wrong value: %s\", string(val))\n\t}\n}\n\nfunc TestNode_ArrayNode(t *testing.T) {\n\tarr := []*Node{\n\t\tNullNode(\"nil\"),\n\t\tNumberNode(\"num\", 42),\n\t\tStringNode(\"str\", \"foobar\"),\n\t\tBoolNode(\"bool\", true),\n\t}\n\n\tnode := ArrayNode(\"test\", arr)\n\n\tif len(node.next) != len(arr) {\n\t\tt.Errorf(\"ArrayNode: want %v got %v\", len(arr), len(node.next))\n\t}\n\n\tfor i, v := range arr {\n\t\tif node.next[strconv.Itoa(i)] == nil {\n\t\t\tt.Errorf(\"ArrayNode: want %v got %v\", v, node.next[strconv.Itoa(i)])\n\t\t}\n\t}\n}\n\nfunc TestNode_AppendArray(t *testing.T) {\n\tif err := Must(Unmarshal([]byte(`[{\"foo\":\"bar\"}]`))).AppendArray(NullNode(\"\")); err != nil {\n\t\tt.Errorf(\"should return error\")\n\t}\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\tif value, err := Marshal(root); err != nil {\n\t\tt.Errorf(\"Marshal returns error: %v\", err)\n\t} else if string(value) != `[{\"foo\":\"bar\"},null]` {\n\t\tt.Errorf(\"Marshal returns wrong value: %s\", string(value))\n\t}\n\n\tif err := root.AppendArray(\n\t\tNumberNode(\"\", 1),\n\t\tStringNode(\"\", \"foo\"),\n\t\tMust(Unmarshal([]byte(`[0,1,null,true,\"example\"]`))),\n\t\tMust(Unmarshal([]byte(`{\"foo\": true, \"bar\": null, \"baz\": 123}`))),\n\t); err != nil {\n\t\tt.Errorf(\"AppendArray returns error: %v\", err)\n\t}\n\n\tif value, err := Marshal(root); err != nil {\n\t\tt.Errorf(\"Marshal returns error: %v\", err)\n\t} else if string(value) != `[{\"foo\":\"bar\"},null,1,\"foo\",[0,1,null,true,\"example\"],{\"foo\": true, \"bar\": null, \"baz\": 123}]` {\n\t\tt.Errorf(\"Marshal returns wrong value: %s\", string(value))\n\t}\n}\n\n/******** value getter ********/\n\nfunc TestNode_GetBool(t *testing.T) {\n\troot, err := Unmarshal([]byte(`true`))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err.Error())\n\t\treturn\n\t}\n\n\tvalue, err := root.GetBool()\n\tif err != nil {\n\t\tt.Errorf(\"Error on root.GetBool(): %s\", err.Error())\n\t}\n\n\tif !value {\n\t\tt.Errorf(\"root.GetBool() is corrupted\")\n\t}\n}\n\nfunc TestNode_GetBool_NotSucceed(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"nil node\", (*Node)(nil)},\n\t\t{\"literally null node\", NullNode(\"\")},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif _, err := tt.node.GetBool(); err == nil {\n\t\t\t\tt.Errorf(\"%s should be an error\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_IsBool(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"true\", BoolNode(\"\", true)},\n\t\t{\"false\", BoolNode(\"\", false)},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif !tt.node.IsBool() {\n\t\t\t\tt.Errorf(\"%s should be a bool\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_IsBool_With_Unmarshal(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tjson []byte\n\t\twant bool\n\t}{\n\t\t{\"true\", []byte(\"true\"), true},\n\t\t{\"false\", []byte(\"false\"), true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\troot, err := Unmarshal(tt.json)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Error on Unmarshal(): %s\", err.Error())\n\t\t\t}\n\n\t\t\tif root.IsBool() != tt.want {\n\t\t\t\tt.Errorf(\"%s should be a bool\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nvar nullJson = []byte(`null`)\n\nfunc TestNode_GetNull(t *testing.T) {\n\troot, err := Unmarshal(nullJson)\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err.Error())\n\t}\n\n\tvalue, err := root.GetNull()\n\tif err != nil {\n\t\tt.Errorf(\"error occurred while getting null, %s\", err)\n\t}\n\n\tif value != nil {\n\t\tt.Errorf(\"value is not matched. expected: nil, got: %v\", value)\n\t}\n}\n\nfunc TestNode_GetNull_NotSucceed(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"nil node\", (*Node)(nil)},\n\t\t{\"number node is null\", NumberNode(\"\", 42)},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif _, err := tt.node.GetNull(); err == nil {\n\t\t\t\tt.Errorf(\"%s should be an error\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_MustNull(t *testing.T) {\n\troot, err := Unmarshal(nullJson)\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err.Error())\n\t}\n\n\tvalue := root.MustNull()\n\tif value != nil {\n\t\tt.Errorf(\"value is not matched. expected: nil, got: %v\", value)\n\t}\n}\n\nfunc TestNode_GetNumeric_Float(t *testing.T) {\n\troot, err := Unmarshal([]byte(`123.456`))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n\t\treturn\n\t}\n\n\tvalue, err := root.GetNumeric()\n\tif err != nil {\n\t\tt.Errorf(\"Error on root.GetNumeric(): %s\", err)\n\t}\n\n\tif value != float64(123.456) {\n\t\tt.Errorf(ufmt.Sprintf(\"value is not matched. expected: 123.456, got: %v\", value))\n\t}\n}\n\nfunc TestNode_GetNumeric_Scientific_Notation(t *testing.T) {\n\troot, err := Unmarshal([]byte(`1e3`))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n\t\treturn\n\t}\n\n\tvalue, err := root.GetNumeric()\n\tif err != nil {\n\t\tt.Errorf(\"Error on root.GetNumeric(): %s\", err)\n\t}\n\n\tif value != float64(1000) {\n\t\tt.Errorf(ufmt.Sprintf(\"value is not matched. expected: 1000, got: %v\", value))\n\t}\n}\n\nfunc TestNode_GetNumeric_With_Unmarshal(t *testing.T) {\n\troot, err := Unmarshal([]byte(`123`))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n\t\treturn\n\t}\n\n\tvalue, err := root.GetNumeric()\n\tif err != nil {\n\t\tt.Errorf(\"Error on root.GetNumeric(): %s\", err)\n\t}\n\n\tif value != float64(123) {\n\t\tt.Errorf(ufmt.Sprintf(\"value is not matched. expected: 123, got: %v\", value))\n\t}\n}\n\nfunc TestNode_GetNumeric_NotSucceed(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"nil node\", (*Node)(nil)},\n\t\t{\"null node\", NullNode(\"\")},\n\t\t{\"string node\", StringNode(\"\", \"123\")},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif _, err := tt.node.GetNumeric(); err == nil {\n\t\t\t\tt.Errorf(\"%s should be an error\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_GetString(t *testing.T) {\n\troot, err := Unmarshal([]byte(`\"123foobar 3456\"`))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n\t}\n\n\tvalue, err := root.GetString()\n\tif err != nil {\n\t\tt.Errorf(\"Error on root.GetString(): %s\", err)\n\t}\n\n\tif value != \"123foobar 3456\" {\n\t\tt.Errorf(ufmt.Sprintf(\"value is not matched. expected: 123, got: %s\", value))\n\t}\n}\n\nfunc TestNode_GetString_NotSucceed(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"nil node\", (*Node)(nil)},\n\t\t{\"null node\", NullNode(\"\")},\n\t\t{\"number node\", NumberNode(\"\", 123)},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif _, err := tt.node.GetString(); err == nil {\n\t\t\t\tt.Errorf(\"%s should be an error\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_MustString(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tdata []byte\n\t}{\n\t\t{\"foo\", []byte(`\"foo\"`)},\n\t\t{\"foo bar\", []byte(`\"foo bar\"`)},\n\t\t{\"\", []byte(`\"\"`)},\n\t\t{\"안녕하세요\", []byte(`\"안녕하세요\"`)},\n\t\t{\"こんにちは\", []byte(`\"こんにちは\"`)},\n\t\t{\"你好\", []byte(`\"你好\"`)},\n\t\t{\"one \\\"encoded\\\" string\", []byte(`\"one \\\"encoded\\\" string\"`)},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\troot, err := Unmarshal(tt.data)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n\t\t\t}\n\n\t\t\tvalue := root.MustString()\n\t\t\tif value != tt.name {\n\t\t\t\tt.Errorf(\"value is not matched. expected: %s, got: %s\", tt.name, value)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_Array(t *testing.T) {\n\troot, err := Unmarshal([]byte(\" [1,[\\\"1\\\",[1,[1,2,3]]]]\\r\\n\"))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal: %s\", err.Error())\n\t}\n\n\tif root == nil {\n\t\tt.Errorf(\"Error on Unmarshal: root is nil\")\n\t}\n\n\tif root.Type() != Array {\n\t\tt.Errorf(\"Error on Unmarshal: wrong type\")\n\t}\n\n\tarray, err := root.GetArray()\n\tif err != nil {\n\t\tt.Errorf(\"error occurred while getting array, %s\", err)\n\t} else if len(array) != 2 {\n\t\tt.Errorf(\"expected 2 elements, got %d\", len(array))\n\t} else if val, err := array[0].GetNumeric(); err != nil {\n\t\tt.Errorf(\"value of array[0] is not numeric. got: %v\", array[0].value)\n\t} else if val != 1 {\n\t\tt.Errorf(\"Error on array[0].GetNumeric(): expected to be '1', got: %v\", val)\n\t} else if val, err := array[1].GetArray(); err != nil {\n\t\tt.Errorf(\"error occurred while getting array, %s\", err.Error())\n\t} else if len(val) != 2 {\n\t\tt.Errorf(\"Error on array[1].GetArray(): expected 2 elements, got %d\", len(val))\n\t} else if el, err := val[0].GetString(); err != nil {\n\t\tt.Errorf(\"error occurred while getting string, %s\", err.Error())\n\t} else if el != \"1\" {\n\t\tt.Errorf(\"Error on val[0].GetString(): expected to be '1', got: %s\", el)\n\t}\n}\n\nvar sampleArr = []byte(`[-1, 2, 3, 4, 5, 6]`)\n\nfunc TestNode_GetArray(t *testing.T) {\n\troot, err := Unmarshal(sampleArr)\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n\t\treturn\n\t}\n\n\tarray, err := root.GetArray()\n\tif err != nil {\n\t\tt.Errorf(\"Error on root.GetArray(): %s\", err)\n\t}\n\n\tif len(array) != 6 {\n\t\tt.Errorf(ufmt.Sprintf(\"length is not matched. expected: 3, got: %d\", len(array)))\n\t}\n\n\tfor i, node := range array {\n\t\tfor j, val := range []int{-1, 2, 3, 4, 5, 6} {\n\t\t\tif i == j {\n\t\t\t\tif v, err := node.GetNumeric(); err != nil {\n\t\t\t\t\tt.Errorf(ufmt.Sprintf(\"Error on node.GetNumeric(): %s\", err))\n\t\t\t\t} else if v != float64(val) {\n\t\t\t\t\tt.Errorf(ufmt.Sprintf(\"value is not matched. expected: %d, got: %v\", val, v))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestNode_GetArray_NotSucceed(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"nil node\", (*Node)(nil)},\n\t\t{\"null node\", NullNode(\"\")},\n\t\t{\"number node\", NumberNode(\"\", 123)},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif _, err := tt.node.GetArray(); err == nil {\n\t\t\t\tt.Errorf(\"%s should be an error\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_IsArray(t *testing.T) {\n\troot, err := Unmarshal(sampleArr)\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n\t\treturn\n\t}\n\n\tif root.Type() != Array {\n\t\tt.Errorf(ufmt.Sprintf(\"Must be an array. got: %s\", root.Type().String()))\n\t}\n}\n\nfunc TestNode_ArrayEach(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tjson     string\n\t\texpected []int\n\t}{\n\t\t{\n\t\t\tname:     \"empty array\",\n\t\t\tjson:     `[]`,\n\t\t\texpected: []int{},\n\t\t},\n\t\t{\n\t\t\tname:     \"single element\",\n\t\t\tjson:     `[42]`,\n\t\t\texpected: []int{42},\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple elements\",\n\t\t\tjson:     `[1, 2, 3, 4, 5]`,\n\t\t\texpected: []int{1, 2, 3, 4, 5},\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple elements but all values are same\",\n\t\t\tjson:     `[1, 1, 1, 1, 1]`,\n\t\t\texpected: []int{1, 1, 1, 1, 1},\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple elements with non-numeric values\",\n\t\t\tjson:     `[\"a\", \"b\", \"c\", \"d\", \"e\"]`,\n\t\t\texpected: []int{},\n\t\t},\n\t\t{\n\t\t\tname:     \"non-array node\",\n\t\t\tjson:     `{\"not\": \"an array\"}`,\n\t\t\texpected: []int{},\n\t\t},\n\t\t{\n\t\t\tname:     \"array containing numeric and non-numeric elements\",\n\t\t\tjson:     `[\"1\", 2, 3, \"4\", 5, \"6\"]`,\n\t\t\texpected: []int{2, 3, 5},\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\troot, err := Unmarshal([]byte(tc.json))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Unmarshal failed: %v\", err)\n\t\t\t}\n\n\t\t\tvar result []int // callback result\n\t\t\troot.ArrayEach(func(index int, element *Node) {\n\t\t\t\tif val, err := strconv.Atoi(element.String()); err == nil {\n\t\t\t\t\tresult = append(result, val)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tif len(result) != len(tc.expected) {\n\t\t\t\tt.Errorf(\"%s: expected %d elements, got %d\", tc.name, len(tc.expected), len(result))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tfor i, val := range result {\n\t\t\t\tif val != tc.expected[i] {\n\t\t\t\t\tt.Errorf(\"%s: expected value at index %d to be %d, got %d\", tc.name, i, tc.expected[i], val)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_Key(t *testing.T) {\n\troot, err := Unmarshal([]byte(`{\"foo\": true, \"bar\": null, \"baz\": 123, \"biz\": [1,2,3]}`))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err.Error())\n\t}\n\n\tobj := root.MustObject()\n\tfor key, node := range obj {\n\t\tif key != node.Key() {\n\t\t\tt.Errorf(\"Key() = %v, want %v\", node.Key(), key)\n\t\t}\n\t}\n\n\tkeys := []string{\"foo\", \"bar\", \"baz\", \"biz\"}\n\tfor _, key := range keys {\n\t\tif obj[key].Key() != key {\n\t\t\tt.Errorf(\"Key() = %v, want %v\", obj[key].Key(), key)\n\t\t}\n\t}\n\n\t// TODO: resolve stack overflow\n\t// if root.MustKey(\"foo\").Clone().Key() != \"\" {\n\t// \tt.Errorf(\"wrong key found for cloned key\")\n\t// }\n\n\tif (*Node)(nil).Key() != \"\" {\n\t\tt.Errorf(\"wrong key found for nil node\")\n\t}\n}\n\nfunc TestNode_Size(t *testing.T) {\n\troot, err := Unmarshal(sampleArr)\n\tif err != nil {\n\t\tt.Errorf(\"error occurred while unmarshal\")\n\t}\n\n\tsize := root.Size()\n\tif size != 6 {\n\t\tt.Errorf(ufmt.Sprintf(\"Size() must be 6. got: %v\", size))\n\t}\n\n\tif (*Node)(nil).Size() != 0 {\n\t\tt.Errorf(ufmt.Sprintf(\"Size() must be 0. got: %v\", (*Node)(nil).Size()))\n\t}\n}\n\nfunc TestNode_Index(t *testing.T) {\n\troot, err := Unmarshal([]byte(`[1, 2, 3, 4, 5, 6]`))\n\tif err != nil {\n\t\tt.Error(\"error occurred while unmarshal\")\n\t}\n\n\tarr := root.MustArray()\n\tfor i, node := range arr {\n\t\tif i != node.Index() {\n\t\t\tt.Errorf(ufmt.Sprintf(\"Index() must be nil. got: %v\", i))\n\t\t}\n\t}\n}\n\nfunc TestNode_Index_NotSucceed(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tnode *Node\n\t\twant int\n\t}{\n\t\t{\"nil node\", (*Node)(nil), -1},\n\t\t{\"null node\", NullNode(\"\"), -1},\n\t\t{\"object node\", ObjectNode(\"\", nil), -1},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.node.Index(); got != tt.want {\n\t\t\t\tt.Errorf(\"Index() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_GetIndex(t *testing.T) {\n\troot := Must(Unmarshal([]byte(`[1, 2, 3, 4, 5, 6]`)))\n\texpected := []int{1, 2, 3, 4, 5, 6}\n\n\tif len(expected) != root.Size() {\n\t\tt.Errorf(\"length is not matched. expected: %d, got: %d\", len(expected), root.Size())\n\t}\n\n\t// TODO: if length exceeds, stack overflow occurs. need to fix\n\tfor i, v := range expected {\n\t\tval, err := root.GetIndex(i)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"error occurred while getting index %d, %s\", i, err)\n\t\t}\n\n\t\tif val.MustNumeric() != float64(v) {\n\t\t\tt.Errorf(\"value is not matched. expected: %d, got: %v\", v, val.MustNumeric())\n\t\t}\n\t}\n}\n\nfunc TestNode_GetIndex_InputIndex_Exceed_Original_Node_Index(t *testing.T) {\n\troot, err := Unmarshal([]byte(`[1, 2, 3, 4, 5, 6]`))\n\tif err != nil {\n\t\tt.Errorf(\"error occurred while unmarshal\")\n\t}\n\n\t_, err = root.GetIndex(10)\n\tif err == nil {\n\t\tt.Errorf(\"GetIndex should return error\")\n\t}\n}\n\nfunc TestNode_DeleteIndex(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\texpected string\n\t\tindex    int\n\t\tok       bool\n\t}{\n\t\t{`null`, ``, 0, false},\n\t\t{`1`, ``, 0, false},\n\t\t{`{}`, ``, 0, false},\n\t\t{`{\"foo\":\"bar\"}`, ``, 0, false},\n\t\t{`true`, ``, 0, false},\n\t\t{`[]`, ``, 0, false},\n\t\t{`[]`, ``, -1, false},\n\t\t{`[1]`, `[]`, 0, true},\n\t\t{`[{}]`, `[]`, 0, true},\n\t\t{`[{}, [], 42]`, `[{}, []]`, -1, true},\n\t\t{`[{}, [], 42]`, `[[], 42]`, 0, true},\n\t\t{`[{}, [], 42]`, `[{}, 42]`, 1, true},\n\t\t{`[{}, [], 42]`, `[{}, []]`, 2, true},\n\t\t{`[{}, [], 42]`, ``, 10, false},\n\t\t{`[{}, [], 42]`, ``, -10, false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\troot := Must(Unmarshal([]byte(tt.name)))\n\t\t\terr := root.DeleteIndex(tt.index)\n\t\t\tif err != nil \u0026\u0026 tt.ok {\n\t\t\t\tt.Errorf(\"DeleteIndex returns error: %v\", err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_GetKey(t *testing.T) {\n\troot, err := Unmarshal([]byte(`{\"foo\": true, \"bar\": null}`))\n\tif err != nil {\n\t\tt.Error(\"error occurred while unmarshal\")\n\t}\n\n\tvalue, err := root.GetKey(\"foo\")\n\tif err != nil {\n\t\tt.Errorf(\"error occurred while getting key, %s\", err)\n\t}\n\n\tif value.MustBool() != true {\n\t\tt.Errorf(\"value is not matched. expected: true, got: %v\", value.MustBool())\n\t}\n\n\tvalue, err = root.GetKey(\"bar\")\n\tif err != nil {\n\t\tt.Errorf(\"error occurred while getting key, %s\", err)\n\t}\n\n\t_, err = root.GetKey(\"baz\")\n\tif err == nil {\n\t\tt.Errorf(\"key baz is not exist. must be failed\")\n\t}\n\n\tif value.MustNull() != nil {\n\t\tt.Errorf(\"value is not matched. expected: nil, got: %v\", value.MustNull())\n\t}\n}\n\nfunc TestNode_GetKey_NotSucceed(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"nil node\", (*Node)(nil)},\n\t\t{\"null node\", NullNode(\"\")},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif _, err := tt.node.GetKey(\"\"); err == nil {\n\t\t\t\tt.Errorf(\"%s should be an error\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_GetUniqueKeyList(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tjson     string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname:     \"simple foo/bar\",\n\t\t\tjson:     `{\"foo\": true, \"bar\": null}`,\n\t\t\texpected: []string{\"foo\", \"bar\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"empty object\",\n\t\t\tjson:     `{}`,\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"nested object\",\n\t\t\tjson: `{\n\t\t\t\t\"outer\": {\n\t\t\t\t\t\"inner\": {\n\t\t\t\t\t\t\"key\": \"value\"\n\t\t\t\t\t},\n\t\t\t\t\t\"array\": [1, 2, 3]\n\t\t\t\t},\n\t\t\t\t\"another\": \"item\"\n\t\t\t}`,\n\t\t\texpected: []string{\"outer\", \"inner\", \"key\", \"array\", \"another\"},\n\t\t},\n\t\t{\n\t\t\tname: \"complex object\",\n\t\t\tjson: `{\n\t\t\t\t\"Image\": {\n\t\t\t\t\t\"Width\": 800,\n\t\t\t\t\t\"Height\": 600,\n\t\t\t\t\t\"Title\": \"View from 15th Floor\",\n\t\t\t\t\t\"Thumbnail\": {\n\t\t\t\t\t\t\"Url\": \"http://www.example.com/image/481989943\",\n\t\t\t\t\t\t\"Height\": 125,\n\t\t\t\t\t\t\"Width\": 100\n\t\t\t\t\t},\n\t\t\t\t\t\"Animated\": false,\n\t\t\t\t\t\"IDs\": [116, 943, 234, 38793]\n\t\t\t\t}\n\t\t\t}`,\n\t\t\texpected: []string{\"Image\", \"Width\", \"Height\", \"Title\", \"Thumbnail\", \"Url\", \"Animated\", \"IDs\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\troot, err := Unmarshal([]byte(tt.json))\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"error occurred while unmarshal\")\n\t\t\t}\n\n\t\t\tvalue := root.UniqueKeyLists()\n\t\t\tif len(value) != len(tt.expected) {\n\t\t\t\tt.Errorf(\"%s length must be %v. got: %v. retrieved keys: %s\", tt.name, len(tt.expected), len(value), value)\n\t\t\t}\n\n\t\t\tfor _, key := range value {\n\t\t\t\tif !contains(tt.expected, key) {\n\t\t\t\t\tt.Errorf(\"EachKey() must be in %v. got: %v\", tt.expected, key)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TODO: resolve stack overflow\nfunc TestNode_IsEmpty(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tnode     *Node\n\t\texpected bool\n\t}{\n\t\t{\"nil node\", (*Node)(nil), false}, // nil node is not empty.\n\t\t// {\"null node\", NullNode(\"\"), true},\n\t\t{\"empty object\", ObjectNode(\"\", nil), true},\n\t\t{\"empty array\", ArrayNode(\"\", nil), true},\n\t\t{\"non-empty object\", ObjectNode(\"\", map[string]*Node{\"foo\": BoolNode(\"foo\", true)}), false},\n\t\t{\"non-empty array\", ArrayNode(\"\", []*Node{BoolNode(\"0\", true)}), false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.node.Empty(); got != tt.expected {\n\t\t\t\tt.Errorf(\"%s = %v, want %v\", tt.name, got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_Index_EmptyList(t *testing.T) {\n\troot, err := Unmarshal([]byte(`[]`))\n\tif err != nil {\n\t\tt.Errorf(\"error occurred while unmarshal\")\n\t}\n\n\tarray := root.MustArray()\n\tfor i, node := range array {\n\t\tif i != node.Index() {\n\t\t\tt.Errorf(ufmt.Sprintf(\"Index() must be nil. got: %v\", i))\n\t\t}\n\t}\n}\n\nfunc TestNode_GetObject(t *testing.T) {\n\troot, err := Unmarshal([]byte(`{\"foo\": true,\"bar\": null}`))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err.Error())\n\t\treturn\n\t}\n\n\tvalue, err := root.GetObject()\n\tif err != nil {\n\t\tt.Errorf(\"Error on root.GetObject(): %s\", err.Error())\n\t}\n\n\tif _, ok := value[\"foo\"]; !ok {\n\t\tt.Errorf(\"root.GetObject() is corrupted: foo\")\n\t}\n\n\tif _, ok := value[\"bar\"]; !ok {\n\t\tt.Errorf(\"root.GetObject() is corrupted: bar\")\n\t}\n}\n\nfunc TestNode_GetObject_NotSucceed(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"nil node\", (*Node)(nil)},\n\t\t{\"get object from null node\", NullNode(\"\")},\n\t\t{\"not object node\", NumberNode(\"\", 123)},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif _, err := tt.node.GetObject(); err == nil {\n\t\t\t\tt.Errorf(\"%s should be an error\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_ObjectEach(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tjson     string\n\t\texpected map[string]int\n\t}{\n\t\t{\n\t\t\tname:     \"empty object\",\n\t\t\tjson:     `{}`,\n\t\t\texpected: make(map[string]int),\n\t\t},\n\t\t{\n\t\t\tname:     \"single key-value pair\",\n\t\t\tjson:     `{\"key\": 42}`,\n\t\t\texpected: map[string]int{\"key\": 42},\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple key-value pairs\",\n\t\t\tjson:     `{\"one\": 1, \"two\": 2, \"three\": 3}`,\n\t\t\texpected: map[string]int{\"one\": 1, \"two\": 2, \"three\": 3},\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple key-value pairs with some non-numeric values\",\n\t\t\tjson:     `{\"one\": 1, \"two\": \"2\", \"three\": 3, \"four\": \"4\"}`,\n\t\t\texpected: map[string]int{\"one\": 1, \"three\": 3},\n\t\t},\n\t\t{\n\t\t\tname:     \"non-object node\",\n\t\t\tjson:     `[\"not\", \"an\", \"object\"]`,\n\t\t\texpected: make(map[string]int),\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\troot, err := Unmarshal([]byte(tc.json))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Unmarshal failed: %v\", err)\n\t\t\t}\n\n\t\t\tresult := make(map[string]int)\n\t\t\troot.ObjectEach(func(key string, value *Node) {\n\t\t\t\t// extract integer values from the object\n\t\t\t\tif val, err := strconv.Atoi(value.String()); err == nil {\n\t\t\t\t\tresult[key] = val\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tif len(result) != len(tc.expected) {\n\t\t\t\tt.Errorf(\"%s: expected %d key-value pairs, got %d\", tc.name, len(tc.expected), len(result))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tfor key, val := range tc.expected {\n\t\t\t\tif result[key] != val {\n\t\t\t\t\tt.Errorf(\"%s: expected value for key %s to be %d, got %d\", tc.name, key, val, result[key])\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Example_TestNode_Must() {\n\tdata := []byte(`{\n        \"Image\": {\n            \"Width\":  800,\n            \"Height\": 600,\n            \"Title\":  \"View from 15th Floor\",\n            \"Thumbnail\": {\n                \"Url\":    \"http://www.example.com/image/481989943\",\n                \"Height\": 125,\n                \"Width\":  100\n            },\n            \"Animated\" : false,\n            \"IDs\": [116, 943, 234, 38793]\n        }\n    }`)\n\n\troot := Must(Unmarshal(data))\n\tif root.Size() != 1 {\n\t\tufmt.Printf(\"root.Size() must be 1. got: %v\\n\", root.Size())\n\t\treturn\n\t}\n\n\tufmt.Printf(\"Object has %d inheritors inside\\n\", root.Size())\n\t// Output:\n\t// Object has 1 inheritors inside\n}\n\n// Calculate AVG price from different types of objects, JSON from: https://goessner.net/articles/JsonPath/index.html#e3\nfunc TestExampleUnmarshal(t *testing.T) {\n\tdata := []byte(`{ \"store\": {\n    \"book\": [ \n      { \"category\": \"reference\",\n        \"author\": \"Nigel Rees\",\n        \"title\": \"Sayings of the Century\",\n        \"price\": 8.95\n      },\n      { \"category\": \"fiction\",\n        \"author\": \"Evelyn Waugh\",\n        \"title\": \"Sword of Honour\",\n        \"price\": 12.99\n      },\n      { \"category\": \"fiction\",\n        \"author\": \"Herman Melville\",\n        \"title\": \"Moby Dick\",\n        \"isbn\": \"0-553-21311-3\",\n        \"price\": 8.99\n      },\n      { \"category\": \"fiction\",\n        \"author\": \"J. R. R. Tolkien\",\n        \"title\": \"The Lord of the Rings\",\n        \"isbn\": \"0-395-19395-8\",\n        \"price\": 22.99\n      }\n    ],\n    \"bicycle\": { \"color\": \"red\",\n      \"price\": 19.95\n    },\n    \"tools\": null\n  }\n}`)\n\n\troot, err := Unmarshal(data)\n\tif err != nil {\n\t\tt.Errorf(\"error occurred when unmarshal\")\n\t}\n\n\tstore := root.MustKey(\"store\").MustObject()\n\n\tvar prices float64\n\tsize := 0\n\tfor _, objects := range store {\n\t\tif objects.IsArray() \u0026\u0026 objects.Size() \u003e 0 {\n\t\t\tsize += objects.Size()\n\t\t\tfor _, object := range objects.MustArray() {\n\t\t\t\tprices += object.MustKey(\"price\").MustNumeric()\n\t\t\t}\n\t\t} else if objects.IsObject() \u0026\u0026 objects.HasKey(\"price\") {\n\t\t\tsize++\n\t\t\tprices += objects.MustKey(\"price\").MustNumeric()\n\t\t}\n\t}\n\n\tresult := int(prices / float64(size))\n\tufmt.Sprintf(\"AVG price: %d\", result)\n}\n\nfunc TestNode_ExampleMust_panic(t *testing.T) {\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Errorf(\"The code did not panic\")\n\t\t}\n\t}()\n\tdata := []byte(`{]`)\n\troot := Must(Unmarshal(data))\n\tufmt.Sprintf(\"Object has %d inheritors inside\", root.Size())\n}\n\nfunc TestNode_Path(t *testing.T) {\n\tdata := []byte(`{\n        \"Image\": {\n            \"Width\":  800,\n            \"Height\": 600,\n            \"Title\":  \"View from 15th Floor\",\n            \"Thumbnail\": {\n                \"Url\":    \"http://www.example.com/image/481989943\",\n                \"Height\": 125,\n                \"Width\":  100\n            },\n            \"Animated\" : false,\n            \"IDs\": [116, 943, 234, 38793]\n          }\n      }`)\n\n\troot, err := Unmarshal(data)\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err.Error())\n\t\treturn\n\t}\n\n\tif root.Path() != \"$\" {\n\t\tt.Errorf(\"Wrong root.Path()\")\n\t}\n\n\telement := root.MustKey(\"Image\").MustKey(\"Thumbnail\").MustKey(\"Url\")\n\tif element.Path() != \"$['Image']['Thumbnail']['Url']\" {\n\t\tt.Errorf(\"Wrong path found: %s\", element.Path())\n\t}\n\n\tif (*Node)(nil).Path() != \"\" {\n\t\tt.Errorf(\"Wrong (nil).Path()\")\n\t}\n}\n\nfunc TestNode_Path2(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tnode *Node\n\t\twant string\n\t}{\n\t\t{\n\t\t\tname: \"Node with key\",\n\t\t\tnode: \u0026Node{\n\t\t\t\tprev: \u0026Node{},\n\t\t\t\tkey:  func() *string { s := \"key\"; return \u0026s }(),\n\t\t\t},\n\t\t\twant: \"$['key']\",\n\t\t},\n\t\t{\n\t\t\tname: \"Node with index\",\n\t\t\tnode: \u0026Node{\n\t\t\t\tprev:  \u0026Node{},\n\t\t\t\tindex: func() *int { i := 1; return \u0026i }(),\n\t\t\t},\n\t\t\twant: \"$[1]\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.node.Path(); got != tt.want {\n\t\t\t\tt.Errorf(\"Path() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_Root(t *testing.T) {\n\troot := \u0026Node{}\n\tchild := \u0026Node{prev: root}\n\tgrandChild := \u0026Node{prev: child}\n\n\ttests := []struct {\n\t\tname string\n\t\tnode *Node\n\t\twant *Node\n\t}{\n\t\t{\n\t\t\tname: \"Root node\",\n\t\t\tnode: root,\n\t\t\twant: root,\n\t\t},\n\t\t{\n\t\t\tname: \"Child node\",\n\t\t\tnode: child,\n\t\t\twant: root,\n\t\t},\n\t\t{\n\t\t\tname: \"Grandchild node\",\n\t\t\tnode: grandChild,\n\t\t\twant: root,\n\t\t},\n\t\t{\n\t\t\tname: \"Node is nil\",\n\t\t\tnode: nil,\n\t\t\twant: nil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.node.root(); got != tt.want {\n\t\t\t\tt.Errorf(\"root() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc contains(slice []string, item string) bool {\n\tfor _, a := range slice {\n\t\tif a == item {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// ignore the sequence of keys by ordering them.\n// need to avoid import encoding/json and reflect package.\n// because gno does not support them for now.\n// TODO: use encoding/json to compare the result after if possible in gno.\nfunc isSameObject(a, b string) bool {\n\taPairs := strings.Split(strings.Trim(a, \"{}\"), \",\")\n\tbPairs := strings.Split(strings.Trim(b, \"{}\"), \",\")\n\n\taMap := make(map[string]string)\n\tbMap := make(map[string]string)\n\tfor _, pair := range aPairs {\n\t\tkv := strings.Split(pair, \":\")\n\t\tkey := strings.Trim(kv[0], `\"`)\n\t\tvalue := strings.Trim(kv[1], `\"`)\n\t\taMap[key] = value\n\t}\n\tfor _, pair := range bPairs {\n\t\tkv := strings.Split(pair, \":\")\n\t\tkey := strings.Trim(kv[0], `\"`)\n\t\tvalue := strings.Trim(kv[1], `\"`)\n\t\tbMap[key] = value\n\t}\n\n\taKeys := make([]string, 0, len(aMap))\n\tbKeys := make([]string, 0, len(bMap))\n\tfor k := range aMap {\n\t\taKeys = append(aKeys, k)\n\t}\n\n\tfor k := range bMap {\n\t\tbKeys = append(bKeys, k)\n\t}\n\n\tsort.Strings(aKeys)\n\tsort.Strings(bKeys)\n\n\tif len(aKeys) != len(bKeys) {\n\t\treturn false\n\t}\n\n\tfor i := range aKeys {\n\t\tif aKeys[i] != bKeys[i] {\n\t\t\treturn false\n\t\t}\n\n\t\tif aMap[aKeys[i]] != bMap[bKeys[i]] {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\nfunc compareNodes(n1, n2 *Node) bool {\n\tif n1 == nil || n2 == nil {\n\t\treturn n1 == n2\n\t}\n\n\tif n1.key != n2.key {\n\t\treturn false\n\t}\n\n\tif !bytes.Equal(n1.data, n2.data) {\n\t\treturn false\n\t}\n\n\tif n1.index != n2.index {\n\t\treturn false\n\t}\n\n\tif n1.borders != n2.borders {\n\t\treturn false\n\t}\n\n\tif n1.modified != n2.modified {\n\t\treturn false\n\t}\n\n\tif n1.nodeType != n2.nodeType {\n\t\treturn false\n\t}\n\n\tif !compareNodes(n1.prev, n2.prev) {\n\t\treturn false\n\t}\n\n\tif len(n1.next) != len(n2.next) {\n\t\treturn false\n\t}\n\n\tfor k, v := range n1.next {\n\t\tif !compareNodes(v, n2.next[k]) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\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":"parser_test.gno","body":"package json\n\nimport \"testing\"\n\nfunc TestParseStringLiteral(t *testing.T) {\n\ttests := []struct {\n\t\tinput    string\n\t\texpected string\n\t\tisError  bool\n\t}{\n\t\t{`\"Hello, World!\"`, \"\\\"Hello, World!\\\"\", false},\n\t\t{`\\uFF11`, \"\\uFF11\", false},\n\t\t{`\\uFFFF`, \"\\uFFFF\", false},\n\t\t{`true`, \"true\", false},\n\t\t{`false`, \"false\", false},\n\t\t{`\\uDF00`, \"\", true},\n\t}\n\n\tfor i, tt := range tests {\n\t\ts, err := ParseStringLiteral([]byte(tt.input))\n\n\t\tif !tt.isError \u0026\u0026 err != nil {\n\t\t\tt.Errorf(\"%d. unexpected error: %s\", i, err)\n\t\t}\n\n\t\tif tt.isError \u0026\u0026 err == nil {\n\t\t\tt.Errorf(\"%d. expected error, but not error\", i)\n\t\t}\n\n\t\tif s != tt.expected {\n\t\t\tt.Errorf(\"%d. expected=%s, but actual=%s\", i, tt.expected, s)\n\t\t}\n\t}\n}\n\nfunc TestParseBoolLiteral(t *testing.T) {\n\ttests := []struct {\n\t\tinput    string\n\t\texpected bool\n\t\tisError  bool\n\t}{\n\t\t{`true`, true, false},\n\t\t{`false`, false, false},\n\t\t{`TRUE`, false, true},\n\t\t{`FALSE`, false, true},\n\t\t{`foo`, false, true},\n\t\t{`\"true\"`, false, true},\n\t\t{`\"false\"`, false, true},\n\t}\n\n\tfor i, tt := range tests {\n\t\tb, err := ParseBoolLiteral([]byte(tt.input))\n\n\t\tif !tt.isError \u0026\u0026 err != nil {\n\t\t\tt.Errorf(\"%d. unexpected error: %s\", i, err)\n\t\t}\n\n\t\tif tt.isError \u0026\u0026 err == nil {\n\t\t\tt.Errorf(\"%d. expected error, but not error\", i)\n\t\t}\n\n\t\tif b != tt.expected {\n\t\t\tt.Errorf(\"%d. expected=%t, but actual=%t\", i, tt.expected, b)\n\t\t}\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":"path_test.gno","body":"package json\n\nimport \"testing\"\n\nfunc TestParseJSONPath(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tpath     string\n\t\texpected []string\n\t}{\n\t\t{name: \"Empty string path\", path: \"\", expected: []string{}},\n\t\t{name: \"Root only path\", path: \"$\", expected: []string{\"$\"}},\n\t\t{name: \"Root with dot path\", path: \"$.\", expected: []string{\"$\"}},\n\t\t{name: \"All objects in path\", path: \"$..\", expected: []string{\"$\", \"..\"}},\n\t\t{name: \"Only children in path\", path: \"$.*\", expected: []string{\"$\", \"*\"}},\n\t\t{name: \"All objects' children in path\", path: \"$..*\", expected: []string{\"$\", \"..\", \"*\"}},\n\t\t{name: \"Simple dot notation path\", path: \"$.root.element\", expected: []string{\"$\", \"root\", \"element\"}},\n\t\t{name: \"Complex dot notation path with wildcard\", path: \"$.root.*.element\", expected: []string{\"$\", \"root\", \"*\", \"element\"}},\n\t\t{name: \"Path with array wildcard\", path: \"$.phoneNumbers[*].type\", expected: []string{\"$\", \"phoneNumbers\", \"*\", \"type\"}},\n\t\t{name: \"Path with filter expression\", path: \"$.store.book[?(@.price \u003c 10)].title\", expected: []string{\"$\", \"store\", \"book\", \"?(@.price \u003c 10)\", \"title\"}},\n\t\t{name: \"Path with formula\", path: \"$..phoneNumbers..('ty' + 'pe')\", expected: []string{\"$\", \"..\", \"phoneNumbers\", \"..\", \"('ty' + 'pe')\"}},\n\t\t{name: \"Simple bracket notation path\", path: \"$['root']['element']\", expected: []string{\"$\", \"'root'\", \"'element'\"}},\n\t\t{name: \"Complex bracket notation path with wildcard\", path: \"$['root'][*]['element']\", expected: []string{\"$\", \"'root'\", \"*\", \"'element'\"}},\n\t\t{name: \"Bracket notation path with integer index\", path: \"$['store']['book'][0]['title']\", expected: []string{\"$\", \"'store'\", \"'book'\", \"0\", \"'title'\"}},\n\t\t{name: \"Complex path with wildcard in bracket notation\", path: \"$['root'].*['element']\", expected: []string{\"$\", \"'root'\", \"*\", \"'element'\"}},\n\t\t{name: \"Mixed notation path with dot after bracket\", path: \"$.['root'].*.['element']\", expected: []string{\"$\", \"'root'\", \"*\", \"'element'\"}},\n\t\t{name: \"Mixed notation path with dot before bracket\", path: \"$['root'].*.['element']\", expected: []string{\"$\", \"'root'\", \"*\", \"'element'\"}},\n\t\t{name: \"Single character path with root\", path: \"$.a\", expected: []string{\"$\", \"a\"}},\n\t\t{name: \"Multiple characters path with root\", path: \"$.abc\", expected: []string{\"$\", \"abc\"}},\n\t\t{name: \"Multiple segments path with root\", path: \"$.a.b.c\", expected: []string{\"$\", \"a\", \"b\", \"c\"}},\n\t\t{name: \"Multiple segments path with wildcard and root\", path: \"$.a.*.c\", expected: []string{\"$\", \"a\", \"*\", \"c\"}},\n\t\t{name: \"Multiple segments path with filter and root\", path: \"$.a[?(@.b == 'c')].d\", expected: []string{\"$\", \"a\", \"?(@.b == 'c')\", \"d\"}},\n\t\t{name: \"Complex path with multiple filters\", path: \"$.a[?(@.b == 'c')].d[?(@.e == 'f')].g\", expected: []string{\"$\", \"a\", \"?(@.b == 'c')\", \"d\", \"?(@.e == 'f')\", \"g\"}},\n\t\t{name: \"Complex path with multiple filters and wildcards\", path: \"$.a[?(@.b == 'c')].*.d[?(@.e == 'f')].g\", expected: []string{\"$\", \"a\", \"?(@.b == 'c')\", \"*\", \"d\", \"?(@.e == 'f')\", \"g\"}},\n\t\t{name: \"Path with array index and root\", path: \"$.a[0].b\", expected: []string{\"$\", \"a\", \"0\", \"b\"}},\n\t\t{name: \"Path with multiple array indices and root\", path: \"$.a[0].b[1].c\", expected: []string{\"$\", \"a\", \"0\", \"b\", \"1\", \"c\"}},\n\t\t{name: \"Path with array index, wildcard and root\", path: \"$.a[0].*.c\", expected: []string{\"$\", \"a\", \"0\", \"*\", \"c\"}},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\treult, _ := ParsePath(tt.path)\n\t\t\tif !isEqualSlice(reult, tt.expected) {\n\t\t\t\tt.Errorf(\"ParsePath(%s) expected: %v, got: %v\", tt.path, tt.expected, reult)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc isEqualSlice(a, b []string) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\n\tfor i, v := range a {\n\t\tif v != b[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"pK1W7pwt/axJNRSdR+wCG0o4yQTuTsE2fAgbUlFKTWJMLBaxtrKmbNE3e1dwOLLnViCsRKEzsyqrYN5H8pwXYQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"table","path":"gno.land/p/sunspirit/table/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/sunspirit/table/v0\"\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"},{"name":"table_test.gno","body":"package table\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestNew(t *testing.T) {\n\theader := []string{\"Name\", \"Age\", \"Country\"}\n\trows := [][]string{\n\t\t{\"Alice\", \"30\", \"USA\"},\n\t\t{\"Bob\", \"25\", \"UK\"},\n\t}\n\n\ttable, err := New(header, rows)\n\turequire.NoError(t, err)\n\n\tuassert.Equal(t, len(header), len(table.header))\n\tuassert.Equal(t, len(rows), len(table.rows))\n}\n\nfunc Test_AddRow(t *testing.T) {\n\theader := []string{\"Name\", \"Age\"}\n\trows := [][]string{\n\t\t{\"Alice\", \"30\"},\n\t\t{\"Bob\", \"25\"},\n\t}\n\n\ttable, err := New(header, rows)\n\turequire.NoError(t, err)\n\n\t// Add a valid row\n\terr = table.AddRow([]string{\"Charlie\", \"28\"})\n\turequire.NoError(t, err)\n\n\texpectedRows := [][]string{\n\t\t{\"Alice\", \"30\"},\n\t\t{\"Bob\", \"25\"},\n\t\t{\"Charlie\", \"28\"},\n\t}\n\tuassert.Equal(t, len(expectedRows), len(table.rows))\n\n\t// Attempt to add a row with a different number of columns\n\terr = table.AddRow([]string{\"David\"})\n\tuassert.Error(t, err)\n}\n\nfunc Test_AddColumn(t *testing.T) {\n\theader := []string{\"Name\", \"Age\"}\n\trows := [][]string{\n\t\t{\"Alice\", \"30\"},\n\t\t{\"Bob\", \"25\"},\n\t}\n\n\ttable, err := New(header, rows)\n\turequire.NoError(t, err)\n\n\t// Add a valid column\n\terr = table.AddColumn(\"Country\", []string{\"USA\", \"UK\"})\n\turequire.NoError(t, err)\n\n\texpectedHeader := []string{\"Name\", \"Age\", \"Country\"}\n\texpectedRows := [][]string{\n\t\t{\"Alice\", \"30\", \"USA\"},\n\t\t{\"Bob\", \"25\", \"UK\"},\n\t}\n\tuassert.Equal(t, len(expectedHeader), len(table.header))\n\tuassert.Equal(t, len(expectedRows), len(table.rows))\n\n\t// Attempt to add a column with a different number of values\n\terr = table.AddColumn(\"City\", []string{\"New York\"})\n\tuassert.Error(t, err)\n}\n\nfunc Test_RemoveRow(t *testing.T) {\n\theader := []string{\"Name\", \"Age\", \"Country\"}\n\trows := [][]string{\n\t\t{\"Alice\", \"30\", \"USA\"},\n\t\t{\"Bob\", \"25\", \"UK\"},\n\t}\n\n\ttable, err := New(header, rows)\n\turequire.NoError(t, err)\n\n\t// Remove the first row\n\terr = table.RemoveRow(0)\n\turequire.NoError(t, err)\n\n\texpectedRows := [][]string{\n\t\t{\"Bob\", \"25\", \"UK\"},\n\t}\n\tuassert.Equal(t, len(expectedRows), len(table.rows))\n\n\t// Attempt to remove a row out of range\n\terr = table.RemoveRow(5)\n\tuassert.Error(t, err)\n}\n\nfunc Test_RemoveColumn(t *testing.T) {\n\theader := []string{\"Name\", \"Age\", \"Country\"}\n\trows := [][]string{\n\t\t{\"Alice\", \"30\", \"USA\"},\n\t\t{\"Bob\", \"25\", \"UK\"},\n\t}\n\n\ttable, err := New(header, rows)\n\turequire.NoError(t, err)\n\n\t// Remove the second column (Age)\n\terr = table.RemoveColumn(1)\n\turequire.NoError(t, err)\n\n\texpectedHeader := []string{\"Name\", \"Country\"}\n\texpectedRows := [][]string{\n\t\t{\"Alice\", \"USA\"},\n\t\t{\"Bob\", \"UK\"},\n\t}\n\tuassert.Equal(t, len(expectedHeader), len(table.header))\n\tuassert.Equal(t, len(expectedRows), len(table.rows))\n\n\t// Attempt to remove a column out of range\n\terr = table.RemoveColumn(5)\n\tuassert.Error(t, err)\n}\n\nfunc Test_Validate(t *testing.T) {\n\theader := []string{\"Name\", \"Age\", \"Country\"}\n\trows := [][]string{\n\t\t{\"Alice\", \"30\", \"USA\"},\n\t\t{\"Bob\", \"25\"},\n\t}\n\n\ttable, err := New(header, rows[:1])\n\turequire.NoError(t, err)\n\n\t// Validate should pass\n\terr = table.Validate()\n\turequire.NoError(t, err)\n\n\t// Add an invalid row and validate again\n\ttable.rows = append(table.rows, rows[1])\n\terr = table.Validate()\n\tuassert.Error(t, err)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"V1em1nTWHufNwLbCIsGpybcytm5URpIsWu4+yXQkeAV0WGbm0+jb0e1dD+G8CDYiZS+2BYdzkShF10e5xfEduQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"md","path":"gno.land/p/sunspirit/md/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/sunspirit/md/v0\"\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"},{"name":"md_test.gno","body":"package md\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/sunspirit/table/v0\"\n)\n\nfunc TestNewBuilder(t *testing.T) {\n\tmdBuilder := NewBuilder()\n\n\tuassert.Equal(t, len(mdBuilder.elements), 0, \"Expected 0 elements\")\n}\n\nfunc TestAdd(t *testing.T) {\n\tmdBuilder := NewBuilder()\n\n\theader := H1(\"Hi\")\n\tbody := Paragraph(\"This is a test\")\n\n\tmdBuilder.Add(header, body)\n\n\tuassert.Equal(t, len(mdBuilder.elements), 2, \"Expected 2 element\")\n\tuassert.Equal(t, mdBuilder.elements[0], header, \"Expected element %s, got %s\", header, mdBuilder.elements[0])\n\tuassert.Equal(t, mdBuilder.elements[1], body, \"Expected element %s, got %s\", body, mdBuilder.elements[1])\n}\n\nfunc TestRender(t *testing.T) {\n\tmdBuilder := NewBuilder()\n\n\theader := H1(\"Hello\")\n\tbody := Paragraph(\"This is a test\")\n\n\tseperator := \"\\n\"\n\texpected := header + seperator + body\n\n\toutput := mdBuilder.Add(header, body).Render(seperator)\n\n\tuassert.Equal(t, output, expected, \"Expected rendered string %s, got %s\", expected, output)\n}\n\nfunc Test_Bold(t *testing.T) {\n\tuassert.Equal(t, Bold(\"Hello\"), \"**Hello**\")\n}\n\nfunc Test_Italic(t *testing.T) {\n\tuassert.Equal(t, Italic(\"Hello\"), \"*Hello*\")\n}\n\nfunc Test_Strikethrough(t *testing.T) {\n\tuassert.Equal(t, Strikethrough(\"Hello\"), \"~~Hello~~\")\n}\n\nfunc Test_H1(t *testing.T) {\n\tuassert.Equal(t, H1(\"Header 1\"), \"# Header 1\\n\")\n}\n\nfunc Test_H2(t *testing.T) {\n\tuassert.Equal(t, H2(\"Header 2\"), \"## Header 2\\n\")\n}\n\nfunc Test_H3(t *testing.T) {\n\tuassert.Equal(t, H3(\"Header 3\"), \"### Header 3\\n\")\n}\n\nfunc Test_H4(t *testing.T) {\n\tuassert.Equal(t, H4(\"Header 4\"), \"#### Header 4\\n\")\n}\n\nfunc Test_H5(t *testing.T) {\n\tuassert.Equal(t, H5(\"Header 5\"), \"##### Header 5\\n\")\n}\n\nfunc Test_H6(t *testing.T) {\n\tuassert.Equal(t, H6(\"Header 6\"), \"###### Header 6\\n\")\n}\n\nfunc Test_BulletList(t *testing.T) {\n\titems := []string{\"Item 1\", \"Item 2\", \"Item 3\"}\n\tresult := BulletList(items)\n\texpected := \"- Item 1\\n- Item 2\\n- Item 3\\n\"\n\tuassert.Equal(t, result, expected)\n}\n\nfunc Test_OrderedList(t *testing.T) {\n\titems := []string{\"Item 1\", \"Item 2\", \"Item 3\"}\n\tresult := OrderedList(items)\n\texpected := \"1. Item 1\\n2. Item 2\\n3. Item 3\\n\"\n\tuassert.Equal(t, result, expected)\n}\n\nfunc Test_TodoList(t *testing.T) {\n\titems := []string{\"Task 1\", \"Task 2\"}\n\tdone := []bool{true, false}\n\tresult := TodoList(items, done)\n\texpected := \"- [x] Task 1\\n- [ ] Task 2\\n\"\n\tuassert.Equal(t, result, expected)\n}\n\nfunc Test_Blockquote(t *testing.T) {\n\ttext := \"This is a blockquote.\\nIt has multiple lines.\"\n\tresult := Blockquote(text)\n\texpected := \"\u003e This is a blockquote.\\n\u003e It has multiple lines.\\n\"\n\tuassert.Equal(t, result, expected)\n}\n\nfunc Test_InlineCode(t *testing.T) {\n\tresult := InlineCode(\"code\")\n\tuassert.Equal(t, result, \"`code`\")\n}\n\nfunc Test_LanguageCodeBlock(t *testing.T) {\n\tresult := LanguageCodeBlock(\"python\", \"print('Hello')\")\n\texpected := \"```python\\nprint('Hello')\\n```\"\n\tuassert.Equal(t, result, expected)\n}\n\nfunc Test_CodeBlock(t *testing.T) {\n\tresult := CodeBlock(\"print('Hello')\")\n\texpected := \"```\\nprint('Hello')\\n```\"\n\tuassert.Equal(t, result, expected)\n}\n\nfunc Test_LineBreak(t *testing.T) {\n\tresult := LineBreak(2)\n\texpected := \"\\n\\n\\n\"\n\tuassert.Equal(t, result, expected)\n\n\tresult = LineBreak(0)\n\texpected = \"\"\n\tuassert.Equal(t, result, expected)\n}\n\nfunc Test_HorizontalRule(t *testing.T) {\n\tresult := HorizontalRule()\n\tuassert.Equal(t, result, \"---\\n\")\n}\n\nfunc Test_Link(t *testing.T) {\n\tresult := Link(\"Google\", \"http://google.com\")\n\tuassert.Equal(t, result, \"[Google](http://google.com)\")\n}\n\nfunc Test_Image(t *testing.T) {\n\tresult := Image(\"Alt text\", \"http://image.url\")\n\tuassert.Equal(t, result, \"![Alt text](http://image.url)\")\n}\n\nfunc Test_Footnote(t *testing.T) {\n\tresult := Footnote(\"1\", \"This is a footnote.\")\n\tuassert.Equal(t, result, \"[1]: This is a footnote.\")\n}\n\nfunc Test_Paragraph(t *testing.T) {\n\tresult := Paragraph(\"This is a paragraph.\")\n\tuassert.Equal(t, result, \"This is a paragraph.\\n\")\n}\n\nfunc Test_Table(t *testing.T) {\n\ttb, err := table.New([]string{\"Header1\", \"Header2\"}, [][]string{\n\t\t{\"Row1Col1\", \"Row1Col2\"},\n\t\t{\"Row2Col1\", \"Row2Col2\"},\n\t})\n\tuassert.NoError(t, err)\n\n\tresult := Table(tb)\n\texpected := \"| Header1 | Header2 |\\n| ---|---|\\n| Row1Col1 | Row1Col2 |\\n| Row2Col1 | Row2Col2 |\\n\"\n\tuassert.Equal(t, result, expected)\n}\n\nfunc Test_EscapeMarkdown(t *testing.T) {\n\tresult := EscapeMarkdown(\"- This is `code`\")\n\tuassert.Equal(t, result, \"``- This is `code```\")\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"zH0uSgm+U1eBflf2dp0sSyw6ap2H64KIFUBHVh8kDVB2EdiTKhzAyK/KmYiPvSioR4S1dkasd1gwM38eOHk+ZA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1kfd9f5zlvcvy6aammcmqswa7cyjpu2nyt9qfen","package":{"name":"piechart","path":"gno.land/p/samcrew/piechart/v0","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/v0\"\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/nt/svg/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/sunspirit/md/v0\"\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"},{"name":"piechart_test.gno","body":"package piechart\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestRender(t *testing.T) {\n\ttitle := \"Test\"\n\tslices := []PieSlice{\n\t\t{Value: 10, Color: \"#00FF00\", Label: \"Green\"},\n\t\t{Value: 20, Color: \"#FFFF00\", Label: \"Yellow\"},\n\t\t{Value: 30, Color: \"#FF0000\", Label: \"Red\"},\n\t}\n\n\tresult := Render(slices, title)\n\n\t// Check if the result contains the expected image SVG structure\n\tif !strings.Contains(result, \"data:image/svg+xml;base64,\") {\n\t\tt.Errorf(\"Expected result to contain data:image/svg+xml;base64, got %s\", result)\n\t}\n\n\t// Check if the result contains the title\n\tif !strings.Contains(result, title) {\n\t\tt.Errorf(\"SVG does not contain the title %q\", title)\n\t}\n\n\t// Check if the result contains non-empty slices\n\temptySlices := []PieSlice{}\n\temptyResult := Render(emptySlices, title)\n\texpectedErr := \"\\npiechart fails: no data provided\"\n\tif emptyResult != expectedErr {\n\t\tt.Errorf(\"Expected exact error for empty slices: %q, got %q\", expectedErr, emptyResult)\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"xpTObk6bnjlntQoXL4d+DCKnHKuEAopPT7w4enED06Alt9v4WlVeGnEa8puQxPDtrlRgoLV+VSuKatKYyGIOkg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1kfd9f5zlvcvy6aammcmqswa7cyjpu2nyt9qfen","package":{"name":"tablesort","path":"gno.land/p/samcrew/tablesort/v0","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/v0\"\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/memberstore/v0:members?filter=T1](/r/gov/dao/memberstore/v0: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/v0\"\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/v0\"\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"},{"name":"tablesort_test.gno","body":"package tablesort\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestRender(t *testing.T) {\n\t// --- Case 1: Testing Render with a Table with \"Role\" column sorted descending and param prefix \"members-\"\n\ttable := \u0026Table{\n\t\tHeadings: []string{\"Tier\", \"Member\", \"Role\"},\n\t\tRows: [][]string{\n\t\t\t{\"T1\", \"g11111\", \"finance-officer\"},\n\t\t\t{\"T2\", \"g22222\", \"developer\"},\n\t\t\t{\"T3\", \"g33333\", \"developer\"},\n\t\t},\n\t}\n\n\tu, _ := url.Parse(\"/test?members-sort-desc=Tier\")\n\tmd := Render(u, table, \"members-\")\n\n\texpected := \"| [Tier ↓](/test?members-sort-asc=Tier) | [Member](/test?members-sort-desc=Member) | [Role](/test?members-sort-desc=Role) |\\n\" +\n\t\t\"| --- | --- | --- |\\n\" +\n\t\t\"| T3 | g33333 | developer |\\n\" +\n\t\t\"| T2 | g22222 | developer |\\n\" +\n\t\t\"| T1 | g11111 | finance-officer |\\n\"\n\n\t// Trim spaces for comparison\n\tmd = strings.TrimSpace(md)\n\texpected = strings.TrimSpace(expected)\n\n\tif md != expected {\n\t\tt.Errorf(\"Render() output mismatch.\\nExpected:\\n%s\\nGot:\\n%s\", expected, md)\n\t}\n\n\t// --- Case 2: Testing Render with an invalid Table (row with missing cell)\n\ttable = \u0026Table{\n\t\tHeadings: []string{\"Tier\", \"Member\", \"Role\"},\n\t\tRows: [][]string{\n\t\t\t{\"T1\", \"g11111\"}, // Missing the \"Role\" cell\n\t\t},\n\t}\n\n\tmd = Render(u, table, \"\")\n\texpected = \"tablesort fails: row 1 has 2 cells, expected 3, because there are 3 columns.\\n\"\n\n\tif md != expected {\n\t\tt.Errorf(\"Expected error message:\\n%s\\nGot:\\n%s\", expected, md)\n\t}\n\n\t// --- Case 3: Testing SortRows\n\trows := [][]string{\n\t\t{\"T1\", \"g11111\", \"finance-officer\"},\n\t\t{\"T2\", \"g22222\", \"developer\"},\n\t\t{\"T3\", \"g33333\", \"developer\"},\n\t}\n\n\tSortRows(rows, 2, false) // Sort by \"Role\" descending\n\texpected = \"finance-officer\"\n\n\tif rows[0][2] != expected {\n\t\tt.Errorf(\"SortRows() failed. Expected first role: %s, got: %s\", expected, rows[0][2])\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"ldcGWOMo2tUyRduC9YJH0o4VelOekHlT4iCuMzin0xMOQvA4+v1sdUIqDEahIsODGI01ZiimJZ0u0S5cxqA66Q=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1kfd9f5zlvcvy6aammcmqswa7cyjpu2nyt9qfen","package":{"name":"urlfilter","path":"gno.land/p/samcrew/urlfilter/v0","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/v0?filter=size:XL) - [~~T2~~](/p/samcrew/urlfilter/v0=T1,T2,size:XL) - [**size:XL**](/p/samcrew/urlfilter/v0?filter=T1)\n```\n\n**Rendered as:**\n[**T1**](/p/samcrew/urlfilter/v0?filter=size:XL) - [~~T2~~](/p/samcrew/urlfilter/v0=T1,T2,size:XL) - [**size:XL**](/p/samcrew/urlfilter/v0?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/memberstore/v0:members?filter=T1](/r/gov/dao/memberstore/v0:members?filter=T1)\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/samcrew/urlfilter/v0\"\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/v0\"\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"},{"name":"urlfilter_test.gno","body":"package urlfilter\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/bptree/v0\"\n)\n\nfunc buildTestTree() *bptree.BPTree {\n\troot := bptree.NewBPTree32()\n\n\t// keyF1 subtree\n\tt1 := bptree.NewBPTree32()\n\tt1.Set(\"key1_1\", nil)\n\tt1.Set(\"key1_2\", nil)\n\troot.Set(\"keyF1\", t1)\n\n\t// keyF2 subtree\n\tt2 := bptree.NewBPTree32()\n\tt2.Set(\"key2_1\", nil)\n\tt2.Set(\"key2_2\", nil)\n\troot.Set(\"keyF2\", t2)\n\n\treturn root\n}\n\nfunc TestApplyFilters(t *testing.T) {\n\tTreeParent := buildTestTree()\n\n\t// --- Case 1: No filter selected\n\tu, _ := url.Parse(\"/test\")\n\tmdFilters, items := ApplyFilters(u, TreeParent, \"filter\")\n\texpectedMarkdown := \" | [~~keyF1~~](/test?filter=keyF1)  | [~~keyF2~~](/test?filter=keyF2) \"\n\tif mdFilters != expectedMarkdown {\n\t\tt.Errorf(\"Expected Markdown %q, got %q\", expectedMarkdown, mdFilters)\n\t}\n\t// All items should be present\n\tcount := 0\n\titems.Iterate(\"\", \"\", func(k string, _ interface{}) bool {\n\t\tcount++\n\t\treturn false\n\t})\n\tif count != 4 {\n\t\tt.Errorf(\"Expected 4 items, got %d\", count)\n\t}\n\n\t// Try using ApplyFilters with a different name.\n\twithCustom, _ := ApplyFilters(u, TreeParent, \"custom-param-name\")\n\tif !strings.Contains(withCustom, \"custom-param-name=keyF1\") {\n\t\tt.Errorf(\"Expected 'custom-param-name' parameter in Markdown, got %q\", mdFilters)\n\t}\n\n\t// --- Case 2: One filter active (keyF1)\n\tu, _ = url.Parse(\"/test?filter=keyF1\")\n\tmdFilters, items = ApplyFilters(u, TreeParent, \"filter\")\n\texpectedMarkdown = \" | [**keyF1**](/test)  | [~~keyF2~~](/test?filter=keyF1%2CkeyF2) \"\n\tif mdFilters != expectedMarkdown {\n\t\tt.Errorf(\"Expected Markdown %q, got %q\", expectedMarkdown, mdFilters)\n\t}\n\t// Only keyF1 items should be present\n\tkeys := map[string]bool{}\n\titems.Iterate(\"\", \"\", func(k string, _ interface{}) bool {\n\t\tkeys[k] = true\n\t\treturn false\n\t})\n\tif len(keys) != 2 || !keys[\"key1_1\"] || !keys[\"key1_2\"] {\n\t\tt.Errorf(\"Unexpected items in filtered result: %#v\", keys)\n\t}\n\n\t// --- Case 3: Multiple filters active (keyF1, keyF2)\n\tu, _ = url.Parse(\"/test?filter=keyF1,keyF2\")\n\tmdFilters, items = ApplyFilters(u, TreeParent, \"filter\")\n\t// Both filters should be bold, no remove query for last one\n\tif items.Size() != 4 {\n\t\tt.Errorf(\"Expected 4 items, got %d\", items.Size())\n\t}\n\n\t// --- Case 4: Filter not existing\n\tu, _ = url.Parse(\"/test?filter=unknown\")\n\tmdFilters, items = ApplyFilters(u, TreeParent, \"filter\")\n\t// No matching items\n\tif items.Size() != 0 {\n\t\tt.Errorf(\"Expected 0 items for unknown filter, got %d\", items.Size())\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"Wdu/U+OFSWsFPi5LlMHrlyJifAc830ToKfYsx2TXAO08zWAJH1qxt0WjVHAlOSLn/ybZYOp86or5/DtqvWfOTA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1g73v2anukg4ej7axwqpthsatzrxjsh0wk797da","package":{"name":"dao","path":"gno.land/r/gov/dao","files":[{"name":"allowlist_test.gno","body":"package dao\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\n// allowedDAOs is the sole authorization for UpdateImpl, memberstore.Get,\n// treasury.Send and treasury.SetTokenKeys, and InAllowedDAOs() fails OPEN when\n// it is empty — the bootstrap window that lets the genesis MsgRun seed the\n// member set before lockdown.\n//\n// These tests pin that the transition empty -\u003e non-empty is one-way: once the\n// DAO is locked down, UpdateImpl cannot put it back into the fail-open state.\n// Both paths below reopened the gate before the guard, because\n// NewUpdateRequest copies nil into a NON-nil empty slice and the old test was\n// `r.AllowedDAOs != nil`.\n//\n// These call the real UpdateImpl through a code realm rather than replaying\n// its logic, so reverting the guard makes them fail.\nfunc TestUpdateImplIgnoresEmptyAllowedDAOs(cur realm, t *testing.T) {\n\tsavedDAOs, savedDAO := allowedDAOs, dao\n\tdefer func() { allowedDAOs, dao = savedDAOs, savedDAO }()\n\n\tlock := func() {\n\t\tallowedDAOs = nil // reopen so the next UpdateImpl is permitted\n\t\ttesting.SetRealm(testing.NewCodeRealm(v0))\n\t\tUpdateImpl(cross(cur), UpdateRequest{DAO: \u0026dummyDao{}, AllowedDAOs: []string{v0}})\n\t\tuassert.False(t, InAllowedDAOs(invalid), \"precondition: locked down\")\n\t}\n\n\t// Path 1: NewUpdateRequest(d, nil) — \"swap the implementation, leave\n\t// permissions alone\", and the form v0/loader uses.\n\tlock()\n\ttesting.SetRealm(testing.NewCodeRealm(v0))\n\tUpdateImpl(cross(cur), NewUpdateRequest(\u0026dummyDao{}, nil))\n\tuassert.False(t, InAllowedDAOs(invalid),\n\t\t\"a nil AllowedDAOs must not reopen the permission gate\")\n\n\t// Path 2: an explicitly empty slice via the struct literal.\n\tlock()\n\ttesting.SetRealm(testing.NewCodeRealm(v0))\n\tUpdateImpl(cross(cur), UpdateRequest{DAO: \u0026dummyDao{}, AllowedDAOs: []string{}})\n\tuassert.False(t, InAllowedDAOs(invalid),\n\t\t\"an empty AllowedDAOs must not reopen the permission gate\")\n\n\t// A legitimate extension of the list must still apply.\n\tlock()\n\ttesting.SetRealm(testing.NewCodeRealm(v0))\n\tUpdateImpl(cross(cur), NewUpdateRequest(\u0026dummyDao{}, []string{v0, v1}))\n\tuassert.True(t, InAllowedDAOs(v1), \"a non-empty AllowedDAOs must still be stored\")\n\tuassert.False(t, InAllowedDAOs(invalid), \"and must not admit anyone else\")\n}\n\n// The bootstrap window itself must survive the guard: with no allowlist\n// configured yet, any caller is allowed so the genesis MsgRun can seed the\n// member set and then lock down.\nfunc TestBootstrapWindowStillOpen(t *testing.T) {\n\tsaved := allowedDAOs\n\tdefer func() { allowedDAOs = saved }()\n\n\tallowedDAOs = nil\n\tuassert.True(t, InAllowedDAOs(\"gno.land/r/gov/dao/loader/v0\"),\n\t\t\"an unset allowlist must stay open for genesis bootstrap\")\n}\n\n// len(AllowedDAOs) != 0 is not sufficient on its own. InAllowedDAOs compares by\n// exact string and a user realm's PkgPath() is \"\", so a single \"\" entry admits\n// any caller whose previous frame is a user realm — the same fail-open outcome\n// the guard exists to prevent. NewUpgradeDaoImplRequest passes its realmPkg\n// argument straight into the list, so an empty one reaches here.\nfunc TestUpdateImplRejectsBlankAllowedDAOEntry(cur realm, t *testing.T) {\n\tsavedDAOs, savedDAO := allowedDAOs, dao\n\tdefer func() { allowedDAOs, dao = savedDAOs, savedDAO }()\n\n\tfor _, blank := range []string{\"\", \"   \"} {\n\t\tallowedDAOs = nil\n\t\ttesting.SetRealm(testing.NewCodeRealm(v0))\n\t\tUpdateImpl(cross(cur), UpdateRequest{DAO: \u0026dummyDao{}, AllowedDAOs: []string{v0}})\n\n\t\ttesting.SetRealm(testing.NewCodeRealm(v0))\n\t\turequire.AbortsWithMessage(t, cur,\n\t\t\t\"AllowedDAOs entries must be realm paths; got an empty one\",\n\t\t\tfunc() {\n\t\t\t\tUpdateImpl(cross(cur), UpdateRequest{\n\t\t\t\t\tDAO:         \u0026dummyDao{},\n\t\t\t\t\tAllowedDAOs: []string{v0, blank},\n\t\t\t\t})\n\t\t\t})\n\n\t\tuassert.False(t, InAllowedDAOs(\"\"),\n\t\t\t\"a blank entry must never make it into the allowlist\")\n\t\tuassert.True(t, InAllowedDAOs(v0),\n\t\t\t\"the rejected request must leave the previous allowlist intact\")\n\t}\n}\n\n// A padded entry passes a plain non-blank test but is useless: entries are\n// stored exactly as given and InAllowedDAOs compares whole strings, so\n// \" gno.land/r/x \" matches no caller. The list is still non-empty, so the\n// bootstrap window is closed. A proposal that padded every entry would lock\n// the DAO out of its own allowlist with no way back.\nfunc TestUpdateImplRejectsPaddedAllowedDAOEntry(cur realm, t *testing.T) {\n\tsavedDAOs, savedDAO := allowedDAOs, dao\n\tdefer func() { allowedDAOs, dao = savedDAOs, savedDAO }()\n\n\tfor _, padded := range []string{\" \" + v1, v1 + \" \", \"\\t\" + v1} {\n\t\tallowedDAOs = nil\n\t\ttesting.SetRealm(testing.NewCodeRealm(v0))\n\t\tUpdateImpl(cross(cur), UpdateRequest{DAO: \u0026dummyDao{}, AllowedDAOs: []string{v0}})\n\n\t\ttesting.SetRealm(testing.NewCodeRealm(v0))\n\t\turequire.AbortsWithMessage(t, cur,\n\t\t\t\"AllowedDAOs entries must not have leading or trailing spaces; entry 1\",\n\t\t\tfunc() {\n\t\t\t\tUpdateImpl(cross(cur), UpdateRequest{\n\t\t\t\t\tDAO:         \u0026dummyDao{},\n\t\t\t\t\tAllowedDAOs: []string{v0, padded},\n\t\t\t\t})\n\t\t\t})\n\n\t\tuassert.False(t, InAllowedDAOs(padded),\n\t\t\t\"a padded entry must never make it into the allowlist\")\n\t\tuassert.True(t, InAllowedDAOs(v0),\n\t\t\t\"the rejected request must leave the previous allowlist intact\")\n\t}\n}\n"},{"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\t\"strings\"\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. A nil DAO is ignored.\n// If AllowedDAOs field is not set correctly, the actual DAO\n// implementation wont be able to execute new Proposals!\n//\n// An empty AllowedDAOs is ignored rather than stored. An empty list makes\n// InAllowedDAOs() return true for every caller — the bootstrap-only state that\n// lets the genesis MsgRun seed the member set. Since this is the only site that\n// assigns allowedDAOs, ignoring empty here makes the transition\n// empty -\u003e non-empty one-way: once locked down the DAO cannot be reopened,\n// whether the empty value arrives as a literal []string{} or from\n// NewUpdateRequest(d, nil), which copies nil into a non-nil empty slice.\n// Individual entries must be non-blank realm paths; an empty entry would match\n// a user realm's empty PkgPath() and is rejected.\nfunc UpdateImpl(cur realm, r UpdateRequest) {\n\t// AGENTS.md: in a crossing function, always check IsCurrent() before\n\t// deriving caller identity from cur.Previous(). Redundant under the\n\t// crossing-frame guarantee, but mandated, and this is the single most\n\t// powerful entrypoint (it rewrites the allowlist and swaps the impl).\n\tif !cur.IsCurrent() {\n\t\tpanic(\"UpdateImpl: realm value is not the caller's live cur\")\n\t}\n\tgRealm := cur.Previous().PkgPath()\n\n\tif !InAllowedDAOs(gRealm) {\n\t\tpanic(\"permission denied for prev realm: \" + gRealm)\n\t}\n\n\tif len(r.AllowedDAOs) != 0 {\n\t\t// Every entry must be a real realm path. len() != 0 alone is the wrong\n\t\t// invariant: InAllowedDAOs compares by exact string, and a user realm's\n\t\t// PkgPath() is \"\", so a single \"\" entry admits any caller whose previous\n\t\t// frame is a user realm -- the same fail-open outcome this guard exists\n\t\t// to prevent, just spelled differently. An empty entry can only be a\n\t\t// drafting mistake, so reject the whole request rather than silently\n\t\t// dropping it and storing a list the proposal did not describe.\n\t\tfor i, d := range r.AllowedDAOs {\n\t\t\ttrimmed := strings.TrimSpace(d)\n\t\t\tif trimmed == \"\" {\n\t\t\t\tpanic(\"AllowedDAOs entries must be realm paths; got an empty one\")\n\t\t\t}\n\t\t\t// Entries are stored exactly as given, and InAllowedDAOs compares\n\t\t\t// whole strings, so an entry with surrounding spaces matches no\n\t\t\t// caller at all. A non-empty list also closes the bootstrap\n\t\t\t// window, so a list of only padded entries is locked shut against\n\t\t\t// everyone, the DAO included, with no way to reopen it.\n\t\t\t//\n\t\t\t// This does not make the list typo-proof, and is not meant to be:\n\t\t\t// any wrong path locks the DAO out exactly the same way, and no\n\t\t\t// check here can tell a typo from a realm that does not exist yet.\n\t\t\t// Whitespace is worth rejecting because it is the one spelling a\n\t\t\t// human reviewing the proposal cannot see. Rejected rather than\n\t\t\t// trimmed, so what gets stored is what the proposal said.\n\t\t\t// Reported by position, not by value. A panic message becomes the\n\t\t\t// proposal's DeniedReason, which is stored, and the entry is\n\t\t\t// caller-supplied and unbounded — echoing it would put an\n\t\t\t// arbitrary amount of someone else's text into this realm's\n\t\t\t// storage. The index is enough to find it in a list the proposal\n\t\t\t// author wrote.\n\t\t\tif d != trimmed {\n\t\t\t\tpanic(\"AllowedDAOs entries must not have leading or trailing spaces; entry \" + strconv.Itoa(i))\n\t\t\t}\n\t\t}\n\t\t// Stored as given. A defensive copy here looks prudent but would\n\t\t// guard a write no outside realm can perform, which was checked from\n\t\t// a separate realm rather than assumed:\n\t\t//\n\t\t//   - Building this request as a literal fails outright, with\n\t\t//     \"cannot allocate gno.land/r/gov/dao.UpdateRequest in realm ...\".\n\t\t//   - Writing through a request obtained from NewUpdateRequest fails\n\t\t//     with \"cannot directly modify readonly tainted object\".\n\t\t//\n\t\t// So the only way in is NewUpdateRequest, which copies already. Both\n\t\t// checks are language guarantees, not conventions.\n\t\tallowedDAOs = r.AllowedDAOs\n\t}\n\n\tif r.DAO != nil {\n\t\tdao = r.DAO\n\t\tinstallGeneration++\n\t}\n}\n\n// installGeneration counts implementation installations: UpdateImpl\n// increments it on every non-nil DAO assignment, including one that assigns\n// the implementation already installed. An allowlist-only update (nil DAO)\n// leaves it unchanged.\nvar installGeneration uint64\n\n// InstallGeneration returns the number of implementation installations so\n// far. An implementation that must act once per installation (impl/v1 applies\n// the law its install proposal carried) compares it with the generation it\n// last acted on: it is a structural signal that UpdateImpl just installed a\n// DAO, which no proposal or executor can fake.\nfunc InstallGeneration() uint64 {\n\treturn installGeneration\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":"proxy_test.gno","body":"package dao\n\nimport (\n\t\"chain/runtime/unsafe\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\n// cur is a zero-value realm used as a placeholder when forwarding to\n// uassert/urequire dispatch helpers that gained an `rlm realm` param.\n// These tests pass `func()` callbacks (no crossing inside the callback),\n// so rlm is ignored — a nil realm here is safe.\nvar cur realm\n\nconst (\n\tv0 = \"gno.land/r/gov/dao/impl/v0\"\n\tv1 = \"gno.land/r/gov/dao/v1/impl\"\n\tv2 = \"gno.land/r/gov/dao/v2/impl\"\n\tv3 = \"gno.land/r/gov/dao/v3/impl\"\n)\n\nconst invalid = \"gno.land/r/invalid/dao\"\n\nvar alice = testutils.TestAddress(\"alice\")\n\nfunc TestProxy_Functions(cur realm, t *testing.T) {\n\t// initialize tests\n\tUpdateImpl(cross(cur), UpdateRequest{\n\t\tDAO:         \u0026dummyDao{},\n\t\tAllowedDAOs: []string{v0},\n\t})\n\n\t// invalid package cannot add a new dao in charge\n\ttesting.SetRealm(testing.NewCodeRealm(invalid))\n\turequire.AbortsWithMessage(t, cur, \"permission denied for prev realm: gno.land/r/invalid/dao\", func() {\n\t\tUpdateImpl(cross(cur), UpdateRequest{\n\t\t\tDAO: \u0026dummyDao{},\n\t\t})\n\t})\n\n\t// dao in charge can add a new dao\n\ttesting.SetRealm(testing.NewCodeRealm(v0))\n\turequire.NotPanics(t, cur, func() {\n\t\tUpdateImpl(cross(cur), UpdateRequest{\n\t\t\tDAO: \u0026dummyDao{},\n\t\t})\n\t})\n\n\t// v0 that is in charge adds v2 in charge\n\ttesting.SetRealm(testing.NewCodeRealm(v0))\n\turequire.NotPanics(t, cur, func() {\n\t\tUpdateImpl(cross(cur), UpdateRequest{\n\t\t\tDAO:         \u0026dummyDao{},\n\t\t\tAllowedDAOs: []string{v0, v2},\n\t\t})\n\t})\n\n\t// v0 can still do updates\n\ttesting.SetRealm(testing.NewCodeRealm(v0))\n\turequire.NotPanics(t, cur, func() {\n\t\tUpdateImpl(cross(cur), UpdateRequest{\n\t\t\tAllowedDAOs: []string{v1},\n\t\t})\n\t})\n\n\t// not after removing himself from allowedDAOs list\n\ttesting.SetRealm(testing.NewCodeRealm(v0))\n\turequire.AbortsWithMessage(t, cur, \"permission denied for prev realm: gno.land/r/gov/dao/impl/v0\", func() {\n\t\tUpdateImpl(cross(cur), UpdateRequest{\n\t\t\tAllowedDAOs: []string{v0},\n\t\t})\n\t})\n\n\tvar pid ProposalID\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\turequire.NotPanics(t, cur, func() {\n\t\te := NewSimpleExecutor(0, cur,\n\t\t\tfunc(realm) error {\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\t\"\",\n\t\t)\n\t\tpid = MustCreateProposal(cross(cur), NewProposalRequest(\"Proposal Title\", \"Description\", e))\n\t})\n\n\tp, err := GetProposal(1000)\n\tif p != nil || err == nil {\n\t\tpanic(\"proposal should not exist and should return an error\")\n\t}\n\tp = MustGetProposal(pid)\n\turequire.Equal(t, \"Proposal Title\", p.Title())\n\turequire.Equal(t, p.Author().String(), alice.String())\n\n\t// need to switch the context back to v1\n\ttesting.SetRealm(testing.NewCodeRealm(v1))\n\turequire.Equal(\n\t\tt,\n\t\t\"Render: gno.land/r/gov/dao/test\",\n\t\tRender(cross(cur), \"test\"),\n\t)\n\n\t// reset state\n\ttesting.SetRealm(testing.NewCodeRealm(v1))\n\tUpdateImpl(cross(cur), UpdateRequest{\n\t\tDAO: \u0026dummyDao{},\n\t})\n\t// UpdateImpl deliberately ignores an empty AllowedDAOs, so it can no\n\t// longer be used to restore the bootstrap-open state — that is the point\n\t// of the guard. Reset the package var directly instead; we are in the\n\t// same package.\n\tallowedDAOs = nil\n}\n\ntype dummyDao struct{}\n\nfunc (dd *dummyDao) PreCreateProposal(_ int, rlm realm, r ProposalRequest) (address, error) {\n\treturn unsafe.OriginCaller(), nil\n}\n\nfunc (dd *dummyDao) PostCreateProposal(_ int, rlm realm, r ProposalRequest, pid ProposalID) {\n}\n\nfunc (dd *dummyDao) VoteOnProposal(_ int, rlm realm, r VoteRequest) error {\n\treturn nil\n}\n\nfunc (dd *dummyDao) PreExecuteProposal(_ int, rlm realm, pid ProposalID) (bool, error) {\n\treturn true, nil\n}\n\nfunc (dd *dummyDao) ExecuteProposal(_ int, rlm realm, pid ProposalID, e Executor) error {\n\treturn nil\n}\n\nfunc (dd *dummyDao) Render(cur realm, pkgpath string, path string) string {\n\treturn \"Render: \" + pkgpath + \"/\" + path\n}\n\n// InstallGeneration counts non-nil DAO assignments, including the same DAO\n// assigned again, and ignores allowlist-only updates.\nfunc TestInstallGeneration(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(v0))\n\td := \u0026dummyDao{}\n\tstart := InstallGeneration()\n\n\tUpdateImpl(cross(cur), UpdateRequest{DAO: d, AllowedDAOs: []string{v0}})\n\turequire.Equal(t, start+1, InstallGeneration(), \"a DAO assignment is an installation\")\n\n\tUpdateImpl(cross(cur), UpdateRequest{DAO: d})\n\turequire.Equal(t, start+2, InstallGeneration(), \"assigning the same DAO again is another installation\")\n\n\tUpdateImpl(cross(cur), UpdateRequest{AllowedDAOs: []string{v0, v2}})\n\turequire.Equal(t, start+2, InstallGeneration(), \"an allowlist-only update is not an installation\")\n}\n"},{"name":"types.gno","body":"package dao\n\nimport (\n\t\"errors\"\n\t\"strings\"\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\n// proxyPkgPath is this package: the realm whose frame Execute mints for a\n// callback, and therefore the only realm entitled to invoke one.\nconst proxyPkgPath = \"gno.land/r/gov/dao\"\n\n// Execute runs the proposal's callback. Invocable only from this proxy.\n//\n// SECURITY: Execute is a CROSSING method declared here, so invoking it mints\n// a gno.land/r/gov/dao frame for the callback — `cur.Previous()` inside the\n// callback is this path whoever called. That is a capability: realms gate on\n// it (authz.NewContractAuthority(\"gno.land/r/gov/dao\") + DoByPrevious,\n// ownable, anything reading Previous().Address()). Ungated, any realm could\n// wrap a reachable `func(realm) error` — including an ordinary exported\n// entrypoint of the victim — and run it with governance's identity, with no\n// proposal id minted, so nothing to render, audit or deny afterwards.\n//\n// NOT InAllowedDAOs: that is SafeExecutor's bug. It holds the impl path while\n// an executor's caller is the proxy, so it rejects the approved route and\n// fails open while empty. Exact path plus subpackages, because\n// impl.ExecuteProposal is non-crossing (Previous() is the proxy exactly), so\n// a bare prefix would reject every real proposal.\n//\n// Gates the executor object's INVOCATION only. Keeping a privileged closure,\n// or an exported entrypoint shaped like `func(realm) error`, out of reach\n// stays the consumer's job — see r/gnops/valopers/admin.gno\n// and p/moul/authz's NewContractAuthority godoc.\nfunc (e *SimpleExecutor) Execute(cur realm) error {\n\t// IsCurrent first, as every other gate here does: otherwise Previous() is\n\t// read from whatever realm value the caller threaded in.\n\tif !cur.IsCurrent() {\n\t\treturn errors.New(\"execution denied: cur is not the caller's live realm\")\n\t}\n\tif prev := cur.Previous().PkgPath(); prev != proxyPkgPath \u0026\u0026\n\t\t!strings.HasPrefix(prev, proxyPkgPath+\"/\") {\n\t\treturn errors.New(\"execution denied: executors are only invocable by \" + proxyPkgPath)\n\t}\n\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// IsCurrent first, matching every other allowlist gate in this tree\n\t// (proxy.go's UpdateImpl, treasury, memberstore). Without it this method\n\t// trusts whatever realm value it is handed, so a caller threading a stale\n\t// or sibling-frame cur would have its Previous() read from that value\n\t// rather than from the live frame.\n\t//\n\t// NewSafeExecutor has no call sites, so this type is currently dead code.\n\t// Live proposal execution goes through the Executor interface to\n\t// SimpleExecutor.Execute, which now gates on the invoking realm being the\n\t// proxy -- deliberately NOT on InAllowedDAOs, which holds the impl path\n\t// and therefore rejects the approved route and fails open while empty.\n\t// That is this method's bug; see SimpleExecutor.Execute.\n\tif !cur.IsCurrent() {\n\t\treturn errors.New(\"execution denied: cur is not the caller's live realm\")\n\t}\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"RHTG2hpZPh3qCgdXngv7NJcsZirUbmNtmoOIZw+ZXth1hKbjZJH1CuxckUO4O3vel/oZAbJLa7vGraruIRbPLA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l","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/v0` 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/v0`, 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/v0`'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/v0/init.gno` likewise\n  auto-whitelists `gno.land/r/sys/namereg/v0`. 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/v0\"\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/v0, 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":"crossrealm_test.gno","body":"package users\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\n// TestUpdateNameCallerIdentity verifies that the CurrentRealm() check in\n// UpdateName correctly identifies the calling controller.\n//\n// Background: RegisterUser uses PreviousRealm() because it is a crossing\n// function (cur realm). UpdateName uses CurrentRealm() because it is a\n// non-crossing method on *UserData. Per the interrealm spec:\n//\n//   - Crossing function: CurrentRealm = this realm, PreviousRealm = caller\n//   - Non-crossing method on external object, called from crossing context:\n//     CurrentRealm = caller (unchanged from crossing context)\n//\n// This test verifies that a whitelisted controller can call UpdateName,\n// and a non-whitelisted realm cannot.\nfunc TestUpdateNameCallerIdentity(cur realm, t *testing.T) {\n\tcontrollerPath := initControllerPath\n\tnonControllerPath := \"gno.land/r/evil/attacker\"\n\n\tt.Run(\"whitelisted_controller_can_update\", func(cur realm, t *testing.T) {\n\t\tcleanStore(t)\n\n\t\t// Register as whitelisted controller\n\t\ttesting.SetRealm(testing.NewCodeRealm(controllerPath))\n\t\turequire.NoError(t, RegisterUser(cross(cur), \"testuser1\", testutils.TestAddress(\"testuser1\")))\n\n\t\t// Resolve and update name — should succeed because controller is whitelisted\n\t\tdata := ResolveAddress(testutils.TestAddress(\"testuser1\"))\n\t\turequire.NotEqual(t, nil, data)\n\t\tuassert.NoError(t, data.UpdateName(0, cur, \"newname1\"))\n\t\tuassert.Equal(t, \"newname1\", data.Name())\n\t})\n\n\tt.Run(\"non_whitelisted_realm_cannot_update\", func(cur realm, t *testing.T) {\n\t\tcleanStore(t)\n\n\t\t// Register as whitelisted controller\n\t\ttesting.SetRealm(testing.NewCodeRealm(controllerPath))\n\t\turequire.NoError(t, RegisterUser(cross(cur), \"testuser2\", testutils.TestAddress(\"testuser2\")))\n\n\t\tdata := ResolveAddress(testutils.TestAddress(\"testuser2\"))\n\t\turequire.NotEqual(t, nil, data)\n\n\t\t// Switch to non-whitelisted realm — UpdateName should fail\n\t\ttesting.SetRealm(testing.NewCodeRealm(nonControllerPath))\n\t\terr := data.UpdateName(0, cur, \"hacked\")\n\t\tuassert.ErrorContains(t, err, \"does not exist in whitelist\")\n\t\tuassert.Equal(t, \"testuser2\", data.Name()) // name unchanged\n\t})\n}\n\n// TestDeleteCallerIdentity verifies the same CurrentRealm() behavior for Delete.\nfunc TestDeleteCallerIdentity(cur realm, t *testing.T) {\n\tcontrollerPath := initControllerPath\n\tnonControllerPath := \"gno.land/r/evil/attacker\"\n\n\tt.Run(\"whitelisted_controller_can_delete\", func(cur realm, t *testing.T) {\n\t\tcleanStore(t)\n\n\t\ttesting.SetRealm(testing.NewCodeRealm(controllerPath))\n\t\turequire.NoError(t, RegisterUser(cross(cur), \"deluser1\", testutils.TestAddress(\"deluser1\")))\n\n\t\tdata := ResolveAddress(testutils.TestAddress(\"deluser1\"))\n\t\turequire.NotEqual(t, nil, data)\n\t\tuassert.NoError(t, data.Delete(0, cur))\n\t\tuassert.True(t, data.IsDeleted())\n\t})\n\n\tt.Run(\"non_whitelisted_realm_cannot_delete\", func(cur realm, t *testing.T) {\n\t\tcleanStore(t)\n\n\t\ttesting.SetRealm(testing.NewCodeRealm(controllerPath))\n\t\turequire.NoError(t, RegisterUser(cross(cur), \"deluser2\", testutils.TestAddress(\"deluser2\")))\n\n\t\tdata := ResolveAddress(testutils.TestAddress(\"deluser2\"))\n\t\turequire.NotEqual(t, nil, data)\n\n\t\t// Switch to non-whitelisted realm — Delete should fail\n\t\ttesting.SetRealm(testing.NewCodeRealm(nonControllerPath))\n\t\terr := data.Delete(0, cur)\n\t\tuassert.ErrorContains(t, err, \"does not exist in whitelist\")\n\t\tuassert.False(t, data.IsDeleted()) // not deleted\n\t})\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// IsCurrent before Previous, as UpdateName/Delete below already do. The\n\t// file was inconsistent: those three checked it, these two did not.\n\tif !cur.IsCurrent() {\n\t\treturn NewErrNotWhitelisted(0, cur.Previous())\n\t}\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\t// IsCurrent before Previous, as UpdateName/Delete below already do. The\n\t// file was inconsistent: those three checked it, these two did not.\n\tif !cur.IsCurrent() {\n\t\treturn NewErrNotWhitelisted(0, cur.Previous())\n\t}\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":"store_test.gno","body":"package users\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nvar (\n\talice     = \"alice\"\n\taliceAddr = testutils.TestAddress(alice)\n\tbob       = \"bob\"\n\tbobAddr   = testutils.TestAddress(bob)\n\n\twhitelistedCallerAddr = chain.PackageAddress(initControllerPath)\n)\n\nfunc TestRegister(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(initControllerPath))\n\n\tt.Run(\"valid_registration\", func(t *testing.T) {\n\t\turequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))\n\n\t\tres, isLatest := ResolveName(alice)\n\t\tuassert.Equal(t, aliceAddr, res.Addr())\n\t\tuassert.True(t, isLatest)\n\n\t\tres = ResolveAddress(aliceAddr)\n\t\tuassert.Equal(t, alice, res.Name())\n\t})\n\n\tt.Run(\"invalid_inputs\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\tuassert.ErrorContains(t, RegisterUser(cross(cur), \"\", aliceAddr), ErrEmptyUsername.Error())\n\t\tuassert.ErrorContains(t, RegisterUser(cross(cur), alice, \"\"), ErrInvalidAddress.Error())\n\t\tuassert.ErrorContains(t, RegisterUser(cross(cur), alice, \"invalidaddress\"), ErrInvalidAddress.Error())\n\n\t\tuassert.ErrorContains(t, RegisterUser(cross(cur), \"username with a space\", aliceAddr), ErrInvalidUsername.Error())\n\t\tuassert.ErrorContains(t,\n\t\t\tRegisterUser(cross(cur), \"verylongusernameverylongusernameverylongusernameverylongusername1\", aliceAddr),\n\t\t\tErrInvalidUsername.Error())\n\t\tuassert.ErrorContains(t, RegisterUser(cross(cur), \"namewith^\u0026()\", aliceAddr), ErrInvalidUsername.Error())\n\n\t\t// Lowercase-only enforcement (closes case-confusable squatting via\n\t\t// Alice/alice/ALICE registering as distinct names — see TO_REVIEW\n\t\t// adversarial review). reName mirrors gno's mempackage Re_name shape.\n\t\tuassert.ErrorContains(t, RegisterUser(cross(cur), \"Alice\", aliceAddr), ErrInvalidUsername.Error())\n\t\tuassert.ErrorContains(t, RegisterUser(cross(cur), \"ALICE\", aliceAddr), ErrInvalidUsername.Error())\n\t\tuassert.ErrorContains(t, RegisterUser(cross(cur), \"aLice\", aliceAddr), ErrInvalidUsername.Error())\n\n\t\t// Must start with a lowercase letter. Names starting with a digit,\n\t\t// underscore, or hyphen are rejected.\n\t\tuassert.ErrorContains(t, RegisterUser(cross(cur), \"1alice\", aliceAddr), ErrInvalidUsername.Error())\n\t\tuassert.ErrorContains(t, RegisterUser(cross(cur), \"9\", aliceAddr), ErrInvalidUsername.Error())\n\t\tuassert.ErrorContains(t, RegisterUser(cross(cur), \"_alice\", aliceAddr), ErrInvalidUsername.Error())\n\t\tuassert.ErrorContains(t, RegisterUser(cross(cur), \"-alice\", aliceAddr), ErrInvalidUsername.Error())\n\n\t\t// Cannot end with a separator either (the new mempackage-aligned\n\t\t// regex requires the final char to be in [a-z0-9]).\n\t\tuassert.ErrorContains(t, RegisterUser(cross(cur), \"alice_\", aliceAddr), ErrInvalidUsername.Error())\n\t\tuassert.ErrorContains(t, RegisterUser(cross(cur), \"alice-\", aliceAddr), ErrInvalidUsername.Error())\n\n\t\t// Cannot have consecutive separators in the middle either.\n\t\t// Each separator MUST be followed by at least one alphanumeric.\n\t\tuassert.ErrorContains(t, RegisterUser(cross(cur), \"alice--bob\", aliceAddr), ErrInvalidUsername.Error())\n\t\tuassert.ErrorContains(t, RegisterUser(cross(cur), \"alice__bob\", aliceAddr), ErrInvalidUsername.Error())\n\t\tuassert.ErrorContains(t, RegisterUser(cross(cur), \"alice-_bob\", aliceAddr), ErrInvalidUsername.Error())\n\t\tuassert.ErrorContains(t, RegisterUser(cross(cur), \"alice_-bob\", aliceAddr), ErrInvalidUsername.Error())\n\n\t\t// Length cap of exactly 64 — boundary cases.\n\t\texactly65 := \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\" // 65 chars\n\t\tuassert.ErrorContains(t, RegisterUser(cross(cur), exactly65, aliceAddr), ErrInvalidUsername.Error())\n\t})\n\n\tt.Run(\"valid_edge_cases\", func(t *testing.T) {\n\t\t// Names valid under the mempackage-aligned reName: starts with a\n\t\t// letter, may contain hyphens or underscores in the middle, ends\n\t\t// in [a-z0-9]. The hyphen support is what enables the namereg/v0\n\t\t// `nym-...` prefix shape.\n\t\tcleanStore(t)\n\t\turequire.NoError(t, RegisterUser(cross(cur),\n\t\t\t\"nym-alice123\",\n\t\t\ttestutils.TestAddress(\"u_nym_alice\")))\n\n\t\tcleanStore(t)\n\t\turequire.NoError(t, RegisterUser(cross(cur),\n\t\t\t\"a_b_c\",\n\t\t\ttestutils.TestAddress(\"u_underscore\")))\n\n\t\tcleanStore(t)\n\t\turequire.NoError(t, RegisterUser(cross(cur),\n\t\t\t\"a\",\n\t\t\ttestutils.TestAddress(\"u_singlechar\")))\n\n\t\tcleanStore(t)\n\t\t// 64 chars exactly (length cap is inclusive).\n\t\texactly64 := \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"\n\t\turequire.NoError(t, RegisterUser(cross(cur),\n\t\t\texactly64,\n\t\t\ttestutils.TestAddress(\"u_64\")))\n\t})\n\n\tt.Run(\"addr_already_registered\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))\n\n\t\t// Try registering again\n\t\tuassert.ErrorContains(t, RegisterUser(cross(cur), \"othername\", aliceAddr), ErrAlreadyHasName.Error())\n\t})\n\n\tt.Run(\"name_taken\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))\n\n\t\t// Try registering alice's name with bob's address\n\t\tuassert.ErrorContains(t, RegisterUser(cross(cur), alice, bobAddr), ErrNameTaken.Error())\n\t})\n\n\tt.Run(\"user_deleted\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))\n\t\tdata := ResolveAddress(aliceAddr)\n\t\turequire.NoError(t, data.Delete(0, cur))\n\n\t\t// Try re-registering after deletion\n\t\tuassert.ErrorContains(t, RegisterUser(cross(cur), \"newname\", aliceAddr), ErrDeletedUser.Error())\n\t})\n\n\tt.Run(\"address_lookalike\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\t// Address as username\n\t\tuassert.ErrorContains(t, RegisterUser(cross(cur), \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\", aliceAddr), ErrNameLikeAddress.Error())\n\t\t// Beginning of address as username\n\t\tuassert.ErrorContains(t, RegisterUser(cross(cur), \"g1jg8mtutu9khhfwc4nxmu\", aliceAddr), ErrNameLikeAddress.Error())\n\t\tuassert.NoError(t, RegisterUser(cross(cur), \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5longerthananaddress\", aliceAddr))\n\t})\n}\n\nfunc TestUpdateName(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(initControllerPath))\n\n\tt.Run(\"valid_direct_alias\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))\n\t\tdata := ResolveAddress(aliceAddr)\n\t\t{\n\t\t\ttesting.SetOriginCaller(whitelistedCallerAddr)\n\t\t\tuassert.NoError(t, data.UpdateName(0, cur, \"alice1\"))\n\t\t\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/sys/users\"))\n\t\t}\n\t})\n\n\tt.Run(\"valid_double_alias\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))\n\t\tdata := ResolveAddress(aliceAddr)\n\t\t{\n\t\t\ttesting.SetOriginCaller(whitelistedCallerAddr)\n\t\t\tuassert.NoError(t, data.UpdateName(0, cur, \"alice2\"))\n\t\t\tuassert.NoError(t, data.UpdateName(0, cur, \"alice3\"))\n\t\t\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/sys/users\"))\n\t\t}\n\t\tuassert.Equal(t, ResolveAddress(aliceAddr).username, \"alice3\")\n\t})\n\n\tt.Run(\"name_taken\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))\n\n\t\tdata := ResolveAddress(aliceAddr)\n\t\tuassert.Error(t, data.UpdateName(0, cur, alice), ErrNameTaken.Error())\n\t})\n\n\tt.Run(\"alias_before_name\", func(t *testing.T) {\n\t\tcleanStore(t)\n\t\tdata := ResolveAddress(aliceAddr) // not registered\n\n\t\tuassert.ErrorContains(t, data.UpdateName(0, cur, alice), ErrUserNotExistOrDeleted.Error())\n\t})\n\n\tt.Run(\"alias_after_delete\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))\n\t\tdata := ResolveAddress(aliceAddr)\n\t\t{\n\t\t\turequire.NoError(t, data.Delete(0, cur))\n\t\t\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/sys/users\"))\n\t\t}\n\n\t\tdata = ResolveAddress(aliceAddr)\n\t\t{\n\t\t\tuassert.ErrorContains(t, data.UpdateName(0, cur, \"newalice\"), ErrUserNotExistOrDeleted.Error())\n\t\t\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/sys/users\"))\n\t\t}\n\t})\n\n\t// Audit finding #3: a controller holding a cached *UserData pointer to a\n\t// user that gets deleted between resolve and update must not be able to\n\t// insert a new alias. The alias_after_delete test above re-resolves and\n\t// gets nil, so it only exercises the u==nil branch. This test holds the\n\t// pointer across the delete and verifies the deleted-flag branch.\n\tt.Run(\"alias_with_cached_pointer_after_delete\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))\n\t\t// Cache the pointer BEFORE deletion (simulating a controller that\n\t\t// resolved earlier and held the reference).\n\t\tcached := ResolveAddress(aliceAddr)\n\t\turequire.NotEqual(t, nil, cached)\n\n\t\t// Delete via a fresh resolve.\n\t\tdata := ResolveAddress(aliceAddr)\n\t\t{\n\t\t\turequire.NoError(t, data.Delete(0, cur))\n\t\t\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/sys/users\"))\n\t\t}\n\n\t\t// The cached pointer is non-nil but its .deleted is now true.\n\t\turequire.NotEqual(t, nil, cached)\n\t\tuassert.True(t, cached.IsDeleted())\n\n\t\t// Attempting UpdateName on the cached pointer must reject — without\n\t\t// this check, \"squattedname\" would be inserted into nameStore\n\t\t// pointing at the deleted user, becoming permanently unresolvable\n\t\t// AND unregisterable.\n\t\t{\n\t\t\ttesting.SetOriginCaller(whitelistedCallerAddr)\n\t\t\tuassert.ErrorContains(t, cached.UpdateName(0, cur, \"squattedname\"), ErrUserNotExistOrDeleted.Error())\n\t\t\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/sys/users\"))\n\t\t}\n\n\t\t// Confirm the squat didn't happen: nameStore should NOT have it.\n\t\tuassert.False(t, nameStore.Has(\"squattedname\"))\n\t})\n\n\tt.Run(\"invalid_inputs\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))\n\t\tdata := ResolveAddress(aliceAddr)\n\t\t{\n\t\t\ttesting.SetOriginCaller(whitelistedCallerAddr)\n\t\t\tuassert.ErrorContains(t, data.UpdateName(0, cur, \"\"), ErrEmptyUsername.Error())\n\t\t\tuassert.ErrorContains(t, data.UpdateName(0, cur, \"username with a space\"), ErrInvalidUsername.Error())\n\t\t\tuassert.ErrorContains(t,\n\t\t\t\tdata.UpdateName(0, cur, \"verylongusernameverylongusernameverylongusernameverylongusername1\"),\n\t\t\t\tErrInvalidUsername.Error())\n\t\t\tuassert.ErrorContains(t, data.UpdateName(0, cur, \"namewith^\u0026()\"), ErrInvalidUsername.Error())\n\t\t\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/sys/users\"))\n\t\t}\n\t})\n\n\tt.Run(\"address_lookalike\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))\n\t\tdata := ResolveAddress(aliceAddr)\n\n\t\t{\n\t\t\t// Address as username\n\t\t\tuassert.ErrorContains(t, data.UpdateName(0, cur, \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"), ErrNameLikeAddress.Error())\n\t\t\t// Beginning of address as username\n\t\t\tuassert.ErrorContains(t, data.UpdateName(0, cur, \"g1jg8mtutu9khhfwc4nxmu\"), ErrNameLikeAddress.Error())\n\t\t\tuassert.NoError(t, data.UpdateName(0, cur, \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5longerthananaddress\"))\n\t\t\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/sys/users\"))\n\t\t}\n\t})\n}\n\nfunc TestDelete(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(initControllerPath))\n\n\tt.Run(\"non_existent_user\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\tdata := ResolveAddress(testutils.TestAddress(\"unregistered\"))\n\t\tuassert.ErrorContains(t, data.Delete(0, cur), ErrUserNotExistOrDeleted.Error())\n\t})\n\n\tt.Run(\"double_delete\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))\n\t\tdata := ResolveAddress(aliceAddr)\n\t\turequire.NoError(t, data.Delete(0, cur))\n\t\tdata = ResolveAddress(aliceAddr)\n\t\tuassert.ErrorContains(t, data.Delete(0, cur), ErrUserNotExistOrDeleted.Error())\n\t})\n\n\tt.Run(\"valid_delete\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))\n\t\tdata := ResolveAddress(aliceAddr)\n\t\tuassert.NoError(t, data.Delete(0, cur))\n\n\t\tresolved1, _ := ResolveName(alice)\n\t\tuassert.Equal(t, nil, resolved1)\n\t\tuassert.Equal(t, nil, ResolveAddress(aliceAddr))\n\t})\n}\n\nfunc TestRegisterNotWhitelisted(cur realm, t *testing.T) {\n\tt.Run(\"register_not_whitelisted\", func(t *testing.T) {\n\t\tuassert.ErrorContains(t, RegisterUser(cross(cur), alice, aliceAddr), \"does not exist in whitelist\")\n\t})\n}\n\nfunc TestCanonicalize(cur realm, t *testing.T) {\n\tcases := []struct {\n\t\tin, want string\n\t}{\n\t\t// Single-rule cases.\n\t\t{\"l\", \"i\"}, {\"i\", \"i\"}, {\"1\", \"i\"},\n\t\t{\"0\", \"o\"}, {\"o\", \"o\"},\n\t\t{\"-\", \"\"}, {\".\", \"\"}, {\"_\", \"\"},\n\n\t\t// Identity / pass-through.\n\t\t{\"abc\", \"abc\"}, {\"xyz\", \"xyz\"}, {\"123\", \"i23\"},\n\n\t\t// Open Nym Tier examples from the design doc.\n\t\t{\"nym-vital1k123\", \"nymvitaiiki23\"},\n\t\t{\"nym-vitalik123\", \"nymvitaiiki23\"},\n\t\t{\"nym-vital1k999\", \"nymvitaiik999\"},\n\n\t\t// Same stem, different digit suffix → distinct canonicals.\n\t\t{\"nym-foolbar000\", \"nymfooibarooo\"},\n\t\t{\"nym-foolbar001\", \"nymfooibarooi\"},\n\n\t\t// Three-way separator equivalence.\n\t\t{\"xyz-com\", \"xyzcom\"},\n\t\t{\"xyz_com\", \"xyzcom\"},\n\t\t{\"xyz.com\", \"xyzcom\"},\n\n\t\t// Idempotency: applying twice equals applying once.\n\t\t{\"nymvitaiiki23\", \"nymvitaiiki23\"},\n\n\t\t// Empty input.\n\t\t{\"\", \"\"},\n\t}\n\tfor _, tc := range cases {\n\t\tgot := Canonicalize(tc.in)\n\t\tuassert.Equal(t, tc.want, got)\n\t}\n}\n\nfunc TestCanonicalize_NonASCIIPassthrough(cur realm, t *testing.T) {\n\t// ASCII-only contract: the function passes non-ASCII bytes through\n\t// unchanged. Locks the documented contract — registration paths\n\t// reject non-ASCII upstream via validateName, so this code path is\n\t// only reachable through direct callers from other realms.\n\tuassert.Equal(t, \"café\", Canonicalize(\"café\"))\n\tuassert.Equal(t, \"naïve\", Canonicalize(\"naïve\"))\n}\n\nfunc TestRegisterUser_CanonicalCollision(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(initControllerPath))\n\n\tt.Run(\"non_bypass_blocks\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(cross(cur), \"vitalik\", aliceAddr))\n\t\t// Different exact name, same canonical form → ErrCanonicalCollision.\n\t\tuassert.ErrorContains(t,\n\t\t\tRegisterUser(cross(cur), \"vital1k\", bobAddr),\n\t\t\tErrCanonicalCollision.Error(),\n\t\t)\n\t})\n\n\tt.Run(\"non_bypass_blocks_with_separator_strip\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(cross(cur), \"xyz-com\", aliceAddr))\n\t\tuassert.ErrorContains(t,\n\t\t\tRegisterUser(cross(cur), \"xyz_com\", bobAddr),\n\t\t\tErrCanonicalCollision.Error(),\n\t\t)\n\t})\n\n\tt.Run(\"non_bypass_allows_different_digit_suffix\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\t// Same alpha stem, different digits-after-canonicalization → no collision.\n\t\turequire.NoError(t, RegisterUser(cross(cur), \"nym-foolbar000\", aliceAddr))\n\t\turequire.NoError(t, RegisterUser(cross(cur), \"nym-foolbar999\",\n\t\t\ttestutils.TestAddress(\"foolbar999\")))\n\t})\n\n\tt.Run(\"exact_name_taken_takes_precedence\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(cross(cur), \"vitalik\", aliceAddr))\n\t\t// Same exact name → ErrNameTaken (NOT ErrCanonicalCollision).\n\t\t// The nameStore.Has check precedes the canonical check.\n\t\tuassert.ErrorContains(t,\n\t\t\tRegisterUser(cross(cur), \"vitalik\", bobAddr),\n\t\t\tErrNameTaken.Error(),\n\t\t)\n\t})\n}\n\nfunc TestRegisterUserIgnoreCanonical(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(initControllerPath))\n\n\tt.Run(\"bypass_succeeds_on_canonical_collision\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(cross(cur), \"vitalik\", aliceAddr))\n\t\t// Bypass succeeds even though canonical collides.\n\t\turequire.NoError(t, RegisterUserIgnoreCanonical(cross(cur), \"vital1k\", bobAddr))\n\n\t\t// Both names resolve.\n\t\tres, _ := ResolveName(\"vitalik\")\n\t\tuassert.Equal(t, aliceAddr, res.Addr())\n\t\tres, _ = ResolveName(\"vital1k\")\n\t\tuassert.Equal(t, bobAddr, res.Addr())\n\t})\n\n\tt.Run(\"bypass_later_wins_overwrite\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\t// First registration writes canonical entry.\n\t\turequire.NoError(t, RegisterUser(cross(cur), \"vitalik\", aliceAddr))\n\t\texisting, taken := IsCanonicalTaken(\"vital1k\")\n\t\turequire.True(t, taken)\n\t\tuassert.Equal(t, \"vitalik\", existing)\n\n\t\t// Bypass write overwrites the canonical entry (decision #14).\n\t\turequire.NoError(t, RegisterUserIgnoreCanonical(cross(cur), \"vital1k\", bobAddr))\n\t\texisting, taken = IsCanonicalTaken(\"vitalik\")\n\t\turequire.True(t, taken)\n\t\tuassert.Equal(t, \"vital1k\", existing)\n\t})\n\n\tt.Run(\"bypass_still_blocks_exact_taken\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(cross(cur), \"vitalik\", aliceAddr))\n\t\t// Same exact name still blocked, even via the bypass path.\n\t\tuassert.ErrorContains(t,\n\t\t\tRegisterUserIgnoreCanonical(cross(cur), \"vitalik\", bobAddr),\n\t\t\tErrNameTaken.Error(),\n\t\t)\n\t})\n}\n\nfunc TestUpdateName_CanonicalCollision(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(initControllerPath))\n\n\tt.Run(\"self_collision_blocks_per_decision_15\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\t// Alice registers vital1k. Canonical store maps \"vitaiik\" → \"vital1k\".\n\t\turequire.NoError(t, RegisterUser(cross(cur), \"vital1k\", aliceAddr))\n\t\tdata := ResolveAddress(aliceAddr)\n\n\t\t// Alice attempts to rename to a confusable variant of HER OWN name.\n\t\t// Decision #15: blocked. No self-collision exception in the\n\t\t// non-bypass path.\n\t\ttesting.SetOriginCaller(whitelistedCallerAddr)\n\t\tuassert.ErrorContains(t,\n\t\t\tdata.UpdateName(0, cur, \"vitalik\"),\n\t\t\tErrCanonicalCollision.Error(),\n\t\t)\n\t\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/sys/users\"))\n\t})\n\n\tt.Run(\"different_user_collision_blocks\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(cross(cur), \"vitalik\", aliceAddr))\n\t\turequire.NoError(t, RegisterUser(cross(cur), \"bob\", bobAddr))\n\n\t\tdata := ResolveAddress(bobAddr)\n\t\ttesting.SetOriginCaller(whitelistedCallerAddr)\n\t\tuassert.ErrorContains(t,\n\t\t\tdata.UpdateName(0, cur, \"vital1k\"),\n\t\t\tErrCanonicalCollision.Error(),\n\t\t)\n\t\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/sys/users\"))\n\t})\n\n\tt.Run(\"bypass_allows_self_collision_rename\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(cross(cur), \"vital1k\", aliceAddr))\n\t\tdata := ResolveAddress(aliceAddr)\n\n\t\t// Bypass path allows the rename (DAO grant scenario).\n\t\ttesting.SetOriginCaller(whitelistedCallerAddr)\n\t\turequire.NoError(t, data.UpdateNameIgnoreCanonical(0, cur, \"vitalik\"))\n\t\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/sys/users\"))\n\n\t\t// Both names point to alice; latest is \"vitalik\".\n\t\tuassert.Equal(t, \"vitalik\", ResolveAddress(aliceAddr).Name())\n\t\t// Old name still resolves to alice (decision #4: keep old entry).\n\t\tres, isLatest := ResolveName(\"vital1k\")\n\t\tuassert.Equal(t, aliceAddr, res.Addr())\n\t\tuassert.False(t, isLatest)\n\t})\n}\n\nfunc TestCanonicalEntry_Persists_After_Delete(cur realm, t *testing.T) {\n\t// Decision #5: Delete does NOT remove the canonical entry. Mirrors\n\t// the existing tombstone behavior (anti-revival policy).\n\ttesting.SetRealm(testing.NewCodeRealm(initControllerPath))\n\tcleanStore(t)\n\n\turequire.NoError(t, RegisterUser(cross(cur), \"vitalik\", aliceAddr))\n\tdata := ResolveAddress(aliceAddr)\n\turequire.NoError(t, data.Delete(0, cur))\n\n\t// Canonical entry retained.\n\texisting, taken := IsCanonicalTaken(\"vitalik\")\n\turequire.True(t, taken)\n\tuassert.Equal(t, \"vitalik\", existing)\n\n\t// A different user CANNOT register a confusable variant — the deleted\n\t// user's canonical claim still stands.\n\tuassert.ErrorContains(t,\n\t\tRegisterUser(cross(cur), \"vital1k\", bobAddr),\n\t\tErrCanonicalCollision.Error(),\n\t)\n}\n\nfunc TestCanonicalEntry_Persists_After_UpdateName(cur realm, t *testing.T) {\n\t// Decision #4: UpdateName keeps the OLD canonical entry. Mirrors the\n\t// existing nameStore alias retention (anti-rename-squat policy).\n\ttesting.SetRealm(testing.NewCodeRealm(initControllerPath))\n\tcleanStore(t)\n\n\turequire.NoError(t, RegisterUser(cross(cur), \"alice\", aliceAddr))\n\tdata := ResolveAddress(aliceAddr)\n\ttesting.SetOriginCaller(whitelistedCallerAddr)\n\turequire.NoError(t, data.UpdateName(0, cur, \"alice2\"))\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/sys/users\"))\n\n\t// Both old and new canonical entries exist.\n\texisting, taken := IsCanonicalTaken(\"alice\")\n\turequire.True(t, taken)\n\tuassert.Equal(t, \"alice\", existing)\n\n\texisting, taken = IsCanonicalTaken(\"alice2\")\n\turequire.True(t, taken)\n\tuassert.Equal(t, \"alice2\", existing)\n}\n\n// cleanStore should not be needed, as vm store should be reset after each test.\n// Reference: https://github.com/gnolang/gno/issues/1982\nfunc cleanStore(t *testing.T) {\n\tt.Helper()\n\n\tnameStore = bptree.NewBPTree32()\n\taddressStore = bptree.NewBPTree32()\n\tcanonicalStore = bptree.NewBPTree32()\n}\n"},{"name":"users.gno","body":"package users\n\nimport \"gno.land/p/nt/bptree/rotree/v0\"\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"},{"name":"users_test.gno","body":"package users\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\n// cur is a zero-value realm used as a placeholder when forwarding to\n// uassert/urequire dispatch helpers that gained an `rlm realm` param.\n// These tests pass `func()` callbacks (no crossing inside the callback),\n// so rlm is ignored — a nil realm here is safe.\nvar cur realm\n\nfunc TestResolveName(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(initControllerPath))\n\n\tt.Run(\"single_name\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))\n\n\t\tres, isLatest := ResolveName(alice)\n\t\tuassert.Equal(t, aliceAddr, res.Addr())\n\t\tuassert.Equal(t, alice, res.Name())\n\t\tuassert.True(t, isLatest)\n\t})\n\n\tt.Run(\"name+Alias\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))\n\t\tdata, _ := ResolveName(alice)\n\t\turequire.NoError(t, data.UpdateName(0, cur, \"alice1\"))\n\n\t\tres, isLatest := ResolveName(\"alice1\")\n\t\turequire.NotEqual(t, nil, res)\n\n\t\tuassert.Equal(t, aliceAddr, res.Addr())\n\t\tuassert.Equal(t, \"alice1\", res.Name())\n\t\tuassert.True(t, isLatest)\n\t})\n\n\tt.Run(\"multiple_aliases\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))\n\n\t\t// RegisterUser and check each Alias\n\t\tvar names []string\n\t\tnames = append(names, alice)\n\t\tfor i := 0; i \u003c 5; i++ {\n\t\t\talias := \"alice\" + strconv.Itoa(i)\n\t\t\tnames = append(names, alias)\n\n\t\t\tdata, _ := ResolveName(alice)\n\t\t\turequire.NoError(t, data.UpdateName(0, cur, alias))\n\t\t}\n\n\t\tfor _, alias := range names {\n\t\t\tres, _ := ResolveName(alias)\n\t\t\turequire.NotEqual(t, nil, res)\n\n\t\t\tuassert.Equal(t, aliceAddr, res.Addr())\n\t\t\tuassert.Equal(t, \"alice4\", res.Name())\n\t\t}\n\t})\n}\n\nfunc TestResolveAddress(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(initControllerPath))\n\n\tt.Run(\"single_name\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))\n\n\t\tres := ResolveAddress(aliceAddr)\n\n\t\tuassert.Equal(t, aliceAddr, res.Addr())\n\t\tuassert.Equal(t, alice, res.Name())\n\t})\n\n\tt.Run(\"name+Alias\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))\n\t\tdata, _ := ResolveName(alice)\n\t\turequire.NoError(t, data.UpdateName(0, cur, \"alice1\"))\n\n\t\tres := ResolveAddress(aliceAddr)\n\t\turequire.NotEqual(t, nil, res)\n\n\t\tuassert.Equal(t, aliceAddr, res.Addr())\n\t\tuassert.Equal(t, \"alice1\", res.Name())\n\t})\n\n\tt.Run(\"multiple_aliases\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))\n\n\t\t// RegisterUser and check each Alias\n\t\tvar names []string\n\t\tnames = append(names, alice)\n\n\t\tfor i := 0; i \u003c 5; i++ {\n\t\t\talias := \"alice\" + strconv.Itoa(i)\n\t\t\tnames = append(names, alias)\n\t\t\tdata, _ := ResolveName(alice)\n\t\t\turequire.NoError(t, data.UpdateName(0, cur, alias))\n\t\t}\n\n\t\tres := ResolveAddress(aliceAddr)\n\t\tuassert.Equal(t, aliceAddr, res.Addr())\n\t\tuassert.Equal(t, \"alice4\", res.Name())\n\t})\n}\n\nfunc TestROStores(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(initControllerPath))\n\tcleanStore(t)\n\n\turequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))\n\troNS := GetReadOnlyNameStore()\n\troAS := GetReadonlyAddrStore()\n\n\tt.Run(\"get user data\", func(t *testing.T) {\n\t\t// Name store\n\t\taliceDataRaw := roNS.Get(alice)\n\t\turequire.NotNil(t, aliceDataRaw)\n\n\t\troData, ok := aliceDataRaw.(*UserData)\n\t\tuassert.True(t, ok, \"Could not cast data from RO tree to UserData\")\n\n\t\t// Try to modify data\n\t\troData.Delete(0, cur)\n\t\traw := nameStore.Get(alice)\n\t\tuassert.False(t, raw.(*UserData).deleted)\n\n\t\t// Addr store\n\t\taliceDataRaw = roAS.Get(aliceAddr.String())\n\t\turequire.NotNil(t, aliceDataRaw)\n\n\t\troData, ok = aliceDataRaw.(*UserData)\n\t\tuassert.True(t, ok, \"Could not cast data from RO tree to UserData\")\n\n\t\t// Try to modify data\n\t\troData.Delete(0, cur)\n\t\traw = nameStore.Get(alice)\n\t\tuassert.False(t, raw.(*UserData).deleted)\n\t})\n\n\tt.Run(\"get deleted data\", func(t *testing.T) {\n\t\traw := nameStore.Get(alice)\n\t\taliceData := raw.(*UserData)\n\n\t\turequire.NoError(t, aliceData.Delete(0, cur))\n\t\turequire.True(t, aliceData.IsDeleted())\n\n\t\t// Should be nil because of makeSafeFn intercepting the value.\n\t\trawRoData := roNS.Get(alice)\n\t\tuassert.Equal(t, rawRoData, nil)\n\t\t_, ok := rawRoData.(*UserData) // shouldn't be castable\n\t\tuassert.False(t, ok)\n\t})\n}\n\nfunc TestResolveAny(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(initControllerPath))\n\n\tt.Run(\"name\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))\n\n\t\tres, _ := ResolveAny(alice)\n\n\t\tuassert.Equal(t, aliceAddr, res.Addr())\n\t\tuassert.Equal(t, alice, res.Name())\n\t})\n\n\tt.Run(\"address\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))\n\n\t\tres, _ := ResolveAny(aliceAddr.String())\n\n\t\tuassert.Equal(t, aliceAddr, res.Addr())\n\t\tuassert.Equal(t, alice, res.Name())\n\t})\n\n\tt.Run(\"not_registered\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\tres, _ := ResolveAny(aliceAddr.String())\n\n\t\tuassert.Equal(t, nil, res)\n\t})\n}\n\nfunc TestProposeErrors(cur realm, t *testing.T) {\n\tt.Run(\"propose_register_user_errors\", func(t *testing.T) {\n\t\turequire.PanicsWithMessage(t, cur, ErrInvalidUsername.Error(), func() {\n\t\t\tProposeRegisterUser(cur, \"bad name\", aliceAddr)\n\t\t})\n\t\turequire.PanicsWithMessage(t, cur, ErrInvalidAddress.Error(), func() {\n\t\t\tProposeRegisterUser(cur, alice, \"badaddress\")\n\t\t})\n\t})\n\n\tt.Run(\"propose_update_name_errors\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.PanicsWithMessage(t, cur, ErrInvalidAddress.Error(), func() {\n\t\t\tProposeUpdateName(cur, \"badaddress\", \"alice1\")\n\t\t})\n\t\turequire.PanicsWithMessage(t, cur, ErrInvalidUsername.Error(), func() {\n\t\t\tProposeUpdateName(cur, aliceAddr, \"bad name\")\n\t\t})\n\t\t// Note: unregistered user is not checked at proposal creation time.\n\t\t// The callback handles it at execution time.\n\t})\n\n\tt.Run(\"propose_delete_user_errors\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.PanicsWithMessage(t, cur, ErrInvalidAddress.Error(), func() {\n\t\t\tProposeDeleteUser(cur, \"badaddress\")\n\t\t})\n\t\t// Note: unregistered user is not checked at proposal creation time.\n\t\t// The callback handles it at execution time.\n\t})\n}\n\n// Audit finding #6: ProposeControllerAdditionAndRemoval used to return early\n// from its callback when toAdd was already whitelisted (addToWhitelist\n// returned ErrAlreadyWhitelisted), skipping the remove step entirely. A\n// passed swap proposal would silently leave the old controller active.\n//\n// applyControllerSwap is the extracted callback body. These tests exercise\n// it directly to confirm:\n//   - normal swap (toAdd new, toRemove present) succeeds and updates state\n//   - toAdd already present is benign — remove still happens\n//   - toRemove already absent is benign — add still happens, no error\n//   - errors that aren't the idempotency cases still propagate\nfunc TestApplyControllerSwap(t *testing.T) {\n\t// Use unique addresses per subtest to avoid having to drain the\n\t// controllers set (addrset.Set has no clear/iterate-all method, and\n\t// state persists across subtests in this package's tests).\n\tt.Run(\"normal swap A-\u003eB\", func(t *testing.T) {\n\t\ta := testutils.TestAddress(\"swapA1\")\n\t\tb := testutils.TestAddress(\"swapB1\")\n\t\tcontrollers.Add(a)\n\t\tdefer controllers.Remove(b)\n\n\t\tuassert.NoError(t, applyControllerSwap(b, a))\n\t\tuassert.True(t, controllers.Has(b), \"b should be whitelisted\")\n\t\tuassert.False(t, controllers.Has(a), \"a should be removed\")\n\t})\n\n\tt.Run(\"toAdd already whitelisted, toRemove present\", func(t *testing.T) {\n\t\t// Regression for audit finding #6. Before the fix, this returned\n\t\t// ErrAlreadyWhitelisted from the swap callback, skipping the remove\n\t\t// step entirely — the swap silently no-op'd and the old controller\n\t\t// stayed active.\n\t\ta := testutils.TestAddress(\"swapA2\")\n\t\tb := testutils.TestAddress(\"swapB2\")\n\t\tcontrollers.Add(a)\n\t\tcontrollers.Add(b)\n\t\tdefer controllers.Remove(b)\n\n\t\tuassert.NoError(t, applyControllerSwap(b, a))\n\t\tuassert.True(t, controllers.Has(b), \"b should remain whitelisted\")\n\t\tuassert.False(t, controllers.Has(a), \"a should be removed even though b was already in\")\n\t})\n\n\tt.Run(\"toRemove already absent, toAdd new\", func(t *testing.T) {\n\t\ta := testutils.TestAddress(\"swapA3\")\n\t\tb := testutils.TestAddress(\"swapB3\")\n\t\tc := testutils.TestAddress(\"swapC3\") // never added\n\t\tcontrollers.Add(a)\n\t\tdefer controllers.Remove(a)\n\t\tdefer controllers.Remove(b)\n\n\t\t// Remove c which isn't in the set; should be benign.\n\t\tuassert.NoError(t, applyControllerSwap(b, c))\n\t\tuassert.True(t, controllers.Has(b), \"b should be whitelisted\")\n\t\tuassert.True(t, controllers.Has(a), \"a should still be whitelisted (unchanged)\")\n\t\tuassert.False(t, controllers.Has(c), \"c was never in the set\")\n\t})\n\n\tt.Run(\"both idempotency cases at once\", func(t *testing.T) {\n\t\t// toAdd already in, toRemove already out — should succeed cleanly,\n\t\t// no state change.\n\t\ta := testutils.TestAddress(\"swapA4\")\n\t\tb := testutils.TestAddress(\"swapB4\")\n\t\tc := testutils.TestAddress(\"swapC4\")\n\t\tcontrollers.Add(a)\n\t\tcontrollers.Add(b)\n\t\tdefer controllers.Remove(a)\n\t\tdefer controllers.Remove(b)\n\n\t\tuassert.NoError(t, applyControllerSwap(b, c))\n\t\tuassert.True(t, controllers.Has(a))\n\t\tuassert.True(t, controllers.Has(b))\n\t\tuassert.False(t, controllers.Has(c))\n\t})\n}\n\n// Audit finding #20: the controller whitelist must be queryable from\n// outside the package so operators can monitor authority without\n// source-diving or replaying governance proposals.\nfunc TestControllerQueries(t *testing.T) {\n\tt.Run(\"IsController reflects whitelist state\", func(t *testing.T) {\n\t\ta := testutils.TestAddress(\"queryA1\")\n\t\tb := testutils.TestAddress(\"queryB1\")\n\n\t\t// Before any add: both report false.\n\t\tuassert.False(t, IsController(a))\n\t\tuassert.False(t, IsController(b))\n\n\t\tcontrollers.Add(a)\n\t\tdefer controllers.Remove(a)\n\n\t\tuassert.True(t, IsController(a))\n\t\tuassert.False(t, IsController(b))\n\t})\n\n\tt.Run(\"Controllers returns a snapshot of current whitelist\", func(t *testing.T) {\n\t\t// Use distinct addresses that aren't already in the set from\n\t\t// other tests in this file.\n\t\ta := testutils.TestAddress(\"queryA2\")\n\t\tb := testutils.TestAddress(\"queryB2\")\n\t\tc := testutils.TestAddress(\"queryC2\")\n\n\t\tcontrollers.Add(a)\n\t\tcontrollers.Add(b)\n\t\tcontrollers.Add(c)\n\t\tdefer controllers.Remove(a)\n\t\tdefer controllers.Remove(b)\n\t\tdefer controllers.Remove(c)\n\n\t\tgot := Controllers()\n\n\t\t// All three must appear (regardless of order vs other tests'\n\t\t// leftover entries — we only assert ours are present).\n\t\tseen := map[string]bool{}\n\t\tfor _, e := range got {\n\t\t\tseen[e.String()] = true\n\t\t}\n\t\tuassert.True(t, seen[a.String()], \"snapshot must contain a\")\n\t\tuassert.True(t, seen[b.String()], \"snapshot must contain b\")\n\t\tuassert.True(t, seen[c.String()], \"snapshot must contain c\")\n\t})\n\n\tt.Run(\"Controllers returns a copy — caller mutation does not affect realm\", func(t *testing.T) {\n\t\ta := testutils.TestAddress(\"queryA3\")\n\t\tcontrollers.Add(a)\n\t\tdefer controllers.Remove(a)\n\n\t\tgot := Controllers()\n\t\t// Mutate the returned slice. The realm's controllers set must not\n\t\t// be affected.\n\t\tgot = got[:0]\n\n\t\tuassert.True(t, controllers.Has(a), \"realm state must be unaffected by caller-side slice mutation\")\n\t\tuassert.True(t, IsController(a))\n\t})\n}\n\n// ProposeRegisterUser auto-injects a CANONICAL COLLISION warning into\n// the proposal description when the proposed name canonical-collides\n// with an existing registration. The warning surfaces the colliding\n// existing name to voters; the proposal still goes through if voted in\n// (decision #3, DAO grants always bypass).\nfunc TestProposeRegisterUser_CollisionWarning(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(initControllerPath))\n\n\tt.Run(\"warning_injected_on_collision\", func(t *testing.T) {\n\t\tcleanStore(t)\n\t\turequire.NoError(t, RegisterUser(cross(cur), \"vitalik\", aliceAddr))\n\n\t\treq := ProposeRegisterUser(cur, \"vital1k\", bobAddr)\n\t\tuassert.True(t,\n\t\t\tstrings.Contains(req.Description(), \"CANONICAL COLLISION\"),\n\t\t\t\"description must include collision warning\")\n\t\tuassert.True(t,\n\t\t\tstrings.Contains(req.Description(), \"`vitalik`\"),\n\t\t\t\"description must name the existing colliding registration\")\n\t})\n\n\tt.Run(\"no_warning_when_no_collision\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\treq := ProposeRegisterUser(cur, \"freshname\", aliceAddr)\n\t\tuassert.False(t,\n\t\t\tstrings.Contains(req.Description(), \"CANONICAL COLLISION\"),\n\t\t\t\"description must not include collision warning when name is fresh\")\n\t})\n}\n\n// Note: full execution of ProposeRegisterUser/ProposeUpdateName closures\n// (with ignoreCanonical=true → later-wins overwrite) is exercised end-to-\n// end by the integration test in gno.land/pkg/integration/testdata/.\n// The bypass-write semantics themselves are covered here at the unit\n// level by TestRegisterUserIgnoreCanonical and TestUpdateName_CanonicalCollision.\n\n// TODO Uncomment after gnoweb /u/ page.\n//func TestUserRenderLink(cur realm, t *testing.T) {\n//\ttesting.SetOriginCaller(whitelistedCallerAddr)\n//\tcleanStore(t)\n//\n//\turequire.NoError(t, RegisterUser(alice, aliceAddr))\n//\n//\tdata, _ := ResolveName(alice)\n//\tuassert.Equal(t, data.RenderLink(\"\"), ufmt.Sprintf(\"[@%s](/u/%s)\", alice, alice))\n//\ttext := \"my link text!\"\n//\tuassert.Equal(t, data.RenderLink(text), ufmt.Sprintf(\"[%s](/u/%s)\", text, alice))\n//}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"f2X5e5SRuCePVnEO7J5LN7GT+Syq2Hormfpez80TuTdvyvzufS+DtlwI2lJWY1E26H8Ms0+WnYEPXsq4+FaaZw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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/v0 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/v0).\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/v0.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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"puQwllZiQq1b6LTH9bZ6UT9fPzmC6COfS0XAoYtV4vY2K/lhxH7Ip51WoInwNnmSuPOfiYFxsIAgtX+NXoqw7w=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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/v0\"\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"},{"name":"z_0_a_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_0_a\npackage z_0_a\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/r/archive/boards\"\n)\n\nvar bid boards.BoardID\n\nfunc init(cur realm) {\n\tcaller := testutils.TestAddress(\"caller\")\n\ttesting.SetRealm(testing.NewUserRealm(caller))\n\tbid = boards.CreateBoard(cross(cur), \"test_board\")\n\tboards.CreateThread(cross(cur), bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid := boards.CreateThread(cross(cur), bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n\tboards.CreateReply(cross(cur), bid, pid, pid, \"Reply of the second post\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board\"))\n}\n\n// Error:\n// unauthorized\n"},{"name":"z_0_c_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_0_c\npackage z_0_c\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar bid boards.BoardID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\tboards.CreateThread(cross(cur), 1, \"First Post (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board\"))\n}\n\n// Error:\n// board not exist\n"},{"name":"z_0_d_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_0_d\npackage z_0_d\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar bid boards.BoardID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\tbid = boards.CreateBoard(cross(cur), \"test_board\")\n\tboards.CreateReply(cross(cur), bid, 0, 0, \"Reply of the second post\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board\"))\n}\n\n// Error:\n// thread not exist\n"},{"name":"z_0_e_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_0_e\npackage z_0_e\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar bid boards.BoardID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\tboards.CreateReply(cross(cur), bid, 0, 0, \"Reply of the second post\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board\"))\n}\n\n// Error:\n// board not exist\n"},{"name":"z_0_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_0_filetest\n\npackage z_0_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar bid boards.BoardID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\n\tbid = boards.CreateBoard(cross(cur), \"test_board\")\n\tboards.CreateThread(cross(cur), bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid := boards.CreateThread(cross(cur), bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n\tboards.CreateReply(cross(cur), bid, pid, pid, \"Reply of the second post\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board\"))\n}\n\n// Output:\n// \\[[post](/r/archive/boards$help\u0026func=CreateThread\u0026bid=1)]\n//\n// ----------------------------------------\n// ## [First Post (title)](/r/archive/boards:test_board/1)\n//\n// Body of the first post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm UTC](/r/archive/boards:test_board/1) \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=1\u0026threadid=1)] (0 replies) (0 reposts)\n//\n// ----------------------------------------\n// ## [Second Post (title)](/r/archive/boards:test_board/2)\n//\n// Body of the second post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm UTC](/r/archive/boards:test_board/2) \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=2\u0026threadid=2)] (1 replies) (0 reposts)\n//\n//\n"},{"name":"z_10_a_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_10_a_filetest\n\npackage z_10_a_filetest\n\n// SEND: 1000000ugnot\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\n\tbid = boards.CreateBoard(cross(cur), \"test_board\")\n\tpid = boards.CreateThread(cross(cur), bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main(cur realm) {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\t// boardId 2 not exist\n\tboards.DeletePost(cross(cur), 2, pid, pid, \"\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Error:\n// invalid non-user call\n"},{"name":"z_10_b_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_10_b_filetest\n\npackage z_10_b_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\n\tbid = boards.CreateBoard(cross(cur), \"test_board\")\n\tpid = boards.CreateThread(cross(cur), bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main(cur realm) {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\t// pid of 2 not exist\n\tboards.DeletePost(cross(cur), bid, 2, 2, \"\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Error:\n// invalid non-user call\n"},{"name":"z_10_c_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_10_c_filetest\n\npackage z_10_c_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n\trid boards.PostID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\n\tbid = boards.CreateBoard(cross(cur), \"test_board\")\n\tpid = boards.CreateThread(cross(cur), bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n\trid = boards.CreateReply(cross(cur), bid, pid, pid, \"First reply of the First post\\n\")\n}\n\nfunc main(cur realm) {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\")))\n\tboards.DeletePost(cross(cur), bid, pid, rid, \"\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # First Post in (title)\n//\n// Body of the first post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/archive/boards:test_board/1) \\[[reply](/r/archive/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=1\u0026threadid=1)] \\[[repost](/r/archive/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=1\u0026threadid=1)]\n//\n// \u003e First reply of the First post\n// \u003e\n// \u003e \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/archive/boards:test_board/1/2) \\[[reply](/r/archive/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=2\u0026threadid=1)] \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=2\u0026threadid=1)]\n//\n// ----------------------------------------------------\n// # First Post in (title)\n//\n// Body of the first post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/archive/boards:test_board/1) \\[[reply](/r/archive/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=1\u0026threadid=1)] \\[[repost](/r/archive/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=1\u0026threadid=1)]\n//\n"},{"name":"z_10_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_10_filetest\n\npackage z_10_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\n\tbid = boards.CreateBoard(cross(cur), \"test_board\")\n\tpid = boards.CreateThread(cross(cur), bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main(cur realm) {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\")))\n\tboards.DeletePost(cross(cur), bid, pid, pid, \"\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # First Post in (title)\n//\n// Body of the first post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/archive/boards:test_board/1) \\[[reply](/r/archive/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=1\u0026threadid=1)] \\[[repost](/r/archive/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=1\u0026threadid=1)]\n//\n// ----------------------------------------------------\n// thread does not exist with id: 1\n"},{"name":"z_11_a_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_11_a_filetest\n\npackage z_11_a_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\n\tbid = boards.CreateBoard(cross(cur), \"test_board\")\n\tpid = boards.CreateThread(cross(cur), bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main(cur realm) {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\t// board 2 not exist\n\tboards.EditPost(cross(cur), 2, pid, pid, \"Edited: First Post in (title)\", \"Edited: Body of the first post. (body)\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Error:\n// invalid non-user call\n"},{"name":"z_11_b_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_11_b_filetest\n\npackage z_11_b_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\n\tbid = boards.CreateBoard(cross(cur), \"test_board\")\n\tpid = boards.CreateThread(cross(cur), bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main(cur realm) {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\t// thread 2 not exist\n\tboards.EditPost(cross(cur), bid, 2, pid, \"Edited: First Post in (title)\", \"Edited: Body of the first post. (body)\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Error:\n// invalid non-user call\n"},{"name":"z_11_c_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_11_c_filetest\n\npackage z_11_c_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\n\tbid = boards.CreateBoard(cross(cur), \"test_board\")\n\tpid = boards.CreateThread(cross(cur), bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main(cur realm) {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\t// post 2 not exist\n\tboards.EditPost(cross(cur), bid, pid, 2, \"Edited: First Post in (title)\", \"Edited: Body of the first post. (body)\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Error:\n// invalid non-user call\n"},{"name":"z_11_d_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_11_d_filetest\n\npackage z_11_d_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n\trid boards.PostID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\n\tbid = boards.CreateBoard(cross(cur), \"test_board\")\n\tpid = boards.CreateThread(cross(cur), bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n\trid = boards.CreateReply(cross(cur), bid, pid, pid, \"First reply of the First post\\n\")\n}\n\nfunc main(cur realm) {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\")))\n\tboards.EditPost(cross(cur), bid, pid, rid, \"\", \"Edited: First reply of the First post\\n\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # First Post in (title)\n//\n// Body of the first post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/archive/boards:test_board/1) \\[[reply](/r/archive/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=1\u0026threadid=1)] \\[[repost](/r/archive/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=1\u0026threadid=1)]\n//\n// \u003e First reply of the First post\n// \u003e\n// \u003e \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/archive/boards:test_board/1/2) \\[[reply](/r/archive/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=2\u0026threadid=1)] \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=2\u0026threadid=1)]\n//\n// ----------------------------------------------------\n// # First Post in (title)\n//\n// Body of the first post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/archive/boards:test_board/1) \\[[reply](/r/archive/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=1\u0026threadid=1)] \\[[repost](/r/archive/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=1\u0026threadid=1)]\n//\n// \u003e Edited: First reply of the First post\n// \u003e\n// \u003e \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/archive/boards:test_board/1/2) \\[[reply](/r/archive/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=2\u0026threadid=1)] \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=2\u0026threadid=1)]\n//\n"},{"name":"z_11_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_11_filetest\n\npackage z_11_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\n\tbid = boards.CreateBoard(cross(cur), \"test_board\")\n\tpid = boards.CreateThread(cross(cur), bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main(cur realm) {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\")))\n\tboards.EditPost(cross(cur), bid, pid, pid, \"Edited: First Post in (title)\", \"Edited: Body of the first post. (body)\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # First Post in (title)\n//\n// Body of the first post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/archive/boards:test_board/1) \\[[reply](/r/archive/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=1\u0026threadid=1)] \\[[repost](/r/archive/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=1\u0026threadid=1)]\n//\n// ----------------------------------------------------\n// # Edited: First Post in (title)\n//\n// Edited: Body of the first post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/archive/boards:test_board/1) \\[[reply](/r/archive/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=1\u0026threadid=1)] \\[[repost](/r/archive/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=1\u0026threadid=1)]\n//\n"},{"name":"z_12_a_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_12_a_filetest\n\npackage z_12_a_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\t// create a post via registered user\n\tbid1 := boards.CreateBoard(cross(cur), \"test_board1\")\n\tpid := boards.CreateThread(cross(cur), bid1, \"First Post (title)\", \"Body of the first post. (body)\")\n\tbid2 := boards.CreateBoard(cross(cur), \"test_board2\")\n\n\t// create a repost via anon user\n\ttest2 := testutils.TestAddress(\"test2\")\n\ttesting.SetOriginCaller(test2)\n\ttesting.SetOriginSend(chain.Coins{{\"ugnot\", 9000000}})\n\n\trid := boards.CreateRepost(cross(cur), bid1, pid, \"\", \"Check this out\", bid2)\n\tprintln(rid)\n\tprintln(boards.Render(\"test_board1\"))\n}\n\n// Error:\n// please register, otherwise minimum fee 100000000 is required if anonymous\n"},{"name":"z_12_b_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_12_b_filetest\n\npackage z_12_b_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\tbid1 := boards.CreateBoard(cross(cur), \"test_board1\")\n\tpid := boards.CreateThread(cross(cur), bid1, \"First Post (title)\", \"Body of the first post. (body)\")\n\tbid2 := boards.CreateBoard(cross(cur), \"test_board2\")\n\n\t// create a repost to a non-existing board\n\trid := boards.CreateRepost(cross(cur), 5, pid, \"\", \"Check this out\", bid2)\n\tprintln(rid)\n\tprintln(boards.Render(\"test_board1\"))\n}\n\n// Error:\n// src board not exist\n"},{"name":"z_12_c_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_12_c_filetest\n\npackage z_12_c_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\tbid1 := boards.CreateBoard(cross(cur), \"test_board1\")\n\tboards.CreateThread(cross(cur), bid1, \"First Post (title)\", \"Body of the first post. (body)\")\n\tbid2 := boards.CreateBoard(cross(cur), \"test_board2\")\n\n\t// create a repost to a non-existing thread\n\trid := boards.CreateRepost(cross(cur), bid1, 5, \"\", \"Check this out\", bid2)\n\tprintln(rid)\n\tprintln(boards.Render(\"test_board1\"))\n}\n\n// Error:\n// thread not exist\n"},{"name":"z_12_d_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_12_d_filetest\n\npackage z_12_d_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\tbid1 := boards.CreateBoard(cross(cur), \"test_board1\")\n\tpid := boards.CreateThread(cross(cur), bid1, \"First Post (title)\", \"Body of the first post. (body)\")\n\tboards.CreateBoard(cross(cur), \"test_board2\")\n\n\t// create a repost to a non-existing destination board\n\trid := boards.CreateRepost(cross(cur), bid1, pid, \"\", \"Check this out\", 5)\n\tprintln(rid)\n\tprintln(boards.Render(\"test_board1\"))\n}\n\n// Error:\n// dst board not exist\n"},{"name":"z_12_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_12_filetest\n\npackage z_12_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar (\n\tbid1 boards.BoardID\n\tbid2 boards.BoardID\n\tpid  boards.PostID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\n\tbid1 = boards.CreateBoard(cross(cur), \"test_board1\")\n\tpid = boards.CreateThread(cross(cur), bid1, \"First Post (title)\", \"Body of the first post. (body)\")\n\tbid2 = boards.CreateBoard(cross(cur), \"test_board2\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\")))\n\trid := boards.CreateRepost(cross(cur), bid1, pid, \"\", \"Check this out\", bid2)\n\tprintln(rid)\n\tprintln(boards.Render(\"test_board2\"))\n}\n\n// Output:\n// 1\n// \\[[post](/r/archive/boards$help\u0026func=CreateThread\u0026bid=2)]\n//\n// ----------------------------------------\n// Repost: Check this out\n// ## [First Post (title)](/r/archive/boards:test_board1/1)\n//\n// Body of the first post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm UTC](/r/archive/boards:test_board1/1) \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=1\u0026threadid=1)] (0 replies) (1 reposts)\n//\n//\n"},{"name":"z_1_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_1_filetest\n\npackage z_1_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar board *boards.Board\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\n\t_ = boards.CreateBoard(cross(cur), \"test_board_1\")\n\t_ = boards.CreateBoard(cross(cur), \"test_board_2\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"\"))\n}\n\n// Output:\n// These are all the boards of this realm:\n//\n//  * [/r/archive/boards:test_board_1](/r/archive/boards:test_board_1)\n//  * [/r/archive/boards:test_board_2](/r/archive/boards:test_board_2)\n//\n"},{"name":"z_2_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_2_filetest\n\npackage z_2_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\n\tbid = boards.CreateBoard(cross(cur), \"test_board\")\n\tboards.CreateThread(cross(cur), bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid = boards.CreateThread(cross(cur), bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n\tboards.CreateReply(cross(cur), bid, pid, pid, \"Reply of the second post\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # Second Post (title)\n//\n// Body of the second post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/archive/boards:test_board/2) \\[[reply](/r/archive/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=2\u0026threadid=2)] \\[[repost](/r/archive/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=2)] \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=2\u0026threadid=2)]\n//\n// \u003e Reply of the second post\n// \u003e \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/archive/boards:test_board/2/3) \\[[reply](/r/archive/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=3\u0026threadid=2)] \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=3\u0026threadid=2)]\n//\n"},{"name":"z_3_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_3_filetest\n\npackage z_3_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\n\tbid = boards.CreateBoard(cross(cur), \"test_board\")\n\tboards.CreateThread(cross(cur), bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid = boards.CreateThread(cross(cur), bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\")))\n\trid := boards.CreateReply(cross(cur), bid, pid, pid, \"Reply of the second post\")\n\tprintln(rid)\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// 3\n// # Second Post (title)\n//\n// Body of the second post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/archive/boards:test_board/2) \\[[reply](/r/archive/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=2\u0026threadid=2)] \\[[repost](/r/archive/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=2)] \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=2\u0026threadid=2)]\n//\n// \u003e Reply of the second post\n// \u003e \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/archive/boards:test_board/2/3) \\[[reply](/r/archive/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=3\u0026threadid=2)] \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=3\u0026threadid=2)]\n//\n"},{"name":"z_4_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_4_filetest\n\npackage z_4_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\n\tbid = boards.CreateBoard(cross(cur), \"test_board\")\n\tboards.CreateThread(cross(cur), bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid = boards.CreateThread(cross(cur), bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n\trid := boards.CreateReply(cross(cur), bid, pid, pid, \"Reply of the second post\")\n\tprintln(rid)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\")))\n\trid2 := boards.CreateReply(cross(cur), bid, pid, pid, \"Second reply of the second post\")\n\tprintln(rid2)\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// 3\n// 4\n// # Second Post (title)\n//\n// Body of the second post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/archive/boards:test_board/2) \\[[reply](/r/archive/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=2\u0026threadid=2)] \\[[repost](/r/archive/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=2)] \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=2\u0026threadid=2)]\n//\n// \u003e Reply of the second post\n// \u003e \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/archive/boards:test_board/2/3) \\[[reply](/r/archive/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=3\u0026threadid=2)] \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=3\u0026threadid=2)]\n//\n// \u003e Second reply of the second post\n// \u003e \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/archive/boards:test_board/2/4) \\[[reply](/r/archive/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=4\u0026threadid=2)] \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=4\u0026threadid=2)]\n//\n"},{"name":"z_5_b_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_5_b_filetest\n\npackage z_5_b_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"chain\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nconst admin = address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\t// create board via registered user\n\tbid := boards.CreateBoard(cross(cur), \"test_board\")\n\n\t// create post via anon user\n\ttest2 := testutils.TestAddress(\"test2\")\n\ttesting.SetOriginCaller(test2)\n\ttesting.SetOriginSend(chain.Coins{{\"ugnot\", 9000000}})\n\n\tpid := boards.CreateThread(cross(cur), bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Error:\n// please register, otherwise minimum fee 100000000 is required if anonymous\n"},{"name":"z_5_c_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_5_c_filetest\n\npackage z_5_c_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"chain\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nconst admin = address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\t// create board via registered user\n\tbid := boards.CreateBoard(cross(cur), \"test_board\")\n\n\t// create post via anon user\n\ttest2 := testutils.TestAddress(\"test2\")\n\ttesting.SetOriginCaller(test2)\n\ttesting.SetOriginSend(chain.Coins{{\"ugnot\", 101000000}})\n\n\tpid := boards.CreateThread(cross(cur), bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tboards.CreateReply(cross(cur), bid, pid, pid, \"Reply of the first post\")\n\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # First Post (title)\n//\n// Body of the first post. (body)\n// \\- [g1w3jhxapjta047h6lta047h6lta047h6laqcyu4](/u/g1w3jhxapjta047h6lta047h6lta047h6laqcyu4), [2009-02-13 11:31pm (UTC)](/r/archive/boards:test_board/1) \\[[reply](/r/archive/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=1\u0026threadid=1)] \\[[repost](/r/archive/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=1\u0026threadid=1)]\n//\n// \u003e Reply of the first post\n// \u003e \\- [g1w3jhxapjta047h6lta047h6lta047h6laqcyu4](/u/g1w3jhxapjta047h6lta047h6lta047h6laqcyu4), [2009-02-13 11:31pm (UTC)](/r/archive/boards:test_board/1/2) \\[[reply](/r/archive/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=2\u0026threadid=1)] \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=2\u0026threadid=1)]\n//\n"},{"name":"z_5_d_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_5_d_filetest\n\npackage z_5_d_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"chain\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nconst admin = address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\t// create board via registered user\n\tbid := boards.CreateBoard(cross(cur), \"test_board\")\n\tpid := boards.CreateThread(cross(cur), bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\n\t// create reply via anon user\n\ttest2 := testutils.TestAddress(\"test2\")\n\ttesting.SetOriginCaller(test2)\n\ttesting.SetOriginSend(chain.Coins{{\"ugnot\", 9000000}})\n\tboards.CreateReply(cross(cur), bid, pid, pid, \"Reply of the first post\")\n\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Error:\n// please register, otherwise minimum fee 100000000 is required if anonymous\n"},{"name":"z_5_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_5_filetest\n\npackage z_5_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\n\tbid = boards.CreateBoard(cross(cur), \"test_board\")\n\tboards.CreateThread(cross(cur), bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid = boards.CreateThread(cross(cur), bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n\t_ = boards.CreateReply(cross(cur), bid, pid, pid, \"Reply of the second post\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\")))\n\t_ = boards.CreateReply(cross(cur), bid, pid, pid, \"Second reply of the second post\\n\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # Second Post (title)\n//\n// Body of the second post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/archive/boards:test_board/2) \\[[reply](/r/archive/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=2\u0026threadid=2)] \\[[repost](/r/archive/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=2)] \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=2\u0026threadid=2)]\n//\n// \u003e Reply of the second post\n// \u003e \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/archive/boards:test_board/2/3) \\[[reply](/r/archive/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=3\u0026threadid=2)] \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=3\u0026threadid=2)]\n//\n// \u003e Second reply of the second post\n// \u003e\n// \u003e \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/archive/boards:test_board/2/4) \\[[reply](/r/archive/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=4\u0026threadid=2)] \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=4\u0026threadid=2)]\n//\n"},{"name":"z_6_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_6_filetest\n\npackage z_6_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n\trid boards.PostID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\n\tbid = boards.CreateBoard(cross(cur), \"test_board\")\n\tboards.CreateThread(cross(cur), bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid = boards.CreateThread(cross(cur), bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n\trid = boards.CreateReply(cross(cur), bid, pid, pid, \"Reply of the second post\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\")))\n\tboards.CreateReply(cross(cur), bid, pid, pid, \"Second reply of the second post\\n\")\n\tboards.CreateReply(cross(cur), bid, pid, rid, \"First reply of the first reply\\n\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # Second Post (title)\n//\n// Body of the second post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/archive/boards:test_board/2) \\[[reply](/r/archive/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=2\u0026threadid=2)] \\[[repost](/r/archive/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=2)] \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=2\u0026threadid=2)]\n//\n// \u003e Reply of the second post\n// \u003e \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/archive/boards:test_board/2/3) \\[[reply](/r/archive/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=3\u0026threadid=2)] \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=3\u0026threadid=2)]\n// \u003e\n// \u003e \u003e First reply of the first reply\n// \u003e \u003e\n// \u003e \u003e \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/archive/boards:test_board/2/5) \\[[reply](/r/archive/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=5\u0026threadid=2)] \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=5\u0026threadid=2)]\n//\n// \u003e Second reply of the second post\n// \u003e\n// \u003e \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/archive/boards:test_board/2/4) \\[[reply](/r/archive/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=4\u0026threadid=2)] \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=4\u0026threadid=2)]\n//\n"},{"name":"z_7_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_7_filetest\n\npackage z_7_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nfunc init(cur realm) {\n\t// register\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\n\t// create board and post\n\tbid := boards.CreateBoard(cross(cur), \"test_board\")\n\tboards.CreateThread(cross(cur), bid, \"First Post (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board\"))\n}\n\n// Output:\n// \\[[post](/r/archive/boards$help\u0026func=CreateThread\u0026bid=1)]\n//\n// ----------------------------------------\n// ## [First Post (title)](/r/archive/boards:test_board/1)\n//\n// Body of the first post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm UTC](/r/archive/boards:test_board/1) \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=1\u0026threadid=1)] (0 replies) (0 reposts)\n//\n//\n"},{"name":"z_8_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_8_filetest\n\npackage z_8_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n\trid boards.PostID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\n\tbid = boards.CreateBoard(cross(cur), \"test_board\")\n\tboards.CreateThread(cross(cur), bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid = boards.CreateThread(cross(cur), bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n\trid = boards.CreateReply(cross(cur), bid, pid, pid, \"Reply of the second post\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\")))\n\tboards.CreateReply(cross(cur), bid, pid, pid, \"Second reply of the second post\\n\")\n\trid2 := boards.CreateReply(cross(cur), bid, pid, rid, \"First reply of the first reply\\n\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid)) + \"/\" + strconv.Itoa(int(rid2))))\n}\n\n// Output:\n// _[see thread](/r/archive/boards:test_board/2)_\n//\n// Reply of the second post\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/archive/boards:test_board/2/3) \\[[reply](/r/archive/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=3\u0026threadid=2)] \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=3\u0026threadid=2)]\n//\n// _[see all 1 replies](/r/archive/boards:test_board/2/3)_\n//\n// \u003e First reply of the first reply\n// \u003e\n// \u003e \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/archive/boards:test_board/2/5) \\[[reply](/r/archive/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=5\u0026threadid=2)] \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=5\u0026threadid=2)]\n//\n"},{"name":"z_9_a_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_9_a_filetest\n\npackage z_9_a_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar dstBoard boards.BoardID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\n\tdstBoard = boards.CreateBoard(cross(cur), \"dst_board\")\n}\n\nfunc main(cur realm) {\n\tboards.CreateRepost(cross(cur), 0, 0, \"First Post in (title)\", \"Body of the first post. (body)\", dstBoard)\n}\n\n// Error:\n// invalid non-user call\n"},{"name":"z_9_b_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_9_b_filetest\n\npackage z_9_b_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar (\n\tsrcBoard boards.BoardID\n\tpid      boards.PostID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\n\tsrcBoard = boards.CreateBoard(cross(cur), \"first_board\")\n\tpid = boards.CreateThread(cross(cur), srcBoard, \"First Post in (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main(cur realm) {\n\tboards.CreateRepost(cross(cur), srcBoard, pid, \"First Post in (title)\", \"Body of the first post. (body)\", 0)\n}\n\n// Error:\n// invalid non-user call\n"},{"name":"z_9_filetest.gno","body":"// PKGPATH: gno.land/r/archive/boards/filetests/z_9_filetest\n\npackage z_9_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/archive/boards\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar (\n\tfirstBoard  boards.BoardID\n\tsecondBoard boards.BoardID\n\tpid         boards.PostID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\n\tfirstBoard = boards.CreateBoard(cross(cur), \"first_board\")\n\tsecondBoard = boards.CreateBoard(cross(cur), \"second_board\")\n\tpid = boards.CreateThread(cross(cur), firstBoard, \"First Post in (title)\", \"Body of the first post. (body)\")\n\n\tboards.CreateRepost(cross(cur), firstBoard, pid, \"First Post in (title)\", \"Body of the first post. (body)\", secondBoard)\n}\n\nfunc main() {\n\tprintln(boards.Render(\"second_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # First Post in (title)\n//\n// Body of the first post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/archive/boards:second_board/1/1) \\[[reply](/r/archive/boards$help\u0026func=CreateReply\u0026bid=2\u0026postid=1\u0026threadid=1)] \\[[x](/r/archive/boards$help\u0026func=DeletePost\u0026bid=2\u0026postid=1\u0026threadid=1)]\n//\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"8rFW1hi7FINeo/y53ATf3k/qdeENdWroOwoNSLfc0AUmu0whbKcKCqdUSJQRnvgBNHJmUZBUP17IhlreWvG/3w=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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":"echo_test.gno","body":"package echo\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc Test(t *testing.T) {\n\turequire.Equal(t, \"aa\", Render(\"aa\"))\n\turequire.Equal(t, \"\", Render(\"\"))\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/archive/echo\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"KrkDDwSoLk0Qck+cDYAxXlImsvPKhKv1qCE7i23m8T51+fBThrA+kBUncbT8kxCZ1pv/3Q6g8LWmJpbaKpuXhQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"hL9k+V/L/disrBvMTHD4o2TEqpduoxFRC0c132IITjgP+1XqK9C9JypAQpe+0ilB71PQQoHR/5JDXXfJQiCVuQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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":"counter_test.gno","body":"package counter\n\nimport \"testing\"\n\nfunc TestIncrement(cur realm, t *testing.T) {\n\tcounter = 0\n\tval := Increment(cross(cur))\n\tif val != 1 {\n\t\tt.Fatalf(\"result from Increment(): %d != 1\", val)\n\t}\n\tif counter != val {\n\t\tt.Fatalf(\"counter (%d) != val (%d)\", counter, val)\n\t}\n}\n\nfunc TestRender(t *testing.T) {\n\tcounter = 1337\n\tres := Render(\"\")\n\tif res != \"1337\" {\n\t\tt.Fatalf(\"render result %q != %q\", res, \"1337\")\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/demo/counter\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"0LVEKavUGumXlPXCrPAuOXgRU0m3dcgViUmpVLlzwBoieaZ3i2n+4VV7/6+tOyDBzLmeIaAK57ccNf7JYkMbvg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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.\n//\n// It is built on gno.land/p/nt/grc20/v1 (uint256 amounts) and registered in\n// gno.land/r/nt/grc20reg/v1.\npackage test20\n\nimport (\n\t\"gno.land/p/nt/grc20/v1\"\n\t\"gno.land/r/nt/grc20reg/v1\"\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"crGu21mVECnkcstlWP1BqksxlEDPoi5Iuh10x/eUs4J47hPpPVXdOEUyCc8yi44vm3fFL46R8Pdxr0nG9XVCtA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"atomicswap","path":"gno.land/r/demo/defi/atomicswap","files":[{"name":"amount_test.gno","body":"package atomicswap\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n)\n\n// equal256 and require256 compare 256-bit amounts natively; uassert and\n// urequire do not support uint256 (see p/nt/ufmt's README).\nfunc equal256(t *testing.T, want, got uint256, msgs ...string) bool {\n\tt.Helper()\n\tif want == got {\n\t\treturn true\n\t}\n\tt.Errorf(\"%swant %s, got %s\", msgPrefix(msgs), strconv.FormatUint256(want, 10), strconv.FormatUint256(got, 10))\n\treturn false\n}\n\nfunc require256(t *testing.T, want, got uint256, msgs ...string) {\n\tt.Helper()\n\tif want != got {\n\t\tt.Fatalf(\"%swant %s, got %s\", msgPrefix(msgs), strconv.FormatUint256(want, 10), strconv.FormatUint256(got, 10))\n\t}\n}\n\nfunc msgPrefix(msgs []string) string {\n\tif len(msgs) == 0 {\n\t\treturn \"\"\n\t}\n\treturn msgs[0] + \": \"\n}\n"},{"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/nt/avl/v0\"\n\t\"gno.land/p/nt/grc20/v1\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/nt/grc20reg/v1\"\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/nt/grc20reg/v1 to lookup for a registered token, so the\n// token must be a gno.land/p/nt/grc20/v1 (uint256) 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 := strconv.FormatUint256(allowance, 10) + 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":"atomicswap_test.gno","body":"package atomicswap\n\nimport (\n\t\"chain\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/r/tests/vm/test20\"\n)\n\nfunc TestNewCustomCoinSwap_Claim(cur realm, t *testing.T) {\n\tdefer resetTestState()\n\n\t// Setup\n\tpkgAddr := chain.PackageAddress(\"gno.land/r/demo/defi/atomicswap\")\n\tsender := testutils.TestAddress(\"sender1\")\n\trecipient := testutils.TestAddress(\"recipient1\")\n\tamount := chain.Coins{{Denom: \"ugnot\", Amount: 1}}\n\thashlock := sha256.Sum256([]byte(\"secret\"))\n\thashlockHex := hex.EncodeToString(hashlock[:])\n\ttimelock := time.Now().Add(1 * time.Hour)\n\ttesting.IssueCoins(pkgAddr, chain.Coins{{\"ugnot\", 100000000}})\n\n\t// Create a new swap\n\ttesting.SetRealm(testing.NewUserRealm(sender))\n\ttesting.SetOriginSend(amount)\n\tid, swap := NewCustomCoinSwap(cross(cur), recipient, hashlockHex, timelock)\n\tuassert.Equal(t, 1, id)\n\n\texpected := `- status: active\n- sender: g1wdjkuer9wgc47h6lta047h6lta047h6l56jtjc\n- recipient: g1wfjkx6tsd9jkuap3ta047h6lta047h6lkk20gv\n- amount: 1ugnot\n- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b\n- timelock: 2009-02-14T00:31:30Z\n- remaining: 1h0m0s`\n\tuassert.Equal(t, expected, swap.String())\n\tuassert.Equal(t, expected, Render(\"1\"))\n\n\t// Test initial state\n\tuassert.Equal(t, sender, swap.sender, \"expected sender to match\")\n\tuassert.Equal(t, recipient, swap.recipient, \"expected recipient to match\")\n\tuassert.Equal(t, swap.amountStr, amount.String(), \"expected amount to match\")\n\tuassert.Equal(t, hashlockHex, swap.hashlock, \"expected hashlock to match\")\n\tuassert.True(t, swap.timelock.Equal(timelock), \"expected timelock to match\")\n\tuassert.False(t, swap.claimed, \"expected claimed to be false\")\n\tuassert.False(t, swap.refunded, \"expected refunded to be false\")\n\n\t// Test claim — recipient calls via the public wrapper.\n\ttesting.SetRealm(testing.NewUserRealm(recipient))\n\tuassert.AbortsWithMessage(t, cur, \"invalid preimage\", func() { Claim(cross(cur), id, \"invalid\") })\n\tClaim(cross(cur), id, \"secret\")\n\tuassert.True(t, swap.claimed, \"expected claimed to be true\")\n\n\t// Test refund (should fail because already claimed)\n\tuassert.AbortsWithMessage(t, cur, \"already claimed\", func() { Refund(cross(cur), id) })\n\tuassert.AbortsWithMessage(t, cur, \"already claimed\", func() { Claim(cross(cur), id, \"secret\") })\n\n\texpected = `- status: claimed\n- sender: g1wdjkuer9wgc47h6lta047h6lta047h6l56jtjc\n- recipient: g1wfjkx6tsd9jkuap3ta047h6lta047h6lkk20gv\n- amount: 1ugnot\n- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b\n- timelock: 2009-02-14T00:31:30Z\n- remaining: 1h0m0s`\n\tuassert.Equal(t, expected, swap.String())\n\tuassert.Equal(t, expected, Render(\"1\"))\n}\n\nfunc TestNewCustomCoinSwap_Refund(cur realm, t *testing.T) {\n\tdefer resetTestState()\n\n\t// Setup\n\tpkgAddr := chain.PackageAddress(\"gno.land/r/demo/defi/atomicswap\")\n\tsender := testutils.TestAddress(\"sender2\")\n\trecipient := testutils.TestAddress(\"recipient2\")\n\tamount := chain.Coins{{Denom: \"ugnot\", Amount: 1}}\n\thashlock := sha256.Sum256([]byte(\"secret\"))\n\thashlockHex := hex.EncodeToString(hashlock[:])\n\ttimelock := time.Now().Add(1 * time.Hour)\n\n\t// Create a new swap\n\ttesting.SetRealm(testing.NewUserRealm(sender))\n\ttesting.SetOriginSend(amount)\n\tid, swap := NewCustomCoinSwap(cross(cur), recipient, hashlockHex, timelock)\n\tuassert.Equal(t, 1, id)\n\n\texpected := `- status: active\n- sender: g1wdjkuer9wge97h6lta047h6lta047h6ltfacad\n- recipient: g1wfjkx6tsd9jkuapjta047h6lta047h6lducc3v\n- amount: 1ugnot\n- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b\n- timelock: 2009-02-14T00:31:30Z\n- remaining: 1h0m0s`\n\tuassert.Equal(t, expected, swap.String())\n\tuassert.Equal(t, expected, Render(\"1\"))\n\n\t// Test Refund — sender calls via the public wrapper.\n\ttesting.IssueCoins(pkgAddr, chain.Coins{{\"ugnot\", 100000000}})\n\tuassert.AbortsWithMessage(t, cur, \"timelock not expired\", func() { Refund(cross(cur), id) })\n\tswap.timelock = time.Now().Add(-1 * time.Hour) // override timelock\n\tRefund(cross(cur), id)\n\tuassert.True(t, swap.refunded, \"expected refunded to be true\")\n\n\texpected = `- status: refunded\n- sender: g1wdjkuer9wge97h6lta047h6lta047h6ltfacad\n- recipient: g1wfjkx6tsd9jkuapjta047h6lta047h6lducc3v\n- amount: 1ugnot\n- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b\n- timelock: 2009-02-13T22:31:30Z\n- remaining: 0s`\n\tuassert.Equal(t, expected, swap.String())\n\tuassert.Equal(t, expected, Render(\"1\"))\n}\n\nfunc TestNewCustomGRC20Swap_Claim(cur realm, t *testing.T) {\n\tdefer resetTestState()\n\n\t// Setup\n\tsender := testutils.TestAddress(\"sender3\")\n\trecipient := testutils.TestAddress(\"recipient3\")\n\trlm := chain.PackageAddress(\"gno.land/r/demo/defi/atomicswap\")\n\thashlock := sha256.Sum256([]byte(\"secret\"))\n\thashlockHex := hex.EncodeToString(hashlock[:])\n\ttimelock := time.Now().Add(1 * time.Hour)\n\n\ttest20.PrivateLedger.Mint(sender, 100_000)\n\ttest20.PrivateLedger.Approve(sender, rlm, 70_000)\n\n\t// Create a new swap\n\ttesting.SetRealm(testing.NewUserRealm(sender))\n\tid, swap := NewCustomGRC20Swap(cross(cur), recipient, hashlockHex, timelock, test20.Token)\n\tuassert.Equal(t, 1, id)\n\n\texpected := `- status: active\n- sender: g1wdjkuer9wge47h6lta047h6lta047h6l5rk38l\n- recipient: g1wfjkx6tsd9jkuapnta047h6lta047h6ly6k4pv\n- amount: 70000TST\n- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b\n- timelock: 2009-02-14T00:31:30Z\n- remaining: 1h0m0s`\n\n\tuassert.Equal(t, expected, swap.String())\n\tuassert.Equal(t, expected, Render(\"1\"))\n\n\t// Test initial state\n\tuassert.Equal(t, sender, swap.sender, \"expected sender to match\")\n\tuassert.Equal(t, recipient, swap.recipient, \"expected recipient to match\")\n\tbal := test20.Token.BalanceOf(sender)\n\tequal256(t, uint256(30_000), bal)\n\tbal = test20.Token.BalanceOf(rlm)\n\tequal256(t, uint256(70_000), bal)\n\tbal = test20.Token.BalanceOf(recipient)\n\tequal256(t, uint256(0), bal)\n\n\tuassert.Equal(t, hashlockHex, swap.hashlock, \"expected hashlock to match\")\n\tuassert.True(t, swap.timelock.Equal(timelock), \"expected timelock to match\")\n\tuassert.False(t, swap.claimed, \"expected claimed to be false\")\n\tuassert.False(t, swap.refunded, \"expected refunded to be false\")\n\n\t// Test claim\n\ttesting.SetRealm(testing.NewUserRealm(recipient))\n\tuassert.AbortsWithMessage(t, cur, \"invalid preimage\", func() { Claim(cross(cur), id, \"invalid\") })\n\tClaim(cross(cur), id, \"secret\")\n\tuassert.True(t, swap.claimed, \"expected claimed to be true\")\n\n\tbal = test20.Token.BalanceOf(sender)\n\tequal256(t, uint256(30_000), bal)\n\tbal = test20.Token.BalanceOf(rlm)\n\tequal256(t, uint256(0), bal)\n\tbal = test20.Token.BalanceOf(recipient)\n\tequal256(t, uint256(70_000), bal)\n\n\t// Test refund (should fail because already claimed)\n\tuassert.AbortsWithMessage(t, cur, \"already claimed\", func() { Refund(cross(cur), id) })\n\tuassert.AbortsWithMessage(t, cur, \"already claimed\", func() { Claim(cross(cur), id, \"secret\") })\n\n\texpected = `- status: claimed\n- sender: g1wdjkuer9wge47h6lta047h6lta047h6l5rk38l\n- recipient: g1wfjkx6tsd9jkuapnta047h6lta047h6ly6k4pv\n- amount: 70000TST\n- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b\n- timelock: 2009-02-14T00:31:30Z\n- remaining: 1h0m0s`\n\tuassert.Equal(t, expected, swap.String())\n\tuassert.Equal(t, expected, Render(\"1\"))\n}\n\nfunc TestNewCustomGRC20Swap_Refund(cur realm, t *testing.T) {\n\tdefer resetTestState()\n\n\t// Setup\n\tpkgAddr := chain.PackageAddress(\"gno.land/r/demo/defi/atomicswap\")\n\tsender := testutils.TestAddress(\"sender5\")\n\trecipient := testutils.TestAddress(\"recipient5\")\n\trlm := chain.PackageAddress(\"gno.land/r/demo/defi/atomicswap\")\n\thashlock := sha256.Sum256([]byte(\"secret\"))\n\thashlockHex := hex.EncodeToString(hashlock[:])\n\ttimelock := time.Now().Add(1 * time.Hour)\n\n\ttest20.PrivateLedger.Mint(sender, 100_000)\n\ttest20.PrivateLedger.Approve(sender, rlm, 70_000)\n\n\t// Create a new swap\n\ttesting.SetRealm(testing.NewUserRealm(sender))\n\tid, swap := NewCustomGRC20Swap(cross(cur), recipient, hashlockHex, timelock, test20.Token)\n\tuassert.Equal(t, 1, id)\n\n\texpected := `- status: active\n- sender: g1wdjkuer9wg647h6lta047h6lta047h6l5p6k3k\n- recipient: g1wfjkx6tsd9jkuap4ta047h6lta047h6lmwmj6v\n- amount: 70000TST\n- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b\n- timelock: 2009-02-14T00:31:30Z\n- remaining: 1h0m0s`\n\tuassert.Equal(t, expected, swap.String())\n\tuassert.Equal(t, expected, Render(\"1\"))\n\n\t// Test initial state\n\tuassert.Equal(t, sender, swap.sender, \"expected sender to match\")\n\tuassert.Equal(t, recipient, swap.recipient, \"expected recipient to match\")\n\tbal := test20.Token.BalanceOf(sender)\n\tequal256(t, uint256(30_000), bal)\n\tbal = test20.Token.BalanceOf(rlm)\n\tequal256(t, uint256(70_000), bal)\n\tbal = test20.Token.BalanceOf(recipient)\n\tequal256(t, uint256(0), bal)\n\n\t// Test Refund — sender is still the current realm.\n\ttesting.IssueCoins(pkgAddr, chain.Coins{{\"ugnot\", 100000000}})\n\tuassert.AbortsWithMessage(t, cur, \"timelock not expired\", func() { Refund(cross(cur), id) })\n\n\tswap.timelock = time.Now().Add(-1 * time.Hour) // override timelock\n\tRefund(cross(cur), id)\n\tuassert.True(t, swap.refunded, \"expected refunded to be true\")\n\n\tbal = test20.Token.BalanceOf(sender)\n\tequal256(t, uint256(100_000), bal)\n\tbal = test20.Token.BalanceOf(rlm)\n\tequal256(t, uint256(0), bal)\n\tbal = test20.Token.BalanceOf(recipient)\n\tequal256(t, uint256(0), bal)\n\n\texpected = `- status: refunded\n- sender: g1wdjkuer9wg647h6lta047h6lta047h6l5p6k3k\n- recipient: g1wfjkx6tsd9jkuap4ta047h6lta047h6lmwmj6v\n- amount: 70000TST\n- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b\n- timelock: 2009-02-13T22:31:30Z\n- remaining: 0s`\n\tuassert.Equal(t, expected, swap.String())\n\tuassert.Equal(t, expected, Render(\"1\"))\n}\n\nfunc TestNewGRC20Swap_Claim(cur realm, t *testing.T) {\n\tdefer resetTestState()\n\n\t// Setup\n\tsender := testutils.TestAddress(\"sender4\")\n\trecipient := testutils.TestAddress(\"recipient4\")\n\trlm := chain.PackageAddress(\"gno.land/r/demo/defi/atomicswap\")\n\thashlock := sha256.Sum256([]byte(\"secret\"))\n\thashlockHex := hex.EncodeToString(hashlock[:])\n\ttimelock := time.Now().Add(defaultTimelockDuration)\n\n\ttest20.PrivateLedger.Mint(sender, 100_000)\n\ttest20.PrivateLedger.Approve(sender, rlm, 70_000)\n\n\t// Create a new swap\n\ttesting.SetRealm(testing.NewUserRealm(sender))\n\tid, swap := NewGRC20Swap(cross(cur), recipient, hashlockHex, \"gno.land/r/tests/vm/test20.TST\")\n\tuassert.Equal(t, 1, id)\n\n\texpected := `- status: active\n- sender: g1wdjkuer9wg697h6lta047h6lta047h6ltt3lty\n- recipient: g1wfjkx6tsd9jkuap5ta047h6lta047h6ljg4l2v\n- amount: 70000TST\n- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b\n- timelock: 2009-02-20T23:31:30Z\n- remaining: 168h0m0s`\n\tuassert.Equal(t, expected, swap.String())\n\tuassert.Equal(t, expected, Render(\"1\"))\n\n\t// Test initial state\n\tuassert.Equal(t, sender, swap.sender, \"expected sender to match\")\n\tuassert.Equal(t, recipient, swap.recipient, \"expected recipient to match\")\n\tbal := test20.Token.BalanceOf(sender)\n\tequal256(t, uint256(30_000), bal)\n\tbal = test20.Token.BalanceOf(rlm)\n\tequal256(t, uint256(70_000), bal)\n\tbal = test20.Token.BalanceOf(recipient)\n\tequal256(t, uint256(0), bal)\n\n\tuassert.Equal(t, hashlockHex, swap.hashlock, \"expected hashlock to match\")\n\tuassert.True(t, swap.timelock.Equal(timelock), \"expected timelock to match\")\n\tuassert.False(t, swap.claimed, \"expected claimed to be false\")\n\tuassert.False(t, swap.refunded, \"expected refunded to be false\")\n\n\t// Test claim\n\ttesting.SetRealm(testing.NewUserRealm(recipient))\n\tuassert.AbortsWithMessage(t, cur, \"invalid preimage\", func() { Claim(cross(cur), id, \"invalid\") })\n\tClaim(cross(cur), id, \"secret\")\n\tuassert.True(t, swap.claimed, \"expected claimed to be true\")\n\n\tbal = test20.Token.BalanceOf(sender)\n\tequal256(t, uint256(30_000), bal)\n\tbal = test20.Token.BalanceOf(rlm)\n\tequal256(t, uint256(0), bal)\n\tbal = test20.Token.BalanceOf(recipient)\n\tequal256(t, uint256(70_000), bal)\n\n\t// Test refund (should fail because already claimed)\n\tuassert.AbortsWithMessage(t, cur, \"already claimed\", func() { Refund(cross(cur), id) })\n\tuassert.AbortsWithMessage(t, cur, \"already claimed\", func() { Claim(cross(cur), id, \"secret\") })\n\n\texpected = `- status: claimed\n- sender: g1wdjkuer9wg697h6lta047h6lta047h6ltt3lty\n- recipient: g1wfjkx6tsd9jkuap5ta047h6lta047h6ljg4l2v\n- amount: 70000TST\n- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b\n- timelock: 2009-02-20T23:31:30Z\n- remaining: 168h0m0s`\n\tuassert.Equal(t, expected, swap.String())\n\tuassert.Equal(t, expected, Render(\"1\"))\n}\n\nfunc TestNewGRC20Swap_Refund(cur realm, t *testing.T) {\n\tdefer resetTestState()\n\n\t// Setup\n\tpkgAddr := chain.PackageAddress(\"gno.land/r/demo/defi/atomicswap\")\n\tsender := testutils.TestAddress(\"sender6\")\n\trecipient := testutils.TestAddress(\"recipient6\")\n\trlm := chain.PackageAddress(\"gno.land/r/demo/defi/atomicswap\")\n\thashlock := sha256.Sum256([]byte(\"secret\"))\n\thashlockHex := hex.EncodeToString(hashlock[:])\n\n\ttest20.PrivateLedger.Mint(sender, 100_000)\n\ttest20.PrivateLedger.Approve(sender, rlm, 70_000)\n\n\t// Create a new swap\n\ttesting.SetRealm(testing.NewUserRealm(sender))\n\tid, swap := NewGRC20Swap(cross(cur), recipient, hashlockHex, \"gno.land/r/tests/vm/test20.TST\")\n\tuassert.Equal(t, 1, id)\n\n\texpected := `- status: active\n- sender: g1wdjkuer9wgm97h6lta047h6lta047h6ltj497r\n- recipient: g1wfjkx6tsd9jkuapkta047h6lta047h6lqyf9rv\n- amount: 70000TST\n- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b\n- timelock: 2009-02-20T23:31:30Z\n- remaining: 168h0m0s`\n\tuassert.Equal(t, expected, swap.String())\n\tuassert.Equal(t, expected, Render(\"1\"))\n\n\t// Test initial state\n\tuassert.Equal(t, sender, swap.sender, \"expected sender to match\")\n\tuassert.Equal(t, recipient, swap.recipient, \"expected recipient to match\")\n\tbal := test20.Token.BalanceOf(sender)\n\tequal256(t, uint256(30_000), bal)\n\tbal = test20.Token.BalanceOf(rlm)\n\tequal256(t, uint256(70_000), bal)\n\tbal = test20.Token.BalanceOf(recipient)\n\tequal256(t, uint256(0), bal)\n\n\t// Test Refund — sender is still the current realm.\n\ttesting.IssueCoins(pkgAddr, chain.Coins{{\"ugnot\", 100000000}})\n\tuassert.AbortsWithMessage(t, cur, \"timelock not expired\", func() { Refund(cross(cur), id) })\n\n\tswap.timelock = time.Now().Add(-1 * time.Hour) // override timelock\n\tRefund(cross(cur), id)\n\tuassert.True(t, swap.refunded, \"expected refunded to be true\")\n\n\tbal = test20.Token.BalanceOf(sender)\n\tequal256(t, uint256(100_000), bal)\n\tbal = test20.Token.BalanceOf(rlm)\n\tequal256(t, uint256(0), bal)\n\tbal = test20.Token.BalanceOf(recipient)\n\tequal256(t, uint256(0), bal)\n\n\texpected = `- status: refunded\n- sender: g1wdjkuer9wgm97h6lta047h6lta047h6ltj497r\n- recipient: g1wfjkx6tsd9jkuapkta047h6lta047h6lqyf9rv\n- amount: 70000TST\n- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b\n- timelock: 2009-02-13T22:31:30Z\n- remaining: 0s`\n\tuassert.Equal(t, expected, swap.String())\n\tuassert.Equal(t, expected, Render(\"1\"))\n}\n\nfunc TestRender(cur realm, t *testing.T) {\n\tdefer resetTestState()\n\n\t// Setup\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\tcharly := testutils.TestAddress(\"charly\")\n\trlm := chain.PackageAddress(\"gno.land/r/demo/defi/atomicswap\")\n\thashlock := sha256.Sum256([]byte(\"secret\"))\n\thashlockHex := hex.EncodeToString(hashlock[:])\n\ttimelock := time.Now().Add(1 * time.Hour)\n\n\ttest20.PrivateLedger.Mint(alice, 100_000)\n\t// SetRealm mutates the test's cur in place to look like alice's\n\t// userRealm. cur's HIV pointer is unchanged so IsCurrent still passes;\n\t// RealmTeller(0, cur) binds to alice via cur.Address().\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\n\tuserTeller := test20.Token.RealmTeller(0, cur)\n\tuserTeller.Approve(0, cur, rlm, 10_000)\n\tbobSwapID, _ := NewCustomGRC20Swap(cross(cur), bob, hashlockHex, timelock, test20.Token)\n\n\tuserTeller.Approve(0, cur, rlm, 20_000)\n\t_, _ = NewCustomGRC20Swap(cross(cur), charly, hashlockHex, timelock, test20.Token)\n\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\tClaim(cross(cur), bobSwapID, \"secret\")\n\texpected := `- 2: g1v9kxjcm9ta047h6lta047h6lta047h6lzd40gh -(20000TST)\u003e g1vd5xzunv09047h6lta047h6lta047h6lhsyveh - active\n- 1: g1v9kxjcm9ta047h6lta047h6lta047h6lzd40gh -(10000TST)\u003e g1vfhkyh6lta047h6lta047h6lta047h6l03vdhu - claimed\n`\n\tuassert.Equal(t, expected, Render(\"\"))\n}\n\n// TestGRC20SwapAbove2Pow63 locks and claims an amount no int64 token can hold.\nfunc TestGRC20SwapAbove2Pow63(cur realm, t *testing.T) {\n\tdefer resetTestState()\n\n\tsender := testutils.TestAddress(\"sender_big\")\n\trecipient := testutils.TestAddress(\"recipient_big\")\n\trlm := chain.PackageAddress(\"gno.land/r/demo/defi/atomicswap\")\n\thashlock := sha256.Sum256([]byte(\"secret\"))\n\thashlockHex := hex.EncodeToString(hashlock[:])\n\tbig := uint256(1)\u003c\u003c128 + 1\n\n\t// Earlier tests can leave tokens locked in the swap realm.\n\theld := test20.Token.BalanceOf(rlm)\n\ttest20.PrivateLedger.Mint(sender, big)\n\ttest20.PrivateLedger.Approve(sender, rlm, big)\n\n\ttesting.SetRealm(testing.NewUserRealm(sender))\n\tid, swap := NewGRC20Swap(cross(cur), recipient, hashlockHex, \"gno.land/r/tests/vm/test20.TST\")\n\tuassert.Equal(t, \"340282366920938463463374607431768211457TST\", swap.amountStr)\n\tequal256(t, held+big, test20.Token.BalanceOf(rlm), \"swap holds the amount\")\n\n\ttesting.SetRealm(testing.NewUserRealm(recipient))\n\tClaim(cross(cur), id, \"secret\")\n\tequal256(t, big, test20.Token.BalanceOf(recipient), \"recipient got the amount\")\n\tequal256(t, held, test20.Token.BalanceOf(rlm), \"swap released the amount\")\n}\n\nfunc resetTestState() {\n\tswaps = avl.Tree{}\n\tcounter = 0\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"aVVqo77MpEcSlGYvYJQhKfh+tU5n/UC634y672rZvfBz+VeK7XPLWj7mOkFrQ8pGlH0CXPDloapQob3orIbNVQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"foo20","path":"gno.land/r/demo/defi/foo20","files":[{"name":"amount_test.gno","body":"package foo20\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n)\n\n// equal256 and require256 compare 256-bit amounts natively; uassert and\n// urequire do not support uint256 (see p/nt/ufmt's README).\nfunc equal256(t *testing.T, want, got uint256, msgs ...string) bool {\n\tt.Helper()\n\tif want == got {\n\t\treturn true\n\t}\n\tt.Errorf(\"%swant %s, got %s\", msgPrefix(msgs), strconv.FormatUint256(want, 10), strconv.FormatUint256(got, 10))\n\treturn false\n}\n\nfunc require256(t *testing.T, want, got uint256, msgs ...string) {\n\tt.Helper()\n\tif want != got {\n\t\tt.Fatalf(\"%swant %s, got %s\", msgPrefix(msgs), strconv.FormatUint256(want, 10), strconv.FormatUint256(got, 10))\n\t}\n}\n\nfunc msgPrefix(msgs []string) string {\n\tif len(msgs) == 0 {\n\t\treturn \"\"\n\t}\n\treturn msgs[0] + \": \"\n}\n"},{"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.\n//\n// It is built on gno.land/p/nt/grc20/v1, so every amount is a uint256, and is\n// registered in gno.land/r/nt/grc20reg/v1.\npackage foo20\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/grc20/v1\"\n\t\"gno.land/p/nt/ownable/v0\"\n\t\"gno.land/r/nt/grc20reg/v1\"\n)\n\nvar (\n\tToken         *grc20.Token\n\tprivateLedger *grc20.PrivateLedger\n\tuserTeller    grc20.Teller\n\tOwnable       = ownable.NewWithAddress(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // 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 = privateLedger.CallerTeller()\n\tprivateLedger.Mint(Ownable.Owner(), 1_000_000*10_000) // @privateLedgeristrator (1M)\n\tgrc20reg.Register(cross(cur), Token, \"\")\n}\n\nfunc TotalSupply() uint256 {\n\treturn userTeller.TotalSupply()\n}\n\nfunc BalanceOf(owner address) uint256 {\n\treturn userTeller.BalanceOf(owner)\n}\n\nfunc Allowance(owner, spender address) uint256 {\n\treturn userTeller.Allowance(owner, spender)\n}\n\nfunc Transfer(cur realm, to address, amount uint256) {\n\tcheckErr(userTeller.Transfer(0, cur, to, amount))\n}\n\nfunc Approve(cur realm, spender address, amount uint256) {\n\tcheckErr(userTeller.Approve(0, cur, spender, amount))\n}\n\nfunc TransferFrom(cur realm, from, to address, amount uint256) {\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 := uint256(1_000 * 10_000) // 1k\n\tcheckErr(privateLedger.Mint(caller, amount))\n}\n\nfunc Mint(cur realm, to address, amount uint256) {\n\tOwnable.AssertOwnedBy(cur.Previous().Address())\n\tcheckErr(privateLedger.Mint(to, amount))\n}\n\nfunc Burn(cur realm, from address, amount uint256) {\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 strconv.FormatUint256(balance, 10) + \"\\n\"\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":"foo20_test.gno","body":"package foo20\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\n// cur is a zero-value realm used as a placeholder when forwarding to\n// uassert/urequire dispatch helpers that gained an `rlm realm` param.\n// These tests pass `func()` callbacks (no crossing inside the callback),\n// so rlm is ignored — a nil realm here is safe.\nvar cur realm\n\nfunc TestReadOnlyPublicMethods(cur realm, t *testing.T) {\n\tvar (\n\t\tadmin = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\")\n\t\talice = testutils.TestAddress(\"alice\")\n\t\tbob   = testutils.TestAddress(\"bob\")\n\t)\n\n\ttype test struct {\n\t\tname    string\n\t\tbalance uint256\n\t\tfn      func() uint256\n\t}\n\n\t// check balances #1.\n\t{\n\t\ttests := []test{\n\t\t\t{\"TotalSupply\", 10_000_000_000, func() uint256 { return TotalSupply() }},\n\t\t\t{\"BalanceOf(admin)\", 10_000_000_000, func() uint256 { return BalanceOf(admin) }},\n\t\t\t{\"BalanceOf(alice)\", 0, func() uint256 { return BalanceOf(alice) }},\n\t\t\t{\"Allowance(admin, alice)\", 0, func() uint256 { return Allowance(admin, alice) }},\n\t\t\t{\"BalanceOf(bob)\", 0, func() uint256 { return BalanceOf(bob) }},\n\t\t}\n\t\tfor _, tc := range tests {\n\t\t\tgot := tc.fn()\n\t\t\tequal256(t, tc.balance, got, tc.name)\n\t\t}\n\t}\n\n\t// bob uses the faucet.\n\ttesting.SetOriginCaller(bob)\n\tFaucet(cross(cur))\n\n\t// check balances #2.\n\t{\n\t\ttests := []test{\n\t\t\t{\"TotalSupply\", 10_010_000_000, func() uint256 { return TotalSupply() }},\n\t\t\t{\"BalanceOf(admin)\", 10_000_000_000, func() uint256 { return BalanceOf(admin) }},\n\t\t\t{\"BalanceOf(alice)\", 0, func() uint256 { return BalanceOf(alice) }},\n\t\t\t{\"Allowance(admin, alice)\", 0, func() uint256 { return Allowance(admin, alice) }},\n\t\t\t{\"BalanceOf(bob)\", 10_000_000, func() uint256 { return BalanceOf(bob) }},\n\t\t}\n\t\tfor _, tc := range tests {\n\t\t\tgot := tc.fn()\n\t\t\tequal256(t, tc.balance, got, tc.name)\n\t\t}\n\t}\n}\n\nfunc TestAbove2Pow63(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tbig := uint256(1) \u003c\u003c 128\n\tprivateLedger.Mint(alice, big)\n\tequal256(t, big, BalanceOf(alice))\n\tuassert.Equal(t, \"340282366920938463463374607431768211456\\n\", Render(\"balance/\"+alice.String()))\n}\n\nfunc TestErrConditions(cur realm, t *testing.T) {\n\tvar (\n\t\tadmin = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\")\n\t\tempty = address(\"\")\n\t)\n\n\ttype test struct {\n\t\tname    string\n\t\tmsg     string\n\t\tisCross bool\n\t\tfn      func()\n\t}\n\n\tprivateLedger.Mint(address(admin), 10000)\n\t{\n\t\ttests := []test{\n\t\t\t{\"Transfer(admin, 1)\", \"cannot send transfer to self\", false, func() {\n\t\t\t\t// XXX: should replace with: Transfer(admin, 1)\n\t\t\t\t// but there is currently a limitation in manipulating the frame stack and simulate\n\t\t\t\t// calling this package from an outside point of view.\n\t\t\t\tadminAddr := address(admin)\n\t\t\t\tif err := privateLedger.Transfer(adminAddr, adminAddr, 1); err != nil {\n\t\t\t\t\tpanic(err)\n\t\t\t\t}\n\t\t\t}},\n\t\t\t{\"Approve(empty, 1))\", \"invalid address\", true, func() { Approve(cross(cur), empty, 1) }},\n\t\t}\n\t\tfor _, tc := range tests {\n\t\t\tif tc.isCross {\n\t\t\t\tuassert.AbortsWithMessage(t, cur, tc.msg, tc.fn)\n\t\t\t} else {\n\t\t\t\tuassert.PanicsWithMessage(t, cur, tc.msg, tc.fn)\n\t\t\t}\n\t\t}\n\t}\n}\n\n//func TestNewFoo20(t *testing.T) {\n//\tt.Run(\"invalid input\", func(t *testing.T) {\n//\t\ttestCases := []struct {\n//\t\t\tmsg string\n//\t\t\tfn  func()\n//\t\t}{\n//\t\t\t// Test AbortsWithMessage\n//\tuassert.PanicsWithMessage\", func() { NewFoo20(\"foo\", \"f\", 0) }},\n//\t\t\t{\"symbol cannot be empty\", func() { NewFoo20(\"foo\", \"\", 1) }},\n//\t\t\t{\"name cannot be empty\", func() { NewFoo20(\"\", \"f\", 1) }},\n//\t\t}\n//\t\tfor _, tc := range testCases {\n//\t\t\tuassert.AbortsWithMessage(t, cur, tc.msg, tc.fn)\n//\t\t}\n//\t})\n//\tt.Run(\"transfer\", func(t *testing.T) {\n//\t\t// ... existing code ...\n//\t})\n//}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/demo/defi/foo20\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"3fYLFos4D9sVWofEkqFDWskvBx1fX36JeGaZq8ejKg1rwO+8BVqANqE0asxo8yhqkm+lUGgv5r9+j6najkFr0w=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"disperse","path":"gno.land/r/demo/disperse","files":[{"name":"amount_test.gno","body":"package disperse\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n)\n\n// equal256 and require256 compare 256-bit amounts natively; uassert and\n// urequire do not support uint256 (see p/nt/ufmt's README).\nfunc equal256(t *testing.T, want, got uint256, msgs ...string) bool {\n\tt.Helper()\n\tif want == got {\n\t\treturn true\n\t}\n\tt.Errorf(\"%swant %s, got %s\", msgPrefix(msgs), strconv.FormatUint256(want, 10), strconv.FormatUint256(got, 10))\n\treturn false\n}\n\nfunc require256(t *testing.T, want, got uint256, msgs ...string) {\n\tt.Helper()\n\tif want != got {\n\t\tt.Fatalf(\"%swant %s, got %s\", msgPrefix(msgs), strconv.FormatUint256(want, 10), strconv.FormatUint256(got, 10))\n\t}\n}\n\nfunc msgPrefix(msgs []string) string {\n\tif len(msgs) == 0 {\n\t\treturn \"\"\n\t}\n\treturn msgs[0] + \": \"\n}\n"},{"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\n//\n// The tokens are grc20factory tokens, whose amounts are uint256.\nfunc DisperseGRC20(cur realm, addresses []address, amounts []uint256, 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\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\n// parseTokens parses \"\u003camount\u003e\u003csymbol\u003e,...\" where amount is a decimal uint256.\nfunc parseTokens(tokens string) ([]uint256, []string, error) {\n\tvar amounts []uint256\n\tvar symbols []string\n\n\tfor _, token := range strings.Split(tokens, \",\") {\n\t\tamountStr, symbol := splitString(token)\n\t\tamount, err := strconv.ParseUint256(amountStr, 10)\n\t\tif err != nil {\n\t\t\treturn nil, nil, ErrInvalidAmount\n\t\t}\n\n\t\tamounts = append(amounts, 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"},{"name":"util_test.gno","body":"package disperse\n\nimport (\n\t\"math\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nfunc TestParseTokens(t *testing.T) {\n\tamounts, symbols, err := parseTokens(\"150TEST,115792089237316195423570985008687907853269984665640564039457584007913129639935BIG,0Z\")\n\tuassert.NoError(t, err)\n\tuassert.Equal(t, 3, len(amounts))\n\tequal256(t, uint256(150), amounts[0])\n\tequal256(t, uint256(math.MaxUint256), amounts[1])\n\tequal256(t, uint256(0), amounts[2])\n\tuassert.Equal(t, \"TEST\", symbols[0])\n\tuassert.Equal(t, \"BIG\", symbols[1])\n\tuassert.Equal(t, \"Z\", symbols[2])\n}\n\nfunc TestParseTokensRejectsMalformed(t *testing.T) {\n\tfor _, in := range []string{\n\t\t// One past MaxUint256: refused, not wrapped.\n\t\t\"115792089237316195423570985008687907853269984665640564039457584007913129639936BIG\",\n\t\t\"-1BIG\",  // no sign\n\t\t\"+1BIG\",  // no sign\n\t\t\"BIG\",    // no amount\n\t\t\"\",       // nothing at all\n\t\t\"10,5X\",  // an entry without a symbol has no amount either\n\t\t\"5X,,6Y\", // an empty entry\n\t} {\n\t\t_, _, err := parseTokens(in)\n\t\tuassert.ErrorIs(t, err, ErrInvalidAmount, \"input \"+in)\n\t}\n}\n"},{"name":"z_0_filetest.gno","body":"// PKGPATH: gno.land/r/demo/main\n\npackage main\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/disperse\"\n)\n\nfunc main(cur realm) {\n\tmainAddr := chain.PackageAddress(\"gno.land/r/demo/main\")\n\tdisperseAddr := chain.PackageAddress(\"gno.land/r/demo/disperse\")\n\tbeneficiary1 := address(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0\")\n\tbeneficiary2 := address(\"g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\")\n\n\ttesting.IssueCoins(mainAddr, chain.Coins{{Denom: \"ugnot\", Amount: 250}})\n\ttesting.SetOriginSend(chain.Coins{{Denom: \"ugnot\", Amount: 250}})\n\ttesting.SetRealm(testing.NewUserRealm(mainAddr))\n\n\tbanker_ := banker.NewBanker(banker.BankerTypeRealmSend, cur)\n\tprintln(\"main balance before send:\", banker_.GetCoins(mainAddr))\n\tprintln(\"disperse balance before send:\", banker_.GetCoins(disperseAddr))\n\n\tbanker_.SendCoins(mainAddr, disperseAddr, chain.Coins{{\"ugnot\", 250}})\n\tprintln(\"main balance after send:\", banker_.GetCoins(mainAddr))\n\tprintln(\"disperse balance after send:\", banker_.GetCoins(disperseAddr))\n\n\taddressesStr := beneficiary1.String() + \",\" + beneficiary2.String()\n\tdisperse.DisperseUgnotString(cross(cur), addressesStr, \"150,50\")\n\n\tprintln(\"main balance after disperse:\", banker_.GetCoins(mainAddr))\n\tprintln(\"disperse balance after disperse:\", banker_.GetCoins(disperseAddr))\n\tprintln(\"beneficiary1 balance:\", banker_.GetCoins(beneficiary1))\n\tprintln(\"beneficiary2 balance:\", banker_.GetCoins(beneficiary2))\n}\n\n// Output:\n// main balance before send: 250ugnot\n// disperse balance before send:\n// main balance after send:\n// disperse balance after send: 250ugnot\n// main balance after disperse: 50ugnot\n// disperse balance after disperse:\n// beneficiary1 balance: 150ugnot\n// beneficiary2 balance: 50ugnot\n"},{"name":"z_1_filetest.gno","body":"// PKGPATH: gno.land/r/demo/main\n\npackage main\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/disperse\"\n)\n\nfunc main(cur realm) {\n\tmainAddr := chain.PackageAddress(\"gno.land/r/demo/main\")\n\tdisperseAddr := chain.PackageAddress(\"gno.land/r/demo/disperse\")\n\tbeneficiary1 := address(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0\")\n\tbeneficiary2 := address(\"g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\")\n\n\t// Envelope = 300 ugnot, but only 200 forwarded to disperse (the\n\t// remaining 100 stays in mainAddr). Tests that the realm-balance\n\t// check, not OriginSend, is what gates dispersal.\n\ttesting.IssueCoins(mainAddr, chain.Coins{{Denom: \"ugnot\", Amount: 300}})\n\ttesting.SetOriginSend(chain.Coins{{Denom: \"ugnot\", Amount: 300}})\n\ttesting.SetRealm(testing.NewUserRealm(mainAddr))\n\n\tbanker_ := banker.NewBanker(banker.BankerTypeRealmSend, cur)\n\tprintln(\"main balance before send:\", banker_.GetCoins(mainAddr))\n\tprintln(\"disperse balance before send:\", banker_.GetCoins(disperseAddr))\n\n\tbanker_.SendCoins(mainAddr, disperseAddr, chain.Coins{{\"ugnot\", 200}})\n\tprintln(\"main balance after send:\", banker_.GetCoins(mainAddr))\n\tprintln(\"disperse balance after send:\", banker_.GetCoins(disperseAddr))\n\n\taddressesStr := beneficiary1.String() + \",\" + beneficiary2.String()\n\tdisperse.DisperseUgnotString(cross(cur), addressesStr, \"150,50\")\n\n\tprintln(\"main balance after disperse:\", banker_.GetCoins(mainAddr))\n\tprintln(\"disperse balance after disperse:\", banker_.GetCoins(disperseAddr))\n\tprintln(\"beneficiary1 balance:\", banker_.GetCoins(beneficiary1))\n\tprintln(\"beneficiary2 balance:\", banker_.GetCoins(beneficiary2))\n}\n\n// Output:\n// main balance before send: 300ugnot\n// disperse balance before send:\n// main balance after send: 100ugnot\n// disperse balance after send: 200ugnot\n// main balance after disperse: 100ugnot\n// disperse balance after disperse:\n// beneficiary1 balance: 150ugnot\n// beneficiary2 balance: 50ugnot\n"},{"name":"z_2_filetest.gno","body":"// PKGPATH: gno.land/r/demo/main\n\npackage main\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/disperse\"\n)\n\nfunc main(cur realm) {\n\tmainAddr := chain.PackageAddress(\"gno.land/r/demo/main\")\n\tdisperseAddr := chain.PackageAddress(\"gno.land/r/demo/disperse\")\n\tbeneficiary1 := address(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0\")\n\tbeneficiary2 := address(\"g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\")\n\n\t// Mismatch: requested dispersal sum (200) exceeds the realm balance (100).\n\ttesting.IssueCoins(mainAddr, chain.Coins{{Denom: \"ugnot\", Amount: 100}})\n\ttesting.SetOriginSend(chain.Coins{{Denom: \"ugnot\", Amount: 100}})\n\ttesting.SetRealm(testing.NewUserRealm(mainAddr))\n\n\tbanker_ := banker.NewBanker(banker.BankerTypeRealmSend, cur)\n\tprintln(\"main balance before send:\", banker_.GetCoins(mainAddr))\n\tprintln(\"disperse balance before send:\", banker_.GetCoins(disperseAddr))\n\n\tbanker_.SendCoins(mainAddr, disperseAddr, chain.Coins{{\"ugnot\", 100}})\n\tprintln(\"main balance after send:\", banker_.GetCoins(mainAddr))\n\tprintln(\"disperse balance after send:\", banker_.GetCoins(disperseAddr))\n\n\taddressesStr := beneficiary1.String() + \",\" + beneficiary2.String()\n\tdisperse.DisperseUgnotString(cross(cur), addressesStr, \"150,50\")\n}\n\n// Error:\n// disperse: mismatch between coins sent and params called\n"},{"name":"z_3_filetest.gno","body":"// PKGPATH: gno.land/r/demo/main\n\npackage main\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\ttokens \"gno.land/r/demo/defi/grc20factory\"\n\t\"gno.land/r/demo/disperse\"\n)\n\nfunc main(cur realm) {\n\tdisperseAddr := chain.PackageAddress(\"gno.land/r/demo/disperse\")\n\tmainAddr := chain.PackageAddress(\"gno.land/r/demo/main\")\n\tbeneficiary1 := address(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0\")\n\tbeneficiary2 := address(\"g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\")\n\n\ttesting.SetOriginCaller(mainAddr)\n\n\ttokens.New(cross(cur), \"test\", \"TEST\", 4, 0, 0)\n\ttokens.Mint(cross(cur), \"TEST\", mainAddr, 200)\n\tprintln(\"main balance before:\", tokens.BalanceOf(\"TEST\", mainAddr))\n\n\ttokens.Approve(cross(cur), \"TEST\", disperseAddr, 200)\n\tprintln(\"disperse allowance before:\", tokens.Allowance(\"TEST\", mainAddr, disperseAddr))\n\n\taddressesStr := beneficiary1.String() + \",\" + beneficiary2.String()\n\tdisperse.DisperseGRC20String(cross(cur), addressesStr, \"150TEST,50TEST\")\n\n\tprintln(\"main balance after:\", tokens.BalanceOf(\"TEST\", mainAddr))\n\tprintln(\"disperse allowance after:\", tokens.Allowance(\"TEST\", mainAddr, disperseAddr))\n\tprintln(\"beneficiary1 balance:\", tokens.BalanceOf(\"TEST\", beneficiary1))\n\tprintln(\"beneficiary2 balance:\", tokens.BalanceOf(\"TEST\", beneficiary2))\n}\n\n// Output:\n// main balance before: 200\n// disperse allowance before: 200\n// main balance after: 0\n// disperse allowance after: 0\n// beneficiary1 balance: 150\n// beneficiary2 balance: 50\n"},{"name":"z_4_filetest.gno","body":"// PKGPATH: gno.land/r/demo/main\n\npackage main\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\ttokens \"gno.land/r/demo/defi/grc20factory\"\n\t\"gno.land/r/demo/disperse\"\n)\n\nfunc main(cur realm) {\n\tdisperseAddr := chain.PackageAddress(\"gno.land/r/demo/disperse\")\n\tmainAddr := chain.PackageAddress(\"gno.land/r/demo/main\")\n\tbeneficiary1 := address(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0\")\n\tbeneficiary2 := address(\"g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\")\n\n\ttesting.SetOriginCaller(mainAddr)\n\n\ttokens.New(cross(cur), \"test1\", \"TEST1\", 4, 0, 0)\n\ttokens.Mint(cross(cur), \"TEST1\", mainAddr, 200)\n\tprintln(\"main balance before (TEST1):\", tokens.BalanceOf(\"TEST1\", mainAddr))\n\n\ttokens.New(cross(cur), \"test2\", \"TEST2\", 4, 0, 0)\n\ttokens.Mint(cross(cur), \"TEST2\", mainAddr, 200)\n\tprintln(\"main balance before (TEST2):\", tokens.BalanceOf(\"TEST2\", mainAddr))\n\n\ttokens.Approve(cross(cur), \"TEST1\", disperseAddr, 200)\n\tprintln(\"disperse allowance before (TEST1):\", tokens.Allowance(\"TEST1\", mainAddr, disperseAddr))\n\n\ttokens.Approve(cross(cur), \"TEST2\", disperseAddr, 200)\n\tprintln(\"disperse allowance before (TEST2):\", tokens.Allowance(\"TEST2\", mainAddr, disperseAddr))\n\n\taddressesStr := beneficiary1.String() + \",\" + beneficiary2.String()\n\tdisperse.DisperseGRC20String(cross(cur), addressesStr, \"200TEST1,200TEST2\")\n\n\tprintln(\"main balance after (TEST1):\", tokens.BalanceOf(\"TEST1\", mainAddr))\n\tprintln(\"main balance after (TEST2):\", tokens.BalanceOf(\"TEST2\", mainAddr))\n\tprintln(\"disperse allowance after (TEST1):\", tokens.Allowance(\"TEST1\", mainAddr, disperseAddr))\n\tprintln(\"disperse allowance after (TEST2):\", tokens.Allowance(\"TEST2\", mainAddr, disperseAddr))\n\tprintln(\"beneficiary1 balance (TEST1):\", tokens.BalanceOf(\"TEST1\", beneficiary1))\n\tprintln(\"beneficiary1 balance (TEST2):\", tokens.BalanceOf(\"TEST2\", beneficiary1))\n\tprintln(\"beneficiary2 balance (TEST1):\", tokens.BalanceOf(\"TEST1\", beneficiary2))\n\tprintln(\"beneficiary2 balance (TEST2):\", tokens.BalanceOf(\"TEST2\", beneficiary2))\n}\n\n// Output:\n// main balance before (TEST1): 200\n// main balance before (TEST2): 200\n// disperse allowance before (TEST1): 200\n// disperse allowance before (TEST2): 200\n// main balance after (TEST1): 0\n// main balance after (TEST2): 0\n// disperse allowance after (TEST1): 0\n// disperse allowance after (TEST2): 0\n// beneficiary1 balance (TEST1): 200\n// beneficiary1 balance (TEST2): 0\n// beneficiary2 balance (TEST1): 0\n// beneficiary2 balance (TEST2): 200\n"},{"name":"z_5_filetest.gno","body":"// PKGPATH: gno.land/r/demo/main\n\n// GRC20 amounts are uint256: dispersing more than an int64 can hold works,\n// and an amount that is not a decimal number is refused.\npackage main\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\ttokens \"gno.land/r/demo/defi/grc20factory\"\n\t\"gno.land/r/demo/disperse\"\n)\n\nfunc main(cur realm) {\n\tdisperseAddr := chain.PackageAddress(\"gno.land/r/demo/disperse\")\n\tmainAddr := chain.PackageAddress(\"gno.land/r/demo/main\")\n\tbeneficiary1 := address(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0\")\n\tbeneficiary2 := address(\"g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\")\n\n\ttesting.SetOriginCaller(mainAddr)\n\n\tbig := uint256(1) \u003c\u003c 100\n\ttokens.New(cross(cur), \"big\", \"BIG\", 18, 0, 0)\n\ttokens.Mint(cross(cur), \"BIG\", mainAddr, 2*big)\n\ttokens.Approve(cross(cur), \"BIG\", disperseAddr, 2*big)\n\n\taddressesStr := beneficiary1.String() + \",\" + beneficiary2.String()\n\t// 2^100 = 1267650600228229401496703205376\n\tdisperse.DisperseGRC20String(cross(cur), addressesStr,\n\t\t\"1267650600228229401496703205376BIG,1267650600228229401496703205376BIG\")\n\n\tprintln(\"main balance after:\", tokens.BalanceOf(\"BIG\", mainAddr))\n\tprintln(\"beneficiary1 balance:\", tokens.BalanceOf(\"BIG\", beneficiary1))\n\tprintln(\"beneficiary2 balance:\", tokens.BalanceOf(\"BIG\", beneficiary2))\n\n\tdisperse.DisperseGRC20String(cross(cur), beneficiary1.String(), \"-1BIG\")\n}\n\n// Output:\n// main balance after: 0\n// beneficiary1 balance: 1267650600228229401496703205376\n// beneficiary2 balance: 1267650600228229401496703205376\n\n// Error:\n// disperse: invalid amount\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"4CRdgI3CEtAefLR8/GfOp9/okW640rQhRaNps48nmKln8+FXVQCoPQAmP1U2YYBHLvA3f2Kykt0rQa8UYiEoIw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"rthMCNLt+KY9e5yApTpH2DnWVgs3LJIM9FpxNZ3bfo0KKrTyAT4mN5CqBrM4XCZISuoRKYLgznOBFL9NFSOc4w=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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":"profile_test.gno","body":"package profile\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\n// Global addresses for test users\nvar (\n\talice   = testutils.TestAddress(\"alice\")\n\tbob     = testutils.TestAddress(\"bob\")\n\tcharlie = testutils.TestAddress(\"charlie\")\n\tdave    = testutils.TestAddress(\"dave\")\n\teve     = testutils.TestAddress(\"eve\")\n\tfrank   = testutils.TestAddress(\"frank\")\n\tuser1   = testutils.TestAddress(\"user1\")\n\tuser2   = testutils.TestAddress(\"user2\")\n)\n\nfunc TestStringFields(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\n\t// Get before setting\n\tname := GetStringField(alice, DisplayName, \"anon\")\n\tuassert.Equal(t, \"anon\", name)\n\n\t// Set new key\n\tupdated := SetStringField(cross(cur), DisplayName, \"Alice foo\")\n\tuassert.Equal(t, updated, false)\n\tupdated = SetStringField(cross(cur), Homepage, \"https://example.com\")\n\tuassert.Equal(t, updated, false)\n\n\t// Update the key\n\tupdated = SetStringField(cross(cur), DisplayName, \"Alice foo\")\n\tuassert.Equal(t, updated, true)\n\n\t// Get after setting\n\tname = GetStringField(alice, DisplayName, \"anon\")\n\thomepage := GetStringField(alice, Homepage, \"\")\n\tbio := GetStringField(alice, Bio, \"42\")\n\n\tuassert.Equal(t, \"Alice foo\", name)\n\tuassert.Equal(t, \"https://example.com\", homepage)\n\tuassert.Equal(t, \"42\", bio)\n}\n\nfunc TestIntFields(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\n\t// Get before setting\n\tage := GetIntField(bob, Age, 25)\n\tuassert.Equal(t, 25, age)\n\n\t// Set new key\n\tupdated := SetIntField(cross(cur), Age, 30)\n\tuassert.Equal(t, updated, false)\n\n\t// Update the key\n\tupdated = SetIntField(cross(cur), Age, 30)\n\tuassert.Equal(t, updated, true)\n\n\t// Get after setting\n\tage = GetIntField(bob, Age, 25)\n\tuassert.Equal(t, 30, age)\n}\n\nfunc TestBoolFields(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(charlie))\n\n\t// Get before setting\n\thiring := GetBoolField(charlie, AvailableForHiring, false)\n\tuassert.Equal(t, false, hiring)\n\n\t// Set\n\tupdated := SetBoolField(cross(cur), AvailableForHiring, true)\n\tuassert.Equal(t, updated, false)\n\n\t// Update the key\n\tupdated = SetBoolField(cross(cur), AvailableForHiring, true)\n\tuassert.Equal(t, updated, true)\n\n\t// Get after setting\n\thiring = GetBoolField(charlie, AvailableForHiring, false)\n\tuassert.Equal(t, true, hiring)\n}\n\nfunc TestMultipleProfiles(cur realm, t *testing.T) {\n\t// Set profile for user1\n\ttesting.SetRealm(testing.NewUserRealm(user1))\n\tupdated := SetStringField(cross(cur), DisplayName, \"User One\")\n\tuassert.Equal(t, updated, false)\n\n\t// Set profile for user2\n\ttesting.SetRealm(testing.NewUserRealm(user2))\n\tupdated = SetStringField(cross(cur), DisplayName, \"User Two\")\n\tuassert.Equal(t, updated, false)\n\n\t// Get profiles\n\ttesting.SetRealm(testing.NewUserRealm(user1)) // Switch back to user1\n\tname1 := GetStringField(user1, DisplayName, \"anon\")\n\ttesting.SetRealm(testing.NewUserRealm(user2)) // Switch back to user2\n\tname2 := GetStringField(user2, DisplayName, \"anon\")\n\n\tuassert.Equal(t, \"User One\", name1)\n\tuassert.Equal(t, \"User Two\", name2)\n}\n\nfunc TestArbitraryStringField(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(user1))\n\n\t// Set arbitrary string field\n\tupdated := SetStringField(cross(cur), \"MyEmail\", \"my@email.com\")\n\tuassert.Equal(t, updated, false)\n\n\tval := GetStringField(user1, \"MyEmail\", \"\")\n\tuassert.Equal(t, val, \"my@email.com\")\n}\n\nfunc TestArbitraryIntField(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(user1))\n\n\t// Set arbitrary int field\n\tupdated := SetIntField(cross(cur), \"MyIncome\", 100_000)\n\tuassert.Equal(t, updated, false)\n\n\tval := GetIntField(user1, \"MyIncome\", 0)\n\tuassert.Equal(t, val, 100_000)\n}\n\nfunc TestArbitraryBoolField(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(user1))\n\n\t// Set arbitrary bool field\n\tupdated := SetBoolField(cross(cur), \"IsWinner\", true)\n\tuassert.Equal(t, updated, false)\n\n\tval := GetBoolField(user1, \"IsWinner\", false)\n\tuassert.Equal(t, val, true)\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"7P43QsFWOyeNcS+E98Uw+/Rgq+dfUrriVI0FpRyMTbp3EKp2eEVSsxiq5UQBHqVa4I3D32Bstpaxsw9MWb7CJw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7","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":"events_test.gno","body":"package events\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\n// cur is a zero-value realm used as a placeholder when forwarding to\n// uassert/urequire dispatch helpers that gained an `rlm realm` param.\n// These tests pass `func()` callbacks (no crossing inside the callback),\n// so rlm is ignored — a nil realm here is safe.\nvar cur realm\nvar (\n\tsuRealm          = testing.NewUserRealm(address(\"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\"))\n\tnow              = \"2009-02-13T23:31:30Z\" // time.Now() is hardcoded to this value in the gno test machine currently\n\tparsedTimeNow, _ = time.Parse(time.RFC3339, now)\n)\n\nfunc TestAddEvent(cur realm, t *testing.T) {\n\ttesting.SetRealm(suRealm)\n\n\te1Start := parsedTimeNow.Add(time.Hour * 24 * 5)\n\te1End := e1Start.Add(time.Hour * 4)\n\n\t_, err := AddEvent(cross(cur), \"Event 1\", \"this event is upcoming\", \"gno.land\", \"gnome land\", e1Start.Format(time.RFC3339), e1End.Format(time.RFC3339))\n\n\turequire.NoError(t, err)\n\tgot := renderHome(false)\n\n\tif !strings.Contains(got, \"Event 1\") {\n\t\tt.Fatalf(\"Expected to find Event 1 in render\")\n\t}\n\n\te2Start := parsedTimeNow.Add(-time.Hour * 24 * 5)\n\te2End := e2Start.Add(time.Hour * 4)\n\n\t_, err = AddEvent(cross(cur), \"Event 2\", \"this event is in the past\", \"gno.land\", \"gnome land\", e2Start.Format(time.RFC3339), e2End.Format(time.RFC3339))\n\turequire.NoError(t, err)\n\n\tgot = renderHome(false)\n\n\tupcomingPos := strings.Index(got, \"## Upcoming events\")\n\tpastPos := strings.Index(got, \"## Past events\")\n\n\te1Pos := strings.Index(got, \"Event 1\")\n\te2Pos := strings.Index(got, \"Event 2\")\n\n\t// expected index ordering: upcoming \u003c e1 \u003c past \u003c e2\n\tif e1Pos \u003c upcomingPos || e1Pos \u003e pastPos {\n\t\tt.Fatalf(\"Expected to find Event 1 in Upcoming events\")\n\t}\n\n\tif e2Pos \u003c upcomingPos || e2Pos \u003c pastPos || e2Pos \u003c e1Pos {\n\t\tt.Fatalf(\"Expected to find Event 2 on auth different pos\")\n\t}\n\n\t// larger index =\u003e smaller startTime (future =\u003e past)\n\tif events[0].startTime.Unix() \u003c events[1].startTime.Unix() {\n\t\tt.Fatalf(\"expected ordering to be different\")\n\t}\n}\n\nfunc TestAddEventErrors(cur realm, t *testing.T) {\n\ttesting.SetRealm(suRealm)\n\n\t_, err := AddEvent(cross(cur), \"\", \"sample desc\", \"gno.land\", \"gnome land\", \"2009-02-13T23:31:31Z\", \"2009-02-13T23:33:31Z\")\n\tuassert.ErrorIs(t, err, ErrEmptyName)\n\n\t_, err = AddEvent(cross(cur), \"sample name\", \"sample desc\", \"gno.land\", \"gnome land\", \"\", \"2009-02-13T23:33:31Z\")\n\tuassert.ErrorContains(t, err, ErrInvalidStartTime.Error())\n\n\t_, err = AddEvent(cross(cur), \"sample name\", \"sample desc\", \"gno.land\", \"gnome land\", \"2009-02-13T23:31:31Z\", \"\")\n\tuassert.ErrorContains(t, err, ErrInvalidEndTime.Error())\n\n\t_, err = AddEvent(cross(cur), \"sample name\", \"sample desc\", \"gno.land\", \"gnome land\", \"2009-02-13T23:31:31Z\", \"2009-02-13T23:30:31Z\")\n\tuassert.ErrorIs(t, err, ErrEndBeforeStart)\n\n\t_, err = AddEvent(cross(cur), \"sample name\", \"sample desc\", \"gno.land\", \"gnome land\", \"2009-02-13T23:31:31+06:00\", \"2009-02-13T23:33:31+02:00\")\n\tuassert.ErrorIs(t, err, ErrStartEndTimezonemMismatch)\n\n\ttooLongDesc := `Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean ma`\n\t_, err = AddEvent(cross(cur), \"sample name\", tooLongDesc, \"gno.land\", \"gnome land\", \"2009-02-13T23:31:31Z\", \"2009-02-13T23:33:31Z\")\n\tuassert.ErrorContains(t, err, ErrDescriptionTooLong.Error())\n}\n\nfunc TestDeleteEvent(cur realm, t *testing.T) {\n\ttesting.SetRealm(suRealm)\n\n\te1Start := parsedTimeNow.Add(time.Hour * 24 * 5)\n\te1End := e1Start.Add(time.Hour * 4)\n\n\tid, err := AddEvent(cross(cur), \"ToDelete\", \"description\", \"gno.land\", \"gnome land\", e1Start.Format(time.RFC3339), e1End.Format(time.RFC3339))\n\turequire.NoError(t, err)\n\n\tgot := renderHome(false)\n\n\tif !strings.Contains(got, \"ToDelete\") {\n\t\tt.Fatalf(\"Expected to find ToDelete event in render\")\n\t}\n\n\tDeleteEvent(cross(cur), id)\n\tgot = renderHome(false)\n\n\tif strings.Contains(got, \"ToDelete\") {\n\t\tt.Fatalf(\"Did not expect to find ToDelete event in render\")\n\t}\n}\n\nfunc TestEditEvent(cur realm, t *testing.T) {\n\ttesting.SetRealm(suRealm)\n\n\te1Start := parsedTimeNow.Add(time.Hour * 24 * 5)\n\te1End := e1Start.Add(time.Hour * 4)\n\tloc := \"gnome land\"\n\n\tid, err := AddEvent(cross(cur), \"ToDelete\", \"description\", \"gno.land\", loc, e1Start.Format(time.RFC3339), e1End.Format(time.RFC3339))\n\turequire.NoError(t, err)\n\n\tnewName := \"New Name\"\n\tnewDesc := \"Normal description\"\n\tnewLink := \"new Link\"\n\tnewST := e1Start.Add(time.Hour)\n\tnewET := newST.Add(time.Hour)\n\n\tEditEvent(cross(cur), id, newName, newDesc, newLink, \"\", newST.Format(time.RFC3339), newET.Format(time.RFC3339))\n\tedited, _, _ := GetEventByID(id)\n\n\t// Check updated values\n\tuassert.Equal(t, edited.name, newName)\n\tuassert.Equal(t, edited.description, newDesc)\n\tuassert.Equal(t, edited.link, newLink)\n\tuassert.True(t, edited.startTime.Equal(newST))\n\tuassert.True(t, edited.endTime.Equal(newET))\n\n\t// Check if the old values are the same\n\tuassert.Equal(t, edited.location, loc)\n}\n\nfunc TestInvalidEdit(cur realm, t *testing.T) {\n\ttesting.SetRealm(suRealm)\n\n\tuassert.AbortsWithMessage(t, cur, ErrNoSuchID.Error(), func() {\n\t\tEditEvent(cross(cur), \"123123\", \"\", \"\", \"\", \"\", \"\", \"\")\n\t})\n}\n\nfunc TestParseTimes(t *testing.T) {\n\t// times not provided\n\t// end time before start time\n\t// timezone Missmatch\n\n\t_, _, err := parseTimes(\"\", \"\")\n\tuassert.ErrorContains(t, err, ErrInvalidStartTime.Error())\n\n\t_, _, err = parseTimes(now, \"\")\n\tuassert.ErrorContains(t, err, ErrInvalidEndTime.Error())\n\n\t_, _, err = parseTimes(\"2009-02-13T23:30:30Z\", \"2009-02-13T21:30:30Z\")\n\tuassert.ErrorContains(t, err, ErrEndBeforeStart.Error())\n\n\t_, _, err = parseTimes(\"2009-02-10T23:30:30+02:00\", \"2009-02-13T21:30:33+05:00\")\n\tuassert.ErrorContains(t, err, ErrStartEndTimezonemMismatch.Error())\n}\n\nfunc TestRenderEventWidget(cur realm, t *testing.T) {\n\ttesting.SetRealm(suRealm)\n\n\t// No events yet\n\tevents = nil\n\tout, err := RenderEventWidget(1)\n\tuassert.NoError(t, err)\n\tuassert.Equal(t, out, \"No events.\")\n\n\t// Ordering \u0026 if requested amt is larger than the num of events that exist\n\te1Start := parsedTimeNow.Add(time.Hour * 24 * 5)\n\te1End := e1Start.Add(time.Hour * 4)\n\n\te2Start := parsedTimeNow.Add(time.Hour * 24 * 10) // event 2 is after event 1\n\te2End := e2Start.Add(time.Hour * 4)\n\n\t_, err = AddEvent(cross(cur), \"Event 1\", \"description\", \"gno.land\", \"loc\", e1Start.Format(time.RFC3339), e1End.Format(time.RFC3339))\n\turequire.NoError(t, err)\n\n\t_, err = AddEvent(cross(cur), \"Event 2\", \"description\", \"gno.land\", \"loc\", e2Start.Format(time.RFC3339), e2End.Format(time.RFC3339))\n\turequire.NoError(t, err)\n\n\t// Too many events (must be checked after adding events, otherwise\n\t// RenderEventWidget returns early with \"No events.\")\n\tout, err = RenderEventWidget(MaxWidgetSize + 1)\n\tuassert.ErrorIs(t, err, ErrMaxWidgetSize)\n\n\t// Too few events\n\tout, err = RenderEventWidget(0)\n\tuassert.ErrorIs(t, err, ErrMinWidgetSize)\n\n\tout, err = RenderEventWidget(MaxWidgetSize)\n\turequire.NoError(t, err)\n\n\tuniqueSequence := \"- [\" // sequence that is displayed once per each event as per the RenderEventWidget function\n\tuassert.Equal(t, 2, strings.Count(out, uniqueSequence))\n\n\tuassert.True(t, strings.Index(out, \"Event 1\") \u003e strings.Index(out, \"Event 2\"))\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/v0\"\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"d+QdF9RIYMgM5ybDL2KRRc23e9tEcHyomqqUpkyP6J4j+8FuFBmXbpRcZQUF8Ec5VGb7/1vB5jeWc1tblihGUQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"security_patterns","path":"gno.land/r/docs/security_patterns","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/security_patterns\"\ngno = \"0.9\"\n"},{"name":"security_patterns.gno","body":"package security_patterns\n\nimport \"gno.land/p/nt/markdown/sanitize/v0\"\n\nvar (\n\tadmin   = address(\"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\")\n\tmessage = \"Only the admin can edit this message.\"\n)\n\nfunc SetMessage(cur realm, next string) {\n\tassertAdmin(cur)\n\tmessage = next\n}\n\nfunc TransferAdmin(cur realm, next address) {\n\tif !next.IsValid() {\n\t\tpanic(\"invalid admin\")\n\t}\n\tassertAdmin(cur)\n\tadmin = next\n}\n\nfunc Admin() address {\n\treturn admin\n}\n\nfunc Message() string {\n\treturn message\n}\n\nfunc Render(path string) string {\n\tout := \"# Security Patterns\\n\\n\"\n\tout += \"This realm demonstrates three defensive patterns; read the source \" +\n\t\t\"alongside this page:\\n\\n\"\n\tout += \"1. **Live-realm guard** — `assertAdmin` panics unless \" +\n\t\t\"`cur.IsCurrent()` holds, which checks the realm token against the \" +\n\t\t\"live call frame before any authority is read from it.\\n\"\n\tout += \"2. **Caller identity via `cur.Previous().Address()`** — the admin \" +\n\t\t\"check reads the immediate caller, not `OriginCaller()`, so an \" +\n\t\t\"intermediary realm cannot pass itself off as the user.\\n\"\n\tout += \"3. **Sanitized render output** — every value echoed below is run \" +\n\t\t\"through `p/nt/markdown/sanitize` first, so caller-controlled text \" +\n\t\t\"cannot inject markdown or break out of a code span.\\n\\n\"\n\t// InlineCode wraps in a backtick run wide enough to outscan any backticks\n\t// in the content, so a backtick in path cannot close the span early — a\n\t// naive \"`\" + path + \"`\" would.\n\tout += \"Admin: \" + sanitize.InlineCode(admin.String()) + \"\\n\\n\"\n\tout += \"Message: \" + sanitize.InlineText(message) + \"\\n\"\n\tif path != \"\" {\n\t\tout += \"\\nPath: \" + sanitize.InlineCode(path) + \"\\n\"\n\t}\n\treturn out\n}\n\n// assertAdmin guards a state-mutating call. Callers pass their own cur rather\n// than cross(cur) — a non-crossing call of a crossing function, so\n// PreviousRealm does not shift and still names whoever called SetMessage or\n// TransferAdmin. The cur realm first parameter is what makes it a crossing\n// function: the compiler refuses any other first argument, so the token\n// reaching IsCurrent below is live by construction.\nfunc assertAdmin(cur realm) {\n\tif !cur.IsCurrent() {\n\t\tpanic(\"invalid realm\")\n\t}\n\tif cur.Previous().Address() != admin {\n\t\tpanic(\"admin only\")\n\t}\n}\n"},{"name":"security_patterns_test.gno","body":"package security_patterns\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/markdown/sanitize/v0\"\n)\n\nfunc TestSetMessageRequiresLiveAdmin(cur realm, t *testing.T) {\n\t// cross(cur) at the call site performs the realm crossing: PreviousRealm\n\t// inside SetMessage becomes the UserRealm set just above, so assertAdmin\n\t// can verify the caller's address.\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\tSetMessage(cross(cur), \"hello\")\n\tif Message() != \"hello\" {\n\t\tt.Fatalf(\"message was not updated\")\n\t}\n\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1juz2yxmdsa6audkp6ep9vfv80c8p5u76e03vvh\")))\n\tif rec := revive(func() { SetMessage(cross(cur), \"attacker\") }); rec == nil {\n\t\tt.Fatalf(\"expected non-admin caller to panic\")\n\t}\n\tif Message() != \"hello\" {\n\t\tt.Fatalf(\"non-admin caller changed message\")\n\t}\n}\n\nfunc TestRenderEscapesMessage(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\tSetMessage(cross(cur), \"[x](javascript:alert(1)) \u003e quote\")\n\n\tout := Render(\"\")\n\tif strings.Contains(out, \"[x](javascript\") {\n\t\tt.Fatalf(\"message rendered as a live markdown link: %s\", out)\n\t}\n\tif want := sanitize.InlineText(\"[x](javascript:alert(1)) \u003e quote\"); !strings.Contains(out, want) {\n\t\tt.Fatalf(\"message not escaped via sanitize.InlineText: %q not in %s\", want, out)\n\t}\n}\n\nfunc TestRenderPathUsesSafeCodeSpan(t *testing.T) {\n\t// A backtick in path must not close the code span early; InlineCode picks\n\t// a wider backtick fence, whereas a naive \"`\" + path + \"`\" would let the\n\t// user's backtick break out.\n\tpath := \"a`b`c\"\n\tout := Render(path)\n\tif want := sanitize.InlineCode(path); !strings.Contains(out, want) {\n\t\tt.Fatalf(\"path not rendered via a safe code span: %q not in %s\", want, out)\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"eljeq6yVOz9bgFtgJAddHGnMf8bgNEEIH/P8HTuyDKoq0bfPeGHUdW1Adn868OuClwO3tnaXTqZlrLwkZaiOXA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7","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(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // 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":"admin_test.gno","body":"package blog\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/blog/v0\"\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\n// cur is a zero-value realm used as a placeholder when forwarding to\n// uassert/urequire dispatch helpers that gained an `rlm realm` param.\n// These tests pass `func()` callbacks (no crossing inside the callback),\n// so rlm is ignored — a nil realm here is safe.\nvar cur realm\n\n// clearState wipes the global state between test calls\nfunc clearState(t *testing.T) {\n\tt.Helper()\n\n\tb = \u0026blog.Blog{\n\t\tTitle:  \"Gno.land's blog\",\n\t\tPrefix: \"/r/gnoland/blog:\",\n\t}\n\tadminAddr = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\")\n\tinPause = false\n\tmoderatorList = bptree.NewBPTree32()\n\tcommenterList = bptree.NewBPTree32()\n}\n\nfunc TestBlog_AdminControls(cur realm, t *testing.T) {\n\tt.Run(\"non-admin call\", func(t *testing.T) {\n\t\tclearState(t)\n\n\t\tnonAdmin := testutils.TestAddress(\"bob\")\n\n\t\ttesting.SetOriginCaller(nonAdmin)\n\t\ttesting.SetRealm(testing.NewUserRealm(nonAdmin))\n\n\t\tuassert.AbortsWithMessage(t, cur, errNotAdmin.Error(), func() {\n\t\t\tAdminSetInPause(cross(cur), true)\n\t\t})\n\t})\n\n\tt.Run(\"pause toggled\", func(t *testing.T) {\n\t\tclearState(t)\n\n\t\ttesting.SetOriginCaller(adminAddr)\n\t\ttesting.SetRealm(testing.NewUserRealm(adminAddr))\n\n\t\tuassert.NotAborts(t, cur, func() {\n\t\t\tAdminSetInPause(cross(cur), true)\n\t\t})\n\n\t\tuassert.True(t, inPause)\n\t})\n\n\tt.Run(\"admin set\", func(t *testing.T) {\n\t\tclearState(t)\n\n\t\t// Set the new admin\n\t\tvar (\n\t\t\toldAdmin = adminAddr\n\t\t\tnewAdmin = testutils.TestAddress(\"alice\")\n\t\t)\n\n\t\ttesting.SetRealm(testing.NewUserRealm(adminAddr))\n\t\tAdminSetAdminAddr(cross(cur), newAdmin)\n\n\t\tuassert.Equal(t, adminAddr, newAdmin)\n\n\t\t// Make sure the old admin can't do anything\n\t\ttesting.SetOriginCaller(oldAdmin)\n\t\ttesting.SetRealm(testing.NewUserRealm(oldAdmin))\n\n\t\tuassert.AbortsWithMessage(t, cur, errNotAdmin.Error(), func() {\n\t\t\tAdminSetInPause(cross(cur), false)\n\t\t})\n\t})\n}\n\nfunc TestBlog_AddRemoveModerator(cur realm, t *testing.T) {\n\tclearState(t)\n\n\tmod := testutils.TestAddress(\"mod\")\n\n\t// Add the moderator\n\ttesting.SetOriginCaller(adminAddr)\n\ttesting.SetRealm(testing.NewUserRealm(adminAddr))\n\tAdminAddModerator(cross(cur), mod)\n\n\turequire.True(t, moderatorList.Has(mod.String()))\n\n\t// Remove the moderator\n\tAdminRemoveModerator(cross(cur), mod)\n\n\t// Make sure the moderator is disabled\n\tisMod := moderatorList.Get(mod.String())\n\tuassert.NotNil(t, isMod)\n\n\tuassert.False(t, isMod.(bool))\n}\n\nfunc TestBlog_AddCommenter(cur realm, t *testing.T) {\n\tclearState(t)\n\n\tvar (\n\t\tmod       = testutils.TestAddress(\"mod\")\n\t\tcommenter = testutils.TestAddress(\"comm\")\n\t\trand      = testutils.TestAddress(\"rand\")\n\t)\n\n\t// Appoint the moderator\n\ttesting.SetOriginCaller(adminAddr)\n\ttesting.SetRealm(testing.NewUserRealm(adminAddr))\n\tAdminAddModerator(cross(cur), mod)\n\n\t// Add a commenter as a mod\n\ttesting.SetOriginCaller(mod)\n\ttesting.SetRealm(testing.NewUserRealm(mod))\n\tModAddCommenter(cross(cur), commenter)\n\n\tuassert.True(t, commenterList.Has(commenter.String()))\n\n\t// Make sure a non-mod can't add commenters\n\ttesting.SetOriginCaller(rand)\n\ttesting.SetRealm(testing.NewUserRealm(rand))\n\n\tuassert.AbortsWithMessage(t, cur, errNotModerator.Error(), func() {\n\t\tModAddCommenter(cross(cur), testutils.TestAddress(\"evil\"))\n\t})\n\n\t// Remove a commenter\n\ttesting.SetOriginCaller(mod)\n\ttesting.SetRealm(testing.NewUserRealm(mod))\n\tModDelCommenter(cross(cur), commenter)\n\n\tactive := commenterList.Get(commenter.String())\n\tuassert.False(t, active.(bool))\n}\n\nfunc TestBlog_ManagePost(cur realm, t *testing.T) {\n\tclearState(t)\n\n\tvar (\n\t\tmod  = testutils.TestAddress(\"mod\")\n\t\tslug = \"slug\"\n\t)\n\n\t// Appoint the moderator\n\ttesting.SetOriginCaller(adminAddr)\n\ttesting.SetRealm(testing.NewUserRealm(adminAddr))\n\tAdminAddModerator(cross(cur), mod)\n\n\t// Add the post\n\ttesting.SetOriginCaller(mod)\n\ttesting.SetRealm(testing.NewUserRealm(mod))\n\tModAddPost(\n\t\tcross(cur),\n\t\tslug, \"title\", \"body\", \"2022-05-20T13:17:22Z\", \"moul\", \"tag\",\n\t)\n\n\t// Make sure the post is present\n\tuassert.NotNil(t, b.GetPost(slug))\n\n\t// Remove the post\n\tModRemovePost(cross(cur), slug)\n\tuassert.TypedNil(t, b.GetPost(slug))\n}\n\nfunc TestBlog_ManageComment(cur realm, t *testing.T) {\n\tclearState(t)\n\n\tvar (\n\t\tslug = \"slug\"\n\n\t\tmod       = testutils.TestAddress(\"mod\")\n\t\tcommenter = testutils.TestAddress(\"comm\")\n\t)\n\n\t// Appoint the moderator\n\ttesting.SetOriginCaller(adminAddr)\n\ttesting.SetRealm(testing.NewUserRealm(adminAddr))\n\tAdminAddModerator(cross(cur), mod)\n\n\t// Add a commenter as a mod\n\ttesting.SetOriginCaller(mod)\n\ttesting.SetRealm(testing.NewUserRealm(mod))\n\tModAddCommenter(cross(cur), commenter)\n\n\tuassert.True(t, commenterList.Has(commenter.String()))\n\n\t// Add the post\n\ttesting.SetOriginCaller(mod)\n\ttesting.SetRealm(testing.NewUserRealm(mod))\n\tModAddPost(\n\t\tcross(cur),\n\t\tslug, \"title\", \"body\", \"2022-05-20T13:17:22Z\", \"moul\", \"tag\",\n\t)\n\n\t// Make sure the post is present\n\tuassert.NotNil(t, b.GetPost(slug))\n\n\t// Add the comment\n\ttesting.SetOriginCaller(commenter)\n\ttesting.SetRealm(testing.NewUserRealm(commenter))\n\tuassert.NotAborts(t, cur, func() {\n\t\tAddComment(cross(cur), slug, \"comment\")\n\t})\n\n\t// Delete the comment\n\ttesting.SetOriginCaller(mod)\n\ttesting.SetRealm(testing.NewUserRealm(mod))\n\tuassert.NotAborts(t, cur, func() {\n\t\tModDelComment(cross(cur), slug, 0)\n\t})\n}\n"},{"name":"gnoblog.gno","body":"package blog\n\nimport (\n\t\"chain/runtime/unsafe\"\n\n\t\"gno.land/p/gnoland/blog/v0\"\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":"gnoblog_test.gno","body":"package blog\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestPackage(cur realm, t *testing.T) {\n\tclearState(t)\n\n\ttesting.SetOriginCaller(adminAddr)\n\ttesting.SetRealm(testing.NewUserRealm(adminAddr))\n\n\t// by default, no posts.\n\t{\n\t\tgot := Render(\"\")\n\t\texpected := `\n# Gno.land's blog\n\nNo posts.\n`\n\t\tassertMDEquals(t, got, expected)\n\t}\n\n\t// create two posts, list post.\n\t{\n\t\tModAddPost(cross(cur), \"slug1\", \"title1\", \"body1\", \"2022-05-20T13:17:22Z\", \"moul\", \"tag1,tag2\")\n\t\tModAddPost(cross(cur), \"slug2\", \"title2\", \"body2\", \"2022-05-20T13:17:23Z\", \"moul\", \"tag1,tag3\")\n\t\tgot := Render(\"\")\n\t\texpected := `\n\t\t\t# Gno.land's blog\n\n\u003cgno-columns\u003e\n### [title2](/r/gnoland/blog:p/slug2)\n20 May 2022\n\n\u003cgno-columns-sep\u003e\n\n### [title1](/r/gnoland/blog:p/slug1)\n20 May 2022\n\n\u003cgno-columns-sep\u003e\n\u003c/gno-columns\u003e\n`\n\t\tassertMDEquals(t, got, expected)\n\t}\n\n\t// view post.\n\t{\n\t\tgot := Render(\"p/slug2\")\n\t\texpected := `\n\t\u003cmain class='gno-tmpl-page'\u003e\n\n# title2\n\nbody2\n\n---\n\nTags: [#tag1](/r/gnoland/blog:t/tag1) [#tag3](/r/gnoland/blog:t/tag3)\n\nWritten by moul on 20 May 2022\n\nPublished by g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx to Gno.land's blog\n\n---\n\u003cdetails\u003e\u003csummary\u003eComment section\u003c/summary\u003e\n\n\u003c/details\u003e\n\u003c/main\u003e\n\t\n\t\t`\n\t\tassertMDEquals(t, got, expected)\n\t}\n\n\t// list by tags.\n\t{\n\t\tgot := Render(\"t/invalid\")\n\t\texpected := \"# [Gno.land's blog](/r/gnoland/blog:) / t / invalid\\n\\nNo posts.\"\n\t\tassertMDEquals(t, got, expected)\n\n\t\tgot = Render(\"t/tag2\")\n\t\texpected = `\n# [Gno.land's blog](/r/gnoland/blog:) / t / tag2\n\n\n### [title1](/r/gnoland/blog:p/slug1)\n20 May 2022\n\t\t`\n\t\tassertMDEquals(t, got, expected)\n\t}\n\n\t// add comments.\n\t{\n\t\tAddComment(cross(cur), \"slug1\", \"comment1\")\n\t\tAddComment(cross(cur), \"slug2\", \"comment2\")\n\t\tAddComment(cross(cur), \"slug1\", \"comment3\")\n\t\tAddComment(cross(cur), \"slug2\", \"comment4\")\n\t\tAddComment(cross(cur), \"slug1\", \"comment5\")\n\t\tgot := Render(\"p/slug2\")\n\t\texpected := `\u003cmain class='gno-tmpl-page'\u003e\n\n# title2\n\nbody2\n\n---\n\nTags: [#tag1](/r/gnoland/blog:t/tag1) [#tag3](/r/gnoland/blog:t/tag3)\n\nWritten by moul on 20 May 2022\n\nPublished by g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx to Gno.land's blog\n\n---\n\u003cdetails\u003e\u003csummary\u003eComment section\u003c/summary\u003e\n\n\u003ch5\u003ecomment4\n\n\u003c/h5\u003e\u003ch6\u003eby g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx on 13 Feb 09 23:31 UTC\u003c/h6\u003e\n\n---\n\n\u003ch5\u003ecomment2\n\n\u003c/h5\u003e\u003ch6\u003eby g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx on 13 Feb 09 23:31 UTC\u003c/h6\u003e\n\n---\n\n\u003c/details\u003e\n\u003c/main\u003e\n\n\t\t`\n\t\tassertMDEquals(t, got, expected)\n\t}\n\n\t// edit post.\n\t{\n\t\toldTitle := \"title2\"\n\t\toldDate := \"2022-05-20T13:17:23Z\"\n\n\t\tModEditPost(cur, \"slug2\", oldTitle, \"body2++\", oldDate, \"manfred\", \"tag1,tag4\")\n\t\tgot := Render(\"p/slug2\")\n\t\texpected := `\u003cmain class='gno-tmpl-page'\u003e\n\n# title2\n\nbody2++\n\n---\n\nTags: [#tag1](/r/gnoland/blog:t/tag1) [#tag4](/r/gnoland/blog:t/tag4)\n\nWritten by manfred on 20 May 2022\n\nPublished by g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx to Gno.land's blog\n\n---\n\u003cdetails\u003e\u003csummary\u003eComment section\u003c/summary\u003e\n\n\u003ch5\u003ecomment4\n\n\u003c/h5\u003e\u003ch6\u003eby g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx on 13 Feb 09 23:31 UTC\u003c/h6\u003e\n\n---\n\n\u003ch5\u003ecomment2\n\n\u003c/h5\u003e\u003ch6\u003eby g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx on 13 Feb 09 23:31 UTC\u003c/h6\u003e\n\n---\n\n\u003c/details\u003e\n\u003c/main\u003e\n\n\t\t`\n\t\tassertMDEquals(t, got, expected)\n\n\t\thome := Render(\"\")\n\n\t\tif strings.Count(home, oldTitle) != 1 {\n\t\t\tt.Errorf(\"post not edited properly\")\n\t\t}\n\t\t// Edits work everything except title, slug, and publicationDate\n\t\t// Edits to the above will cause duplication on the blog home page\n\t}\n\t//\n\t{ // Test remove functionality\n\t\ttitle := \"example title\"\n\t\tslug := \"testSlug1\"\n\t\tModAddPost(cross(cur), slug, title, \"body1\", \"2022-05-25T13:17:22Z\", \"moul\", \"tag1,tag2\")\n\n\t\tgot := Render(\"\")\n\n\t\tif !strings.Contains(got, title) {\n\t\t\tt.Errorf(\"post was not added properly\")\n\t\t}\n\n\t\tpostRender := Render(\"p/\" + slug)\n\n\t\tif !strings.Contains(postRender, title) {\n\t\t\tt.Errorf(\"post not rendered properly\")\n\t\t}\n\n\t\tModRemovePost(cur, slug)\n\t\tgot = Render(\"\")\n\n\t\tif strings.Contains(got, title) {\n\t\t\tt.Errorf(\"post was not removed\")\n\t\t}\n\n\t\tpostRender = Render(\"p/\" + slug)\n\n\t\tassertMDEquals(t, postRender, \"404\")\n\t}\n\t//\n\t//\t// TODO: pagination.\n\t//\t// TODO: ?format=...\n\t//\n\t// all 404s\n\t{\n\t\tnotFoundPaths := []string{\n\t\t\t\"p/slug3\",\n\t\t\t\"p\",\n\t\t\t\"p/\",\n\t\t\t\"x/x\",\n\t\t\t\"t\",\n\t\t\t\"t/\",\n\t\t\t\"/\",\n\t\t\t\"p/slug1/\",\n\t\t}\n\t\tfor _, notFoundPath := range notFoundPaths {\n\t\t\tgot := Render(notFoundPath)\n\t\t\texpected := \"404\"\n\t\t\tif got != expected {\n\t\t\t\tt.Errorf(\"path %q: expected %q, got %q.\", notFoundPath, expected, got)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc assertMDEquals(t *testing.T, got, expected string) {\n\tt.Helper()\n\texpected = strings.TrimSpace(expected)\n\tgot = strings.TrimSpace(got)\n\tif expected != got {\n\t\tt.Errorf(\"invalid render output.\\nexpected %q.\\ngot      %q.\", expected, got)\n\t}\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"d9JaeoeXq6H9RrwEtJwbOIWroOG1iC400lrCd0JhLqEnz0nvZdDgXa4repolV0XWAbhXvBMp90ftxxP+HuAWHw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"boards2","path":"gno.land/r/gnoland/boards2/v0","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/v0] 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/v0/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/v0/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/v0$source\u0026file=permissions.gno\n[gno.land/r/gnoland/boards2/v0/permissions]: https://gno.land/r/gnoland/boards2/v0/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/v0]: https://gno.land/p/gnoland/boards/v0\n[Permissions]: https://gno.land/p/gnoland/boards/v0$source\u0026file=permissions.gno#L23\n"},{"name":"boards.gno","body":"package boards2\n\nimport (\n\t\"gno.land/p/gnoland/boards/exts/permissions/v0\"\n\t\"gno.land/p/gnoland/boards/v0\"\n\t\"gno.land/p/moul/realmpath/v0\"\n\t\"gno.land/p/moul/txlink/v0\"\n\t\"gno.land/p/nt/bptree/v0\"\n)\n\nconst (\n\trealmPkgPath         = \"gno.land/r/gnoland/boards2/v0\"\n\tgRealmPath           = \"/r/gnoland/boards2/v0\"\n\tinitialOwner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\" // GovDAO T1 multisig\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(initialOwner)\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\nfunc init() {\n\taddBoard(gBoardsSequence.Next(), initialOwner, \"OpenDiscussions\", true, true)\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/v0\"\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/v0\"\n\t\"gno.land/p/moul/md/v0\"\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/v0\"\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\thubexts \"gno.land/p/gnoland/boards/exts/hub/v0\"\n\t\"gno.land/p/gnoland/boards/v0\"\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 top-level comments of a thread.\n// To reverse iterate use a negative count.\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\t// Replies only has direct replies.\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/v0\"\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/exts/permissions/v0\"\n\t\"gno.land/p/gnoland/boards/v0\"\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(PermissionThreadRepost, validateOpenThreadRepost)\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/v0\"\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/v0\"\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// validateOpenThreadRepost validates PermissionThreadRepost.\n//\n// Expected `args` values:\n// 1. Caller address\n// 2. Board ID\n// 3. Thread ID\n// 4. Destination board ID\n// 5. Repost ID\n// 6. Title\n// 7. Body\nfunc validateOpenThreadRepost(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 repost 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 repost 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/v0\"\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\targs := boards.Args{caller, name, id, listed, open}\n\tgPerms.WithPermission(caller, PermissionBoardCreate, args, func() {\n\t\tassertRealmIsNotLocked()\n\t\tassertBoardNameNotExists(name)\n\t\taddBoard(id, caller, name, listed, open)\n\t})\n\treturn id\n}\n\n// addBoard creates a board and adds it to the realm storage.\n// It performs no permission or validation checks, so callers are responsible\n// for doing their own.\nfunc addBoard(id boards.ID, caller address, name string, listed, open bool) {\n\tboard := boards.New(id)\n\tboard.Name = name\n\tboard.Creator = caller\n\tboard.Meta = \u0026BoardMeta{\n\t\tHiddenThreads: boards.NewPostStorage(),\n\t}\n\n\tif open {\n\t\tboard.Permissions = createOpenBoardPermissions(caller)\n\t} else {\n\t\tboard.Permissions = createBasicBoardPermissions(caller)\n\t}\n\n\tif err := gBoards.Add(board); err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Listed boards are also indexed separately for easier iteration and pagination\n\tif listed {\n\t\tgListedBoardsByID.Set(board.ID.Key(), board)\n\t}\n\n\tchain.Emit(\n\t\t\"BoardCreated\",\n\t\t\"caller\", caller.String(),\n\t\t\"boardID\", board.ID.String(),\n\t\t\"name\", name,\n\t)\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/v0\"\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/v0\"\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/v0\"\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/v0\"\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/v0\"\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/v0\"\n\t\"gno.land/p/jeronimoalbi/mdform/v0\"\n\t\"gno.land/p/jeronimoalbi/pager/v0\"\n\t\"gno.land/p/leon/svgbtn/v0\"\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/moul/mdtable/v0\"\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\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/v0\"\n\t\"gno.land/p/jeronimoalbi/mdform/v0\"\n\t\"gno.land/p/jeronimoalbi/pager/v0\"\n\t\"gno.land/p/moul/md/v0\"\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/v0\"\n\t\"gno.land/p/jeronimoalbi/mdform/v0\"\n\t\"gno.land/p/leon/svgbtn/v0\"\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/moul/mdtable/v0\"\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/v0\"\n\t\"gno.land/p/jeronimoalbi/mdform/v0\"\n\t\"gno.land/p/jeronimoalbi/pager/v0\"\n\t\"gno.land/p/leon/svgbtn/v0\"\n\t\"gno.land/p/moul/md/v0\"\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/v0\"\n\t\"gno.land/p/jeronimoalbi/mdform/v0\"\n\t\"gno.land/p/jeronimoalbi/pager/v0\"\n\t\"gno.land/p/leon/svgbtn/v0\"\n\t\"gno.land/p/moul/md/v0\"\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/v0\"\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/v0\"\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"},{"name":"z_accept_invite_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_accept_invite_00_filetest\n\npackage z_accept_invite_00_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tboards2.RequestInvite(cross(cur), bid)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.AcceptInvite(cross(cur), bid, user)\n\n\tprintln(boards2.IsMember(bid, user))\n\tprintln()\n\tprintln(boards2.Render(\"test123/invites\"))\n}\n\n// Output:\n// true\n//\n// # test123 Invite Requests\n// ### Board has no invite requests\n"},{"name":"z_accept_invite_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_accept_invite_01_filetest\n\npackage z_accept_invite_01_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\n\t// Request an invite as a user that is not a member\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tboards2.RequestInvite(cross(cur), bid)\n\n\t// Add user as a member idependently of the invite request\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.InviteMember(cross(cur), bid, user, boards2.RoleGuest)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.AcceptInvite(cross(cur), bid, user)\n}\n\n// Error:\n// user is already a member\n"},{"name":"z_accept_invite_02_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_accept_invite_02_filetest\n\npackage z_accept_invite_02_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.AcceptInvite(cross(cur), bid, user)\n}\n\n// Error:\n// invite request not found\n"},{"name":"z_accept_invite_03_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_accept_invite_03_filetest\n\npackage z_accept_invite_03_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tboards2.RequestInvite(cross(cur), bid)\n}\n\nfunc main(cur realm) {\n\t// Caller is not a member and has no permission to invite\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tboards2.AcceptInvite(cross(cur), bid, user)\n}\n\n// Error:\n// unauthorized, user g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5 doesn't have the required permission\n"},{"name":"z_ban_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ban_00_filetest\n\npackage z_ban_00_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.Ban(cross(cur), bid, user, boards2.BanDay, \"Unpolite behavior\")\n\n\tprintln(boards2.IsBanned(bid, user))\n}\n\n// Output:\n// true\n"},{"name":"z_ban_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ban_01_filetest\n\npackage z_ban_01_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\n\t// Invite the user as a moderator\n\tboards2.InviteMember(cross(cur), bid, user, boards2.RoleModerator)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.Ban(cross(cur), bid, user, boards2.BanDay, \"Reason\")\n}\n\n// Error:\n// owner, admin and moderator banning is not allowed\n"},{"name":"z_ban_02_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ban_02_filetest\n\npackage z_ban_02_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Try to ban a user without banning permissions\n\tboards2.Ban(cross(cur), bid, \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\", boards2.BanDay, \"Reason\")\n}\n\n// Error:\n// unauthorized, user g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5 doesn't have the required permission\n"},{"name":"z_change_member_role_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_change_member_role_00_filetest\n\npackage z_change_member_role_00_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner   address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tmember  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\tnewRole         = boards2.RoleOwner\n\tbid             = boards.ID(0) // Operate on realm DAO instead of individual boards\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.InviteMember(cross(cur), bid, member, boards2.RoleAdmin)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.ChangeMemberRole(cross(cur), bid, member, newRole)\n\n\t// Ensure that new role has been changed\n\tprintln(boards2.HasMemberRole(bid, member, newRole))\n}\n\n// Output:\n// true\n"},{"name":"z_change_member_role_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_change_member_role_01_filetest\n\npackage z_change_member_role_01_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner   address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tmember  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\tnewRole         = boards2.RoleAdmin\n)\n\nvar bid boards.ID // Operate on board DAO\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"foo123\", false, false)\n\tboards2.InviteMember(cross(cur), bid, member, boards2.RoleGuest)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.ChangeMemberRole(cross(cur), bid, member, newRole)\n\n\t// Ensure that new role has been changed\n\tprintln(boards2.HasMemberRole(bid, member, newRole))\n}\n\n// Output:\n// true\n"},{"name":"z_change_member_role_02_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_change_member_role_02_filetest\n\npackage z_change_member_role_02_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner  address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\towner2 address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\tadmin  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID // Operate on board DAO\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\n\tboards2.InviteMember(cross(cur), bid, owner2, boards2.RoleOwner)\n\tboards2.InviteMember(cross(cur), bid, admin, boards2.RoleAdmin)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\n\tboards2.ChangeMemberRole(cross(cur), bid, owner2, boards2.RoleAdmin)\n}\n\n// Error:\n// admins are not allowed to remove the Owner role\n"},{"name":"z_change_member_role_03_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_change_member_role_03_filetest\n\npackage z_change_member_role_03_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner  address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tadmin  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\tadmin2 address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID // Operate on board DAO\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\n\tboards2.InviteMember(cross(cur), bid, admin, boards2.RoleAdmin)\n\tboards2.InviteMember(cross(cur), bid, admin2, boards2.RoleAdmin)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\n\tboards2.ChangeMemberRole(cross(cur), bid, admin2, boards2.RoleOwner)\n}\n\n// Error:\n// admins are not allowed to promote members to Owner\n"},{"name":"z_change_member_role_04_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_change_member_role_04_filetest\n\npackage z_change_member_role_04_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner   address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tmember  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\tbid             = boards.ID(0)                               // Operate on realm DAO members instead of individual boards\n\tnewRole         = boards2.RoleOwner\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.InviteMember(cross(cur), bid, member, boards2.RoleAdmin)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.ChangeMemberRole(cross(cur), bid, member, newRole) // Owner can promote other members to Owner\n\n\t// Ensure that new role has been changed to owner\n\tprintln(boards2.HasMemberRole(bid, member, newRole))\n}\n\n// Output:\n// true\n"},{"name":"z_change_member_role_05_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_change_member_role_05_filetest\n\npackage z_change_member_role_05_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tadmin address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar bid boards.ID // Operate on board DAO\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\n\tboards2.InviteMember(cross(cur), bid, admin, boards2.RoleGuest)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.ChangeMemberRole(cross(cur), bid, admin, boards.Role(\"foo\"))\n}\n\n// Error:\n// invalid role: foo\n"},{"name":"z_change_member_role_06_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_change_member_role_06_filetest\n\npackage z_change_member_role_06_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tadmin address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar bid boards.ID // Operate on board DAO\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\n\tboards2.InviteMember(cross(cur), bid, admin, boards2.RoleGuest)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.ChangeMemberRole(cross(cur), bid, \"foo\", boards2.RoleModerator)\n}\n\n// Error:\n// invalid member address: foo\n"},{"name":"z_change_member_role_07_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_change_member_role_07_filetest\n\npackage z_change_member_role_07_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.ChangeMemberRole(cross(cur), 0, \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\", boards2.RoleGuest)\n}\n\n// Error:\n// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission\n"},{"name":"z_create_board_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_board_00_filetest\n\npackage z_create_board_00_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tbid := boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\tprintln(\"ID =\", bid)\n}\n\n// Output:\n// ID = 2\n"},{"name":"z_create_board_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_board_01_filetest\n\npackage z_create_board_01_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateBoard(cross(cur), \"\", false, false)\n}\n\n// Error:\n// board name is empty\n"},{"name":"z_create_board_02_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_board_02_filetest\n\npackage z_create_board_02_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner     address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tboardName         = \"test123\"\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.CreateBoard(cross(cur), boardName, false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateBoard(cross(cur), boardName, false, false)\n}\n\n// Error:\n// board already exists\n"},{"name":"z_create_board_03_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_board_03_filetest\n\npackage z_create_board_03_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateBoard(cross(cur), \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\", false, false)\n}\n\n// Error:\n// addresses are not allowed as board name\n"},{"name":"z_create_board_04_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_board_04_filetest\n\npackage z_create_board_04_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc init(cur realm) {\n\t// uinit.RegisterUser is genesis-only since the security fix.\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnoland\", address(\"g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7\"))\n\ttesting.SetHeight(123)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateBoard(cross(cur), \"gnoland\", false, false)\n}\n\n// Error:\n// board name is a user name registered to a different user\n"},{"name":"z_create_board_05_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_board_05_filetest\n\npackage z_create_board_05_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar name = strings.Repeat(\"X\", boards2.MaxBoardNameLength+1)\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateBoard(cross(cur), name, false, false)\n}\n\n// Error:\n// board name is too long, maximum allowed is 50 characters\n"},{"name":"z_create_board_06_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_board_06_filetest\n\npackage z_create_board_06_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner  address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tmember address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\tname           = \"test123\"\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Operate on realm DAO members instead of individual boards\n\tboards2.InviteMember(cross(cur), 0, member, boards2.RoleOwner)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(member))\n\n\t// Create a board as an invited realm member\n\tbid := boards2.CreateBoard(cross(cur), name, false, false)\n\tprintln(\"ID =\", bid)\n}\n\n// Output:\n// ID = 2\n"},{"name":"z_create_board_07_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_board_07_filetest\n\npackage z_create_board_07_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateBoard(cross(cur), \"test123\", false, false)\n}\n\n// Error:\n// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission\n"},{"name":"z_create_board_08_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_board_08_filetest\n\npackage z_create_board_08_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tname          = \"TestBoard\"\n)\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateBoard(cross(cur), name, false, false)\n\n\t// Unlisted board should not be rendered\n\tprintln(boards2.Render(\"\"))\n\n\t// Unlisted board can be rendered by path\n\tprintln(\"\\n==================\")\n\tprintln(boards2.Render(name))\n}\n\n// Output:\n// # Boards\n// [Create Board](/r/gnoland/boards2/v0:create-board) • [List Admin Users](/r/gnoland/boards2/v0:admin-users) • [Help](/r/gnoland/boards2/v0:help)\n//\n// ---\n// Sort by: [newest first](/r/gnoland/boards2/v0:?order=desc)\n//\n// ###### [OpenDiscussions](/r/gnoland/boards2/v0:OpenDiscussions)\n// Created by [g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) on 2009-02-13 11:31pm UTC, #1\n// **0 threads**\n//\n//\n//\n// ==================\n// # [Boards](/r/gnoland/boards2/v0) › TestBoard\n// Created by [g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) on 2009-02-13 11:31pm UTC, #2\n// ↳ [Create Thread](/r/gnoland/boards2/v0:TestBoard/create-thread) • [Request Invite](/r/gnoland/boards2/v0$help\u0026func=RequestInvite\u0026boardID=2) • [Manage Board](?menu=manageBoard)\n//\n// ---\n// ### This board doesn't have any threads\n// Do you want to [start a new conversation](/r/gnoland/boards2/v0:TestBoard/create-thread) in this board?\n"},{"name":"z_create_board_09_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_board_09_filetest\n\npackage z_create_board_09_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.CreateBoard(cross(cur), \"TEST123\", false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Should fail because board name already exists with a different casing\n\tboards2.CreateBoard(cross(cur), \"test123\", false, false)\n}\n\n// Error:\n// board already exists\n"},{"name":"z_create_board_10_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_board_10_filetest\n\npackage z_create_board_10_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Should fail because board name has a space which is not allowed\n\tboards2.CreateBoard(cross(cur), \"test 123\", false, false)\n}\n\n// Error:\n// board name must start with a letter and have letters, numbers, \"-\" and \"_\"\n"},{"name":"z_create_reply_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_reply_00_filetest\n\npackage z_create_reply_00_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner   address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tpath            = \"test-board/1/2\"\n\tcomment         = \"Test comment\"\n)\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\trid := boards2.CreateReply(cross(cur), bid, tid, 0, comment)\n\n\t// Ensure that returned ID is right\n\tprintln(rid == 2)\n\n\t// Render content must contain the reply\n\tcontent := boards2.Render(path)\n\tprintln(strings.Contains(content, \"\\n\u003e \"+comment))\n}\n\n// Output:\n// true\n// true\n"},{"name":"z_create_reply_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_reply_01_filetest\n\npackage z_create_reply_01_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateReply(cross(cur), 404, 1, 0, \"comment\")\n}\n\n// Error:\n// board does not exist with ID: 404\n"},{"name":"z_create_reply_02_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_reply_02_filetest\n\npackage z_create_reply_02_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateReply(cross(cur), bid, 404, 0, \"comment\")\n}\n\n// Error:\n// thread does not exist with ID: 404\n"},{"name":"z_create_reply_03_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_reply_03_filetest\n\npackage z_create_reply_03_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateReply(cross(cur), bid, tid, 404, \"comment\")\n}\n\n// Error:\n// reply does not exist with ID: 404\n"},{"name":"z_create_reply_04_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_reply_04_filetest\n\npackage z_create_reply_04_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n\n\t// Hide thread by flagging it so reply can't be submitted\n\tboards2.FlagThread(cross(cur), bid, tid, \"reason\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateReply(cross(cur), bid, tid, 0, \"Test reply\")\n}\n\n// Error:\n// thread is hidden\n"},{"name":"z_create_reply_05_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_reply_05_filetest\n\npackage z_create_reply_05_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid      boards.ID\n\ttid, rid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross(cur), bid, tid, 0, \"reply1\")\n\n\t// Hide thread by flagging it so reply of a reply can't be submitted\n\tboards2.FlagThread(cross(cur), bid, tid, \"reason\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateReply(cross(cur), bid, tid, rid, \"reply1.1\")\n}\n\n// Error:\n// thread is hidden\n"},{"name":"z_create_reply_06_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_reply_06_filetest\n\npackage z_create_reply_06_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid      boards.ID\n\ttid, rid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"thread\", \"thread\")\n\trid = boards2.CreateReply(cross(cur), bid, tid, 0, \"reply1\")\n\n\t// Hide reply by flagging it so sub reply can't be submitted\n\tboards2.FlagReply(cross(cur), bid, tid, rid, \"reason\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateReply(cross(cur), bid, tid, rid, \"reply1.1\")\n}\n\n// Error:\n// replying to a hidden or frozen reply is not allowed\n"},{"name":"z_create_reply_07_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_reply_07_filetest\n\npackage z_create_reply_07_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tboards2.CreateReply(cross(cur), bid, tid, 0, \"Test reply\")\n}\n\n// Error:\n// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission\n"},{"name":"z_create_reply_08_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_reply_08_filetest\n\npackage z_create_reply_08_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateReply(cross(cur), bid, tid, 0, \"\")\n}\n\n// Error:\n// body is empty\n"},{"name":"z_create_reply_09_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_reply_09_filetest\n\npackage z_create_reply_09_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner   address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tpath            = \"test-board/1/2\"\n\tcomment         = \"Second comment\"\n)\n\nvar (\n\tbid      boards.ID\n\ttid, rid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross(cur), bid, tid, 0, \"First comment\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\trid2 := boards2.CreateReply(cross(cur), bid, tid, rid, comment)\n\n\t// Ensure that returned ID is right\n\tprintln(rid2 == 3)\n\n\t// Render content must contain the sub-reply\n\tcontent := boards2.Render(path)\n\tprintln(strings.Contains(content, \"\\n\u003e \u003e \"+comment))\n}\n\n// Output:\n// true\n// true\n"},{"name":"z_create_reply_10_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_reply_10_filetest\n\npackage z_create_reply_10_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner   address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tcomment         = \"Second comment\"\n)\n\nvar (\n\tbid      boards.ID\n\ttid, rid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross(cur), bid, tid, 0, \"Parent comment\")\n\n\t// Flag parent post so it's hidden\n\tboards2.FlagReply(cross(cur), bid, tid, rid, \"Reason\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateReply(cross(cur), bid, tid, rid, \"Sub comment\")\n}\n\n// Error:\n// replying to a hidden or frozen reply is not allowed\n"},{"name":"z_create_reply_11_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_reply_11_filetest\n\npackage z_create_reply_11_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"foo\", \"bar\")\n\tboards2.FreezeThread(cross(cur), bid, tid)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// cannot reply to a frozen thread\n\tboards2.CreateReply(cross(cur), bid, tid, 0, \"foobar\")\n}\n\n// Error:\n// thread is frozen\n"},{"name":"z_create_reply_12_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_reply_12_filetest\n\npackage z_create_reply_12_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateReply(cross(cur), bid, tid, 0, strings.Repeat(\"x\", boards2.MaxReplyLength+1))\n}\n\n// Error:\n// reply is too long, maximum allowed is 10000 characters\n"},{"name":"z_create_reply_13_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_reply_13_filetest\n\n// A reply may now contain Markdown block structure (here a blockquote).\n// The write-time blacklist used to reject it; now the reply body renders\n// inside a \u003cgno-foreign\u003e sandbox, so its structure is contained and\n// cannot hijack realm chrome.\npackage z_create_reply_13_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Previously rejected (\"headings, blockquotes ... not allowed\").\n\tboards2.CreateReply(cross(cur), bid, tid, 0, \"\u003e Markdown blockquote\")\n\n\t// The reply body is wrapped in a gno-foreign sandbox at render time.\n\tcontent := boards2.Render(\"test-board/1\")\n\tprintln(strings.Contains(content, \"Markdown blockquote\") \u0026\u0026 strings.Contains(content, \"\u003cgno-foreign\u003e\"))\n}\n\n// Output:\n// true\n"},{"name":"z_create_reply_14_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_reply_14_filetest\n\n// Open board: Test creating a new reply as a non member user\npackage z_create_reply_14_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"chain\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner   address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser    address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\tcomment         = \"Test Comment\"\n)\n\nvar (\n\tbid boards.ID // Operate on board DAO\n\ttid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, true)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Title\", \"Body\")\n\n\t// Make sure user account has the required amount of GNOT for open board actions\n\ttesting.IssueCoins(user, chain.Coins{{\"ugnot\", 3_000_000_000}})\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Non members should be able to add replies\n\tboards2.CreateReply(cross(cur), bid, tid, 0, comment)\n\n\t// Render content must contain the reply\n\tcontent := boards2.Render(\"test123/1\")\n\tprintln(strings.Contains(content, \"\\n\u003e \"+comment))\n}\n\n// Output:\n// true\n"},{"name":"z_create_reply_15_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_reply_15_filetest\n\n// Open board: Test creating a new reply as a non member user that has no GNOT\npackage z_create_reply_15_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner   address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser    address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\tcomment         = \"Test Comment\"\n)\n\nvar (\n\tbid boards.ID // Operate on board DAO\n\ttid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, true)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Title\", \"Body\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Non members should be able to add replies only if they have enough GNOT\n\tboards2.CreateReply(cross(cur), bid, tid, 0, comment)\n}\n\n// Error:\n// caller is not allowed to comment: account amount is lower than 3000 GNOT\n"},{"name":"z_create_reply_16_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_reply_16_filetest\n\n// A reply may now contain a \u003cgno-form\u003e. The write-time blacklist used to\n// reject it; now the reply body renders inside a \u003cgno-foreign\u003e sandbox\n// whose inner renderer does not load forms (gnoweb safe-mode omits the\n// raw HTML at render).\npackage z_create_reply_16_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar bid, tid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Previously rejected (\"forms are not allowed in replies\").\n\tboards2.CreateReply(cross(cur), bid, tid, 0, \"\u003cgno-form\u003e\u003cgno-select name=\\\"foo\\\" value=\\\"\\\" /\u003e\u003c/gno-form\u003e\")\n\n\t// Accepted; the body is wrapped in a gno-foreign sandbox.\n\tcontent := boards2.Render(\"test-board/1\")\n\tprintln(strings.Contains(content, \"\u003cgno-foreign\u003e\"))\n}\n\n// Output:\n// true\n"},{"name":"z_create_repost_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_repost_00_filetest\n\npackage z_create_repost_00_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tsrcBID boards.ID\n\tdstBID boards.ID\n\tsrcTID boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tsrcBID = boards2.CreateBoard(cross(cur), \"src-board\", false, false)\n\tdstBID = boards2.CreateBoard(cross(cur), \"dst-board\", false, false)\n\n\tsrcTID = boards2.CreateThread(cross(cur), srcBID, \"Foo\", \"bar\")\n\tboards2.FlagThread(cross(cur), srcBID, srcTID, \"idk\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Repost should fail if source thread is flagged\n\tboards2.CreateRepost(cross(cur), srcBID, srcTID, dstBID, \"foo\", \"bar\")\n}\n\n// Error:\n// thread is hidden\n"},{"name":"z_create_repost_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_repost_01_filetest\n\npackage z_create_repost_01_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tsrcBID boards.ID\n\tdstBID boards.ID\n\tsrcTID boards.ID = 1024\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tsrcBID = boards2.CreateBoard(cross(cur), \"src-board\", false, false)\n\tdstBID = boards2.CreateBoard(cross(cur), \"dst-board\", false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Repost should fail if source thread doesn't exist\n\tboards2.CreateRepost(cross(cur), srcBID, srcTID, dstBID, \"foo\", \"bar\")\n}\n\n// Error:\n// thread does not exist with ID: 1024\n"},{"name":"z_create_repost_02_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_repost_02_filetest\n\npackage z_create_repost_02_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tsrcBID boards.ID\n\tdstBID boards.ID\n\tsrcTID boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tsrcBID = boards2.CreateBoard(cross(cur), \"src-board\", false, false)\n\tdstBID = boards2.CreateBoard(cross(cur), \"dst-board\", false, false)\n\n\tsrcTID = boards2.CreateThread(cross(cur), srcBID, \"Foo\", \"bar\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tboards2.CreateRepost(cross(cur), srcBID, srcTID, dstBID, \"foo\", \"bar\")\n}\n\n// Error:\n// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission\n"},{"name":"z_create_repost_03_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_repost_03_filetest\n\npackage z_create_repost_03_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tsrcBID boards.ID\n\tdstBID boards.ID\n\tsrcTID boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tsrcBID = boards2.CreateBoard(cross(cur), \"src-board\", false, false)\n\tdstBID = boards2.CreateBoard(cross(cur), \"dst-board\", false, false)\n\n\tsrcTID = boards2.CreateThread(cross(cur), srcBID, \"original title\", \"original text\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Success case\n\ttID := boards2.CreateRepost(cross(cur), srcBID, srcTID, dstBID, \"repost title\", \"repost text\")\n\tp := ufmt.Sprintf(\"dst-board/%s\", tID)\n\tout := boards2.Render(p)\n\n\tprintln(strings.Contains(out, \"original text\"))\n\tprintln(strings.Contains(out, \"repost title\"))\n\tprintln(strings.Contains(out, \"repost text\"))\n}\n\n// Output:\n// true\n// true\n// true\n"},{"name":"z_create_repost_04_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_repost_04_filetest\n\npackage z_create_repost_04_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tsrcBID boards.ID\n\tdstBID boards.ID\n\tsrcTID boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a board with a thread\n\torigBID := boards2.CreateBoard(cross(cur), \"origin-board\", false, false)\n\torigTID := boards2.CreateThread(cross(cur), origBID, \"title\", \"text\")\n\n\t// Create a second board and repost a thread using an empty title\n\tsrcBID = boards2.CreateBoard(cross(cur), \"source-board\", false, false)\n\tsrcTID = boards2.CreateRepost(cross(cur), origBID, origTID, srcBID, \"original title\", \"original text\")\n\n\t// Create a third board to try reposting the repost\n\tdstBID = boards2.CreateBoard(cross(cur), \"destination-board\", false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateRepost(cross(cur), srcBID, srcTID, dstBID, \"repost title\", \"repost text\")\n}\n\n// Error:\n// reposting a thread that is a repost is not allowed\n"},{"name":"z_create_repost_05_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_repost_05_filetest\n\n// Creating a thread requires the user to have 3000 GNOT, but reposing a thread\n// also creates a thread. Test that this fails when the user has insufficient GNOT.\npackage z_create_repost_05_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tsrcBID boards.ID\n\tdstBID boards.ID\n\tsrcTID boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tsrcBID = boards2.CreateBoard(cross(cur), \"src-board\", false, false)\n\tdstBID = boards2.CreateBoard(cross(cur), \"dst-board\", false, true)\n\n\tsrcTID = boards2.CreateThread(cross(cur), srcBID, \"Foo\", \"bar\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Non members should be able to repost threads only if they have enough GNOT\n\tboards2.CreateRepost(cross(cur), srcBID, srcTID, dstBID, \"foo\", \"bar\")\n}\n\n// Error:\n// caller is not allowed to repost threads: account amount is lower than 3000 GNOT\n"},{"name":"z_create_thread_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_thread_00_filetest\n\npackage z_create_thread_00_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\ttitle         = \"Test Thread\"\n\tbody          = \"Test body\"\n\tpath          = \"test-board/1\"\n)\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\ttid := boards2.CreateThread(cross(cur), bid, title, body)\n\n\t// Ensure that returned ID is right\n\tprintln(tid == 1)\n\n\t// Thread should not be frozen by default\n\tprintln(boards2.IsThreadFrozen(bid, tid))\n\n\t// Render content must contains thread's title and body\n\tprintln(boards2.Render(path))\n}\n\n// Output:\n// true\n// false\n// # [Boards](/r/gnoland/boards2/v0) › [test\\-board](/r/gnoland/boards2/v0:test-board)\n// ## Test Thread\n//\n// **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC\n//\n//\n// \u003cgno-foreign\u003e\n// Test body\n// \u003c/gno-foreign\u003e\n//\n//\n//\n// ↳ [Flag](/r/gnoland/boards2/v0:test-board/1/flag) • [Repost](/r/gnoland/boards2/v0:test-board/1/repost) • [Comment](/r/gnoland/boards2/v0:test-board/1/reply) • [Edit](/r/gnoland/boards2/v0:test-board/1/edit) • [Delete](/r/gnoland/boards2/v0$help\u0026func=DeleteThread\u0026boardID=2\u0026threadID=1)\n"},{"name":"z_create_thread_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_thread_01_filetest\n\npackage z_create_thread_01_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateThread(cross(cur), 404, \"Foo\", \"bar\")\n}\n\n// Error:\n// board does not exist with ID: 404\n"},{"name":"z_create_thread_02_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_thread_02_filetest\n\npackage z_create_thread_02_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tboards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n}\n\n// Error:\n// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission\n"},{"name":"z_create_thread_03_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_thread_03_filetest\n\npackage z_create_thread_03_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateThread(cross(cur), bid, \"\", \"bar\")\n}\n\n// Error:\n// title is empty\n"},{"name":"z_create_thread_04_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_thread_04_filetest\n\npackage z_create_thread_04_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateThread(cross(cur), bid, \"Foo\", \"\")\n}\n\n// Error:\n// thread body is required\n"},{"name":"z_create_thread_05_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_thread_05_filetest\n\n// Open board: Test creating a new thread as a non member user\npackage z_create_thread_05_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\ttitle         = \"Test Thread\"\n\tbody          = \"Test body\"\n)\n\nvar bid boards.ID // Operate on board DAO\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, true)\n\n\t// Make sure user account has the required amount of GNOT for open board actions\n\ttesting.IssueCoins(user, chain.Coins{{\"ugnot\", 3_000_000_000}})\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Non members should be able to create threads\n\ttid := boards2.CreateThread(cross(cur), bid, title, body)\n\n\t// Ensure that returned ID is right\n\tprintln(tid == 1)\n\n\t// Render content must contains thread's title and body\n\tprintln(boards2.Render(\"test123/1\"))\n}\n\n// Output:\n// true\n// # [Boards](/r/gnoland/boards2/v0) › [test123](/r/gnoland/boards2/v0:test123)\n// ## Test Thread\n//\n// **[g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj](/u/g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj)** · 2009-02-13 11:31pm UTC\n//\n//\n// \u003cgno-foreign\u003e\n// Test body\n// \u003c/gno-foreign\u003e\n//\n//\n//\n// ↳ [Flag](/r/gnoland/boards2/v0:test123/1/flag) • [Repost](/r/gnoland/boards2/v0:test123/1/repost) • [Comment](/r/gnoland/boards2/v0:test123/1/reply) • [Edit](/r/gnoland/boards2/v0:test123/1/edit) • [Delete](/r/gnoland/boards2/v0$help\u0026func=DeleteThread\u0026boardID=2\u0026threadID=1)\n"},{"name":"z_create_thread_06_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_create_thread_06_filetest\n\n// Open board: Test creating a new thread as a non member user that has no GNOT\npackage z_create_thread_06_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar bid boards.ID // Operate on board DAO\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, true)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Non members should be able to create threads only if they have enough GNOT\n\tboards2.CreateThread(cross(cur), bid, \"Title\", \"Body\")\n}\n\n// Error:\n// caller is not allowed to create threads: account amount is lower than 3000 GNOT\n"},{"name":"z_delete_reply_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_delete_reply_00_filetest\n\npackage z_delete_reply_00_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid      boards.ID\n\ttid, rid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross(cur), bid, tid, 0, \"body\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.DeleteReply(cross(cur), bid, tid, rid)\n\n\t// Ensure reply doesn't exist\n\tprintln(boards2.Render(\"test-board/1/2\"))\n}\n\n// Output:\n// Reply not found\n"},{"name":"z_delete_reply_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_delete_reply_01_filetest\n\npackage z_delete_reply_01_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.DeleteReply(cross(cur), 404, 1, 1)\n}\n\n// Error:\n// board does not exist with ID: 404\n"},{"name":"z_delete_reply_02_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_delete_reply_02_filetest\n\npackage z_delete_reply_02_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.DeleteReply(cross(cur), bid, 404, 1)\n}\n\n// Error:\n// thread does not exist with ID: 404\n"},{"name":"z_delete_reply_03_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_delete_reply_03_filetest\n\npackage z_delete_reply_03_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.DeleteReply(cross(cur), bid, tid, 404)\n}\n\n// Error:\n// reply does not exist with ID: 404\n"},{"name":"z_delete_reply_04_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_delete_reply_04_filetest\n\npackage z_delete_reply_04_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid      boards.ID\n\ttid, rid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n\n\trid = boards2.CreateReply(cross(cur), bid, tid, 0, \"Parent\")\n\tboards2.CreateReply(cross(cur), bid, tid, rid, \"Child reply\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.DeleteReply(cross(cur), bid, tid, rid)\n\n\t// Render content must contain the releted message instead of reply's body\n\tprintln(boards2.Render(\"test-board/1/2\"))\n}\n\n// Output:\n// ## Foo\n//\n// **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC\n//\n//\n// \u003cgno-foreign\u003e\n// bar\n// \u003c/gno-foreign\u003e\n//\n//\n//\n// ↳ [Flag](/r/gnoland/boards2/v0:test-board/1/flag) • [Repost](/r/gnoland/boards2/v0:test-board/1/repost) • [Comment](/r/gnoland/boards2/v0:test-board/1/reply) [1] • [Edit](/r/gnoland/boards2/v0:test-board/1/edit) • [Delete](/r/gnoland/boards2/v0$help\u0026func=DeleteThread\u0026boardID=2\u0026threadID=1) • [Show all Replies](/r/gnoland/boards2/v0:test-board/1)\n//\n//\n// \u003e\n// \u003e **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC [\\#2](/r/gnoland/boards2/v0:test-board/1/2)\n// \u003e\n// \u003e\n// \u003e \u003cgno-foreign\u003e\n// \u003e ⚠ This comment has been deleted\n// \u003e \u003c/gno-foreign\u003e\n// \u003e\n// \u003e\n// \u003e\n// \u003e ↳ [Flag](/r/gnoland/boards2/v0:test-board/1/2/flag) • [Reply](/r/gnoland/boards2/v0:test-board/1/2/reply) [1] • [Edit](/r/gnoland/boards2/v0:test-board/1/2/edit) • [Delete](/r/gnoland/boards2/v0$help\u0026func=DeleteReply\u0026boardID=2\u0026replyID=2\u0026threadID=1)\n//\n// ---\n// Sort by: [newest first](/r/gnoland/boards2/v0:test-board/1/2?order=desc)\n// \u003e\n// \u003e \u003e\n// \u003e \u003e **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC [\\#3](/r/gnoland/boards2/v0:test-board/1/3)\n// \u003e \u003e\n// \u003e \u003e\n// \u003e \u003e \u003cgno-foreign\u003e\n// \u003e \u003e Child reply\n// \u003e \u003e \u003c/gno-foreign\u003e\n// \u003e \u003e\n// \u003e \u003e\n// \u003e \u003e\n// \u003e \u003e ↳ [Flag](/r/gnoland/boards2/v0:test-board/1/3/flag) • [Reply](/r/gnoland/boards2/v0:test-board/1/3/reply) • [Edit](/r/gnoland/boards2/v0:test-board/1/3/edit) • [Delete](/r/gnoland/boards2/v0$help\u0026func=DeleteReply\u0026boardID=2\u0026replyID=3\u0026threadID=1)\n"},{"name":"z_delete_reply_05_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_delete_reply_05_filetest\n\npackage z_delete_reply_05_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid      boards.ID\n\ttid, rid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross(cur), bid, tid, 0, \"body\")\n}\n\nfunc main(cur realm) {\n\t// Call using a user that has not permission to delete replies\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tboards2.DeleteReply(cross(cur), bid, tid, rid)\n}\n\n// Error:\n// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission\n"},{"name":"z_delete_reply_06_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_delete_reply_06_filetest\n\npackage z_delete_reply_06_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner  address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tmember address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid      boards.ID\n\ttid, rid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross(cur), bid, tid, 0, \"body\")\n\n\t// Invite a member using a role with permission to delete replies\n\tboards2.InviteMember(cross(cur), bid, member, boards2.RoleAdmin)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(member))\n\n\tboards2.DeleteReply(cross(cur), bid, tid, rid)\n\n\t// Ensure reply doesn't exist\n\tprintln(boards2.Render(\"test-board/1/2\"))\n}\n\n// Output:\n// Reply not found\n"},{"name":"z_delete_reply_07_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_delete_reply_07_filetest\n\n// Open board: Test deleting a reply as the non member user that created it\npackage z_delete_reply_07_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid      boards.ID // Operate on board DAO\n\ttid, rid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, true)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Title\", \"Body\")\n\n\t// Make sure user account has the required amount of GNOT for open board actions\n\ttesting.IssueCoins(user, chain.Coins{{\"ugnot\", 3_000_000_000}})\n\n\t// Create a new reply as a non member user\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\trid = boards2.CreateReply(cross(cur), bid, tid, 0, \"Comment\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Delete the reply as the non member user that created it\n\tboards2.DeleteReply(cross(cur), bid, tid, rid)\n\n\t// Ensure reply doesn't exist\n\tprintln(rid == 2)\n\tprintln(boards2.Render(\"test123/1/2\"))\n}\n\n// Output:\n// true\n// Reply not found\n"},{"name":"z_delete_reply_08_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_delete_reply_08_filetest\n\n// Open board: Test deleting a reply of another non member user\npackage z_delete_reply_08_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\tuser2 address = \"g125t352u4pmdrr57emc4pe04y40sknr5ztng5mt\"\n)\n\nvar (\n\tbid      boards.ID // Operate on board DAO\n\ttid, rid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, true)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Title\", \"Body\")\n\n\t// Make sure user account has the required amount of GNOT for open board actions\n\ttesting.IssueCoins(user, chain.Coins{{\"ugnot\", 3_000_000_000}})\n\n\t// Create a new reply as a non member user\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\trid = boards2.CreateReply(cross(cur), bid, tid, 0, \"Comment\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user2))\n\n\t// Try to delete the reply of another non member user\n\tboards2.DeleteReply(cross(cur), bid, tid, rid)\n}\n\n// Error:\n// unauthorized, user g125t352u4pmdrr57emc4pe04y40sknr5ztng5mt doesn't have the required permission\n"},{"name":"z_delete_reply_09_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_delete_reply_09_filetest\n\n// Hard-deleting a childless DIRECT thread reply removes it from the thread's\n// direct-children list (not just the flat AllReplies index), so the threaded\n// view doesn't render a ghost comment and the reply count stays accurate.\npackage z_delete_reply_09_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n\trb  boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"OP body\")\n\tboards2.CreateReply(cross(cur), bid, tid, 0, \"reply-A\")      // #2, direct, leaf\n\trb = boards2.CreateReply(cross(cur), bid, tid, 0, \"reply-B\") // #3, direct, leaf\n\tboards2.CreateReply(cross(cur), bid, tid, 0, \"reply-C\")      // #4, direct, leaf\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.DeleteReply(cross(cur), bid, tid, rb) // hard delete the leaf\n\n\tout := boards2.Render(\"test-board/1\")\n\t// A and C remain; B must be fully gone (no ghost) from the threaded view.\n\tok := strings.Contains(out, \"reply-A\") \u0026\u0026\n\t\tstrings.Contains(out, \"reply-C\") \u0026\u0026\n\t\t!strings.Contains(out, \"reply-B\")\n\tprintln(ok)\n}\n\n// Output:\n// true\n"},{"name":"z_delete_thread_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_delete_thread_00_filetest\n\npackage z_delete_thread_00_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\ttitle         = \"Test Thread\"\n\tbody          = \"Test body\"\n)\n\nvar (\n\tbid boards.ID\n\tpid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\tpid = boards2.CreateThread(cross(cur), bid, title, body)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.DeleteThread(cross(cur), bid, pid)\n\n\t// Ensure thread doesn't exist\n\tprintln(boards2.Render(\"test-board/1\"))\n}\n\n// Output:\n// Thread not found\n"},{"name":"z_delete_thread_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_delete_thread_01_filetest\n\npackage z_delete_thread_01_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.DeleteThread(cross(cur), 404, 1)\n}\n\n// Error:\n// board does not exist with ID: 404\n"},{"name":"z_delete_thread_02_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_delete_thread_02_filetest\n\npackage z_delete_thread_02_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.DeleteThread(cross(cur), bid, 404)\n}\n\n// Error:\n// thread does not exist with ID: 404\n"},{"name":"z_delete_thread_03_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_delete_thread_03_filetest\n\npackage z_delete_thread_03_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid boards.ID\n\tpid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\tpid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n}\n\nfunc main(cur realm) {\n\t// Call using a user that has not permission to delete threads\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tboards2.DeleteThread(cross(cur), bid, pid)\n}\n\n// Error:\n// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission\n"},{"name":"z_delete_thread_04_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_delete_thread_04_filetest\n\npackage z_delete_thread_04_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner  address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tmember address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid boards.ID\n\tpid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\tpid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n\n\t// Invite a member using a role with permission to delete threads\n\tboards2.InviteMember(cross(cur), bid, member, boards2.RoleAdmin)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(member))\n\n\tboards2.DeleteThread(cross(cur), bid, pid)\n\n\t// Ensure thread doesn't exist\n\tprintln(boards2.Render(\"test-board/1\"))\n}\n\n// Output:\n// Thread not found\n"},{"name":"z_delete_thread_05_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_delete_thread_05_filetest\n\n// Open board: Test deleting a thread as the non member user that created it\npackage z_delete_thread_05_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid boards.ID // Operate on board DAO\n\ttid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, true)\n\n\t// Make sure user account has the required amount of GNOT for open board actions\n\ttesting.IssueCoins(user, chain.Coins{{\"ugnot\", 3_000_000_000}})\n\n\t// Create a new reply as a non member user\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\ttid = boards2.CreateThread(cross(cur), bid, \"Title\", \"Body\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Delete the thread as the non member user that created it\n\tboards2.DeleteThread(cross(cur), bid, tid)\n\n\t// Ensure reply doesn't exist\n\tprintln(tid == 1)\n\tprintln(boards2.Render(\"test123/1\"))\n}\n\n// Output:\n// true\n// Thread not found\n"},{"name":"z_delete_thread_06_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_delete_thread_06_filetest\n\n// Open board: Test deleting a reply of another non member user\npackage z_delete_thread_06_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\tuser2 address = \"g125t352u4pmdrr57emc4pe04y40sknr5ztng5mt\"\n)\n\nvar (\n\tbid boards.ID // Operate on board DAO\n\ttid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, true)\n\n\t// Make sure user account has the required amount of GNOT for open board actions\n\ttesting.IssueCoins(user, chain.Coins{{\"ugnot\", 3_000_000_000}})\n\n\t// Create a new reply as a non member user\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\ttid = boards2.CreateThread(cross(cur), bid, \"Title\", \"Body\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user2))\n\n\t// Try to delete the thread of another non member user\n\tboards2.DeleteThread(cross(cur), bid, tid)\n}\n\n// Error:\n// unauthorized, user g125t352u4pmdrr57emc4pe04y40sknr5ztng5mt doesn't have the required permission\n"},{"name":"z_edit_reply_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_edit_reply_00_filetest\n\npackage z_edit_reply_00_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tbody          = \"Test reply\"\n\tpath          = \"test-board/1/2\"\n)\n\nvar (\n\tbid      boards.ID\n\ttid, rid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross(cur), bid, tid, 0, \"body\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.EditReply(cross(cur), bid, tid, rid, body)\n\n\t// Render content must contain the modified reply\n\tcontent := boards2.Render(path)\n\tprintln(strings.Contains(content, \"\\n\u003e \"+body))\n}\n\n// Output:\n// true\n"},{"name":"z_edit_reply_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_edit_reply_01_filetest\n\npackage z_edit_reply_01_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tbody          = \"Test reply\"\n\tpath          = \"test-board/1/2\"\n)\n\nvar (\n\tbid      boards.ID\n\ttid, rid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n\n\t// Create a reply and a sub reply\n\tparentRID := boards2.CreateReply(cross(cur), bid, tid, 0, \"Parent\")\n\trid = boards2.CreateReply(cross(cur), bid, tid, parentRID, \"Child\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.EditReply(cross(cur), bid, tid, rid, body)\n\n\t// Render content must contain the modified reply\n\tcontent := boards2.Render(path)\n\tprintln(strings.Contains(content, \"\\n\u003e \u003e \"+body))\n}\n\n// Output:\n// true\n"},{"name":"z_edit_reply_02_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_edit_reply_02_filetest\n\npackage z_edit_reply_02_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.EditReply(cross(cur), 404, 1, 0, \"body\")\n}\n\n// Error:\n// board does not exist with ID: 404\n"},{"name":"z_edit_reply_03_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_edit_reply_03_filetest\n\npackage z_edit_reply_03_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.EditReply(cross(cur), bid, 404, 0, \"body\")\n}\n\n// Error:\n// thread does not exist with ID: 404\n"},{"name":"z_edit_reply_04_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_edit_reply_04_filetest\n\npackage z_edit_reply_04_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.EditReply(cross(cur), bid, tid, 404, \"body\")\n}\n\n// Error:\n// reply does not exist with ID: 404\n"},{"name":"z_edit_reply_05_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_edit_reply_05_filetest\n\npackage z_edit_reply_05_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid      boards.ID\n\ttid, rid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross(cur), bid, tid, 0, \"body\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tboards2.EditReply(cross(cur), bid, tid, rid, \"new body\")\n}\n\n// Error:\n// only the reply creator is allowed to edit it\n"},{"name":"z_edit_reply_06_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_edit_reply_06_filetest\n\npackage z_edit_reply_06_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid      boards.ID\n\ttid, rid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross(cur), bid, tid, 0, \"body\")\n\n\t// Flag the reply so it's hidden\n\tboards2.FlagReply(cross(cur), bid, tid, rid, \"reason\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.EditReply(cross(cur), bid, tid, rid, \"body\")\n}\n\n// Error:\n// reply is hidden\n"},{"name":"z_edit_reply_07_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_edit_reply_07_filetest\n\npackage z_edit_reply_07_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid      boards.ID\n\ttid, rid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross(cur), bid, tid, 0, \"body\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.EditReply(cross(cur), bid, tid, rid, \"\")\n}\n\n// Error:\n// body is empty\n"},{"name":"z_edit_reply_08_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_edit_reply_08_filetest\n\n// Editing a reply to contain a \u003cgno-form\u003e is now accepted: the\n// write-time blacklist relaxation applies to edits too (EditReply shares\n// the validator). The body renders inside a \u003cgno-foreign\u003e sandbox.\npackage z_edit_reply_08_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar bid, tid, rid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross(cur), bid, tid, 0, \"body\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Previously rejected on edit (\"forms are not allowed in replies\").\n\tboards2.EditReply(cross(cur), bid, tid, rid, \"\u003cgno-form\u003e\u003cgno-select name=\\\"foo\\\" value=\\\"\\\" /\u003e\u003c/gno-form\u003e\")\n\n\tcontent := boards2.Render(\"test-board/1\")\n\tprintln(strings.Contains(content, \"\u003cgno-foreign\u003e\"))\n}\n\n// Output:\n// true\n"},{"name":"z_edit_thread_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_edit_thread_00_filetest\n\npackage z_edit_thread_00_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\ttitle         = \"Test Thread\"\n\tbody          = \"Test body\"\n\tpath          = \"test-board/1\"\n)\n\nvar (\n\tbid boards.ID\n\tpid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\tpid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.EditThread(cross(cur), bid, pid, title, body)\n\n\tprintln(boards2.Render(path))\n}\n\n// Output:\n// # [Boards](/r/gnoland/boards2/v0) › [test\\-board](/r/gnoland/boards2/v0:test-board)\n// ## Test Thread\n//\n// **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC\n//\n//\n// \u003cgno-foreign\u003e\n// Test body\n// \u003c/gno-foreign\u003e\n//\n//\n//\n// ↳ [Flag](/r/gnoland/boards2/v0:test-board/1/flag) • [Repost](/r/gnoland/boards2/v0:test-board/1/repost) • [Comment](/r/gnoland/boards2/v0:test-board/1/reply) • [Edit](/r/gnoland/boards2/v0:test-board/1/edit) • [Delete](/r/gnoland/boards2/v0$help\u0026func=DeleteThread\u0026boardID=2\u0026threadID=1)\n"},{"name":"z_edit_thread_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_edit_thread_01_filetest\n\npackage z_edit_thread_01_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid boards.ID\n\tpid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\tpid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.EditThread(cross(cur), bid, pid, \"\", \"bar\")\n}\n\n// Error:\n// title is empty\n"},{"name":"z_edit_thread_02_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_edit_thread_02_filetest\n\npackage z_edit_thread_02_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid boards.ID\n\tpid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\tpid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.EditThread(cross(cur), bid, pid, \"Foo\", \"\")\n}\n\n// Error:\n// body is empty\n"},{"name":"z_edit_thread_03_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_edit_thread_03_filetest\n\npackage z_edit_thread_03_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.EditThread(cross(cur), 404, 1, \"Foo\", \"bar\")\n}\n\n// Error:\n// board does not exist with ID: 404\n"},{"name":"z_edit_thread_04_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_edit_thread_04_filetest\n\npackage z_edit_thread_04_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid boards.ID\n\tpid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\tpid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.EditThread(cross(cur), bid, pid, \"Foo\", \"\")\n}\n\n// Error:\n// body is empty\n"},{"name":"z_edit_thread_05_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_edit_thread_05_filetest\n\npackage z_edit_thread_05_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid boards.ID\n\tpid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\tpid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tboards2.EditThread(cross(cur), bid, pid, \"Foo\", \"bar\")\n}\n\n// Error:\n// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission\n"},{"name":"z_edit_thread_06_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_edit_thread_06_filetest\n\npackage z_edit_thread_06_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tadmin address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\ttitle         = \"Test Thread\"\n\tbody          = \"Test body\"\n\tpath          = \"test-board/1\"\n)\n\nvar (\n\tbid boards.ID\n\tpid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\tpid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n\n\t// Invite a member using a role with permission to edit threads\n\tboards2.InviteMember(cross(cur), bid, admin, boards2.RoleAdmin)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\n\tboards2.EditThread(cross(cur), bid, pid, title, body)\n\n\tprintln(boards2.Render(path))\n}\n\n// Output:\n// # [Boards](/r/gnoland/boards2/v0) › [test\\-board](/r/gnoland/boards2/v0:test-board)\n// ## Test Thread\n//\n// **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC\n//\n//\n// \u003cgno-foreign\u003e\n// Test body\n// \u003c/gno-foreign\u003e\n//\n//\n//\n// ↳ [Flag](/r/gnoland/boards2/v0:test-board/1/flag) • [Repost](/r/gnoland/boards2/v0:test-board/1/repost) • [Comment](/r/gnoland/boards2/v0:test-board/1/reply) • [Edit](/r/gnoland/boards2/v0:test-board/1/edit) • [Delete](/r/gnoland/boards2/v0$help\u0026func=DeleteThread\u0026boardID=2\u0026threadID=1)\n"},{"name":"z_edit_thread_07_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_edit_thread_07_filetest\n\npackage z_edit_thread_07_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid   boards.ID\n\tpid   boards.ID\n\ttitle = strings.Repeat(\"X\", boards2.MaxThreadTitleLength+1)\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\tpid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.EditThread(cross(cur), bid, pid, title, \"bar\")\n}\n\n// Error:\n// title is too long, maximum allowed is 100 characters\n"},{"name":"z_flag_reply_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_flag_reply_00_filetest\n\npackage z_flag_reply_00_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid      boards.ID\n\trid, tid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross(cur), bid, tid, 0, \"body\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.FlagReply(cross(cur), bid, tid, rid, \"Reason\")\n\n\t// Render content must contain a message about the hidden reply\n\tprintln(boards2.Render(\"test-board/1/2\"))\n}\n\n// Output:\n// ## Foo\n//\n// **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC\n//\n//\n// \u003cgno-foreign\u003e\n// bar\n// \u003c/gno-foreign\u003e\n//\n//\n//\n// ↳ [Flag](/r/gnoland/boards2/v0:test-board/1/flag) • [Repost](/r/gnoland/boards2/v0:test-board/1/repost) • [Comment](/r/gnoland/boards2/v0:test-board/1/reply) [1] • [Edit](/r/gnoland/boards2/v0:test-board/1/edit) • [Delete](/r/gnoland/boards2/v0$help\u0026func=DeleteThread\u0026boardID=2\u0026threadID=1) • [Show all Replies](/r/gnoland/boards2/v0:test-board/1)\n//\n//\n// \u003e\n// \u003e **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC [\\#2](/r/gnoland/boards2/v0:test-board/1/2)\n// \u003e ⚠ Reply is hidden as it has been flagged as [inappropriate](/r/gnoland/boards2/v0:test-board/1/2/flagging-reasons)\n"},{"name":"z_flag_reply_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_flag_reply_01_filetest\n\npackage z_flag_reply_01_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.FlagReply(cross(cur), 404, 1, 1, \"Reason\")\n}\n\n// Error:\n// board does not exist with ID: 404\n"},{"name":"z_flag_reply_02_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_flag_reply_02_filetest\n\npackage z_flag_reply_02_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.FlagReply(cross(cur), bid, 404, 1, \"Reason\")\n}\n\n// Error:\n// thread does not exist with ID: 404\n"},{"name":"z_flag_reply_03_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_flag_reply_03_filetest\n\npackage z_flag_reply_03_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.FlagReply(cross(cur), bid, tid, 404, \"Reason\")\n}\n\n// Error:\n// reply does not exist with ID: 404\n"},{"name":"z_flag_reply_04_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_flag_reply_04_filetest\n\npackage z_flag_reply_04_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid      boards.ID\n\trid, tid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross(cur), bid, tid, 0, \"body\")\n\tboards2.FlagReply(cross(cur), bid, tid, rid, \"Reason\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.FlagReply(cross(cur), bid, tid, rid, \"Reason\")\n}\n\n// Error:\n// flagging hidden comments or replies is not allowed\n"},{"name":"z_flag_reply_05_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_flag_reply_05_filetest\n\npackage z_flag_reply_05_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid      boards.ID\n\trid, tid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross(cur), bid, tid, 0, \"body\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tboards2.FlagReply(cross(cur), bid, tid, rid, \"Reason\")\n}\n\n// Error:\n// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission\n"},{"name":"z_flag_reply_06_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_flag_reply_06_filetest\n\npackage z_flag_reply_06_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner     address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tmoderator address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid      boards.ID\n\trid, tid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross(cur), bid, tid, 0, \"body\")\n\n\t// Invite a member using a role with permission to flag replies\n\tboards2.InviteMember(cross(cur), bid, moderator, boards2.RoleModerator)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(moderator))\n\n\tboards2.FlagReply(cross(cur), bid, tid, rid, \"Reason\")\n\n\t// Render content must contain a message about the hidden reply\n\tprintln(boards2.Render(\"test-board/1/2\"))\n}\n\n// Output:\n// ## Foo\n//\n// **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC\n//\n//\n// \u003cgno-foreign\u003e\n// bar\n// \u003c/gno-foreign\u003e\n//\n//\n//\n// ↳ [Flag](/r/gnoland/boards2/v0:test-board/1/flag) • [Repost](/r/gnoland/boards2/v0:test-board/1/repost) • [Comment](/r/gnoland/boards2/v0:test-board/1/reply) [1] • [Edit](/r/gnoland/boards2/v0:test-board/1/edit) • [Delete](/r/gnoland/boards2/v0$help\u0026func=DeleteThread\u0026boardID=2\u0026threadID=1) • [Show all Replies](/r/gnoland/boards2/v0:test-board/1)\n//\n//\n// \u003e\n// \u003e **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC [\\#2](/r/gnoland/boards2/v0:test-board/1/2)\n// \u003e ⚠ Reply is hidden as it has been flagged as [inappropriate](/r/gnoland/boards2/v0:test-board/1/2/flagging-reasons)\n"},{"name":"z_flag_reply_07_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_flag_reply_07_filetest\n\npackage z_flag_reply_07_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner     address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tmoderator address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid      boards.ID\n\trid, tid boards.ID\n)\n\nfunc init(cur realm) {\n\t// Created a board with flagging threshold greater than 1\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\tboards2.SetFlaggingThreshold(cross(cur), bid, 2)\n\n\t// Create a reply so the realm owner can flag and hide it with a single flag\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross(cur), bid, tid, 0, \"body\")\n\t// Also freeze board to make sure that realm owner can still flag the reply\n\tboards2.FreezeBoard(cross(cur), bid)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.FlagReply(cross(cur), bid, tid, rid, \"Reason\")\n\n\t// Render content must contain a message about the hidden reply\n\tprintln(boards2.Render(\"test-board/1/2\"))\n}\n\n// Output:\n// ## Foo\n//\n// **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC\n//\n//\n// \u003cgno-foreign\u003e\n// bar\n// \u003c/gno-foreign\u003e\n//\n//\n//\n// ↳ [Flag](/r/gnoland/boards2/v0:test-board/1/flag) • [Repost](/r/gnoland/boards2/v0:test-board/1/repost) • [Show all Replies](/r/gnoland/boards2/v0:test-board/1)\n//\n//\n// \u003e\n// \u003e **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC [\\#2](/r/gnoland/boards2/v0:test-board/1/2)\n// \u003e ⚠ Reply is hidden as it has been flagged as [inappropriate](/r/gnoland/boards2/v0:test-board/1/2/flagging-reasons)\n"},{"name":"z_flag_reply_08_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_flag_reply_08_filetest\n\npackage z_flag_reply_08_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid      boards.ID\n\trid, tid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross(cur), bid, tid, 0, \"body\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.FlagReply(cross(cur), bid, tid, rid, \"\")\n}\n\n// Error:\n// flagging reason is required\n"},{"name":"z_flag_reply_09_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_flag_reply_09_filetest\n\n// A flag reason is user-supplied (only trimmed at write). The flagging-reasons\n// table must escape it so a reason can't inject markdown (links/images) or HTML\n// into the view other moderators see.\npackage z_flag_reply_09_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid      boards.ID\n\trid, tid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross(cur), bid, tid, 0, \"body\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.FlagReply(cross(cur), bid, tid, rid, \"![pwn](https://evil/x.png) [click](https://evil)\")\n\n\tout := boards2.Render(\"test-board/1/2/flagging-reasons\")\n\t// Reason text is shown but escaped — no live markdown image/link syntax.\n\tok := strings.Contains(out, \"pwn\") \u0026\u0026\n\t\t!strings.Contains(out, \"![pwn]\") \u0026\u0026\n\t\t!strings.Contains(out, \"](https://evil)\")\n\tprintln(ok)\n}\n\n// Output:\n// true\n"},{"name":"z_flag_thread_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_flag_thread_00_filetest\n\npackage z_flag_thread_00_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner     address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tmoderator address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid boards.ID\n\tpid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\n\t// Invite a moderator to the new board\n\tboards2.InviteMember(cross(cur), bid, moderator, boards2.RoleModerator)\n\n\t// Create a new thread as a moderator\n\tpid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Flag thread as owner\n\tboards2.FlagThread(cross(cur), bid, pid, \"Reason\")\n\n\t// Ensure thread is not listed\n\tprintln(boards2.Render(\"test-board\"))\n}\n\n// Output:\n// # [Boards](/r/gnoland/boards2/v0) › test-board\n// Created by [g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) on 2009-02-13 11:31pm UTC, #2\n// ↳ [Create Thread](/r/gnoland/boards2/v0:test-board/create-thread) • [Request Invite](/r/gnoland/boards2/v0$help\u0026func=RequestInvite\u0026boardID=2) • [Manage Board](?menu=manageBoard)\n//\n// ---\n// ### This board doesn't have any threads\n// Do you want to [start a new conversation](/r/gnoland/boards2/v0:test-board/create-thread) in this board?\n"},{"name":"z_flag_thread_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_flag_thread_01_filetest\n\npackage z_flag_thread_01_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.FlagThread(cross(cur), 404, 1, \"Reason\")\n}\n\n// Error:\n// board does not exist with ID: 404\n"},{"name":"z_flag_thread_02_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_flag_thread_02_filetest\n\npackage z_flag_thread_02_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid boards.ID\n\tpid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\tpid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n}\n\nfunc main(cur realm) {\n\t// Make the next call as an uninvited user\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tboards2.FlagThread(cross(cur), bid, pid, \"Reason\")\n}\n\n// Error:\n// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission\n"},{"name":"z_flag_thread_03_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_flag_thread_03_filetest\n\npackage z_flag_thread_03_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.FlagThread(cross(cur), bid, 404, \"Reason\")\n}\n\n// Error:\n// thread not found\n"},{"name":"z_flag_thread_04_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_flag_thread_04_filetest\n\npackage z_flag_thread_04_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid boards.ID\n\tpid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\tpid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n\tboards2.FlagThread(cross(cur), bid, pid, \"Reason\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.FlagThread(cross(cur), bid, pid, \"Reason\")\n}\n\n// Error:\n// flagging hidden threads is not allowed\n"},{"name":"z_flag_thread_05_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_flag_thread_05_filetest\n\npackage z_flag_thread_05_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner     address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tmoderator address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid boards.ID\n\tpid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\tpid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n\n\t// Invite a member using a role with permission to flag threads\n\tboards2.InviteMember(cross(cur), bid, moderator, boards2.RoleModerator)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(moderator))\n\n\t// Flag thread as moderator\n\tboards2.FlagThread(cross(cur), bid, pid, \"Reason\")\n\n\t// Ensure that original thread content not visible\n\tprintln(boards2.Render(\"test-board/1\"))\n}\n\n// Output:\n// ⚠ Thread has been flagged as [inappropriate](/r/gnoland/boards2/v0:test-board/1/flagging-reasons)\n"},{"name":"z_flag_thread_06_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_flag_thread_06_filetest\n\npackage z_flag_thread_06_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner     address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tmoderator address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid boards.ID\n\tpid boards.ID\n)\n\nfunc init(cur realm) {\n\t// Created a board with a specific flagging threshold\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\tboards2.SetFlaggingThreshold(cross(cur), bid, 2)\n\n\t// Invite a moderator to the new board\n\tboards2.InviteMember(cross(cur), bid, moderator, boards2.RoleModerator)\n\n\t// Create a new thread and flag it as a moderator\n\ttesting.SetRealm(testing.NewUserRealm(moderator))\n\tpid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n\tboards2.FlagThread(cross(cur), bid, pid, \"Reason\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(moderator))\n\n\tboards2.FlagThread(cross(cur), bid, pid, \"Reason\")\n}\n\n// Error:\n// post has been already flagged by g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\n"},{"name":"z_flag_thread_07_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_flag_thread_07_filetest\n\npackage z_flag_thread_07_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid boards.ID\n\tpid boards.ID\n)\n\nfunc init(cur realm) {\n\t// Created a board with flagging threshold greater than 1\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\tboards2.SetFlaggingThreshold(cross(cur), bid, 2)\n\n\t// Create a thread so the realm owner can flag and hide it with a single flag\n\tpid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n\t// Also freeze board to make sure that realm owner can still flag the thread\n\tboards2.FreezeBoard(cross(cur), bid)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.FlagThread(cross(cur), bid, pid, \"Reason\")\n\n\t// Ensure that original thread content not visible\n\tprintln(boards2.Render(\"test-board/1\"))\n}\n\n// Output:\n// ⚠ Thread has been flagged as [inappropriate](/r/gnoland/boards2/v0:test-board/1/flagging-reasons)\n"},{"name":"z_flag_thread_08_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_flag_thread_08_filetest\n\npackage z_flag_thread_08_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid boards.ID\n\tpid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\tpid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"bar\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.FlagThread(cross(cur), bid, pid, \"\")\n}\n\n// Error:\n// flagging reason is required\n"},{"name":"z_freeze_board_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_freeze_board_00_filetest\n\npackage z_freeze_board_00_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.FreezeBoard(cross(cur), bid)\n\n\tprintln(boards2.IsBoardFrozen(bid))\n}\n\n// Output:\n// true\n"},{"name":"z_freeze_board_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_freeze_board_01_filetest\n\npackage z_freeze_board_01_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\tboards2.FreezeBoard(cross(cur), bid)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.FreezeBoard(cross(cur), bid)\n}\n\n// Error:\n// board is frozen\n"},{"name":"z_freeze_thread_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_freeze_thread_00_filetest\n\npackage z_freeze_thread_00_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"foo\", \"bar\")\n\n\tboards2.FreezeBoard(cross(cur), bid)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Attempt to freeze a thread on frozen board\n\tboards2.FreezeThread(cross(cur), bid, tid)\n}\n\n// Error:\n// board is frozen\n"},{"name":"z_freeze_thread_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_freeze_thread_01_filetest\n\npackage z_freeze_thread_01_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"foo\", \"bar\")\n\n\tboards2.FreezeThread(cross(cur), bid, tid)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Attempt to freeze a frozen thread\n\tboards2.FreezeThread(cross(cur), bid, tid)\n}\n\n// Error:\n// thread is frozen\n"},{"name":"z_freeze_thread_02_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_freeze_thread_02_filetest\n\npackage z_freeze_thread_02_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"foo\", \"bar\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.FreezeThread(cross(cur), bid, tid)\n\n\tprintln(boards2.IsThreadFrozen(bid, tid))\n}\n\n// Output:\n// true\n"},{"name":"z_get_board_id_from_name_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_get_board_id_from_name_00_filetest\n\npackage z_get_board_id_from_name_00_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tname          = \"test123\"\n)\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), name, false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tbid2, found := boards2.GetBoardIDFromName(cross(cur), name)\n\n\tprintln(found)\n\tprintln(bid2 == bid)\n}\n\n// Output:\n// true\n// true\n"},{"name":"z_get_board_id_from_name_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_get_board_id_from_name_01_filetest\n\npackage z_get_board_id_from_name_01_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tbid, found := boards2.GetBoardIDFromName(cross(cur), \"foobar\")\n\n\tprintln(found)\n\tprintln(bid == 0)\n}\n\n// Output:\n// false\n// true\n"},{"name":"z_hub_0_a_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_hub_0_a_filetest\n\n// Test default board values\npackage z_hub_0_a_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nvar boardID boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"))\n\tboardID = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n}\n\nfunc main(cur realm) {\n\tboard, found := boards2.GetBoard(uint64(boardID))\n\tif !found {\n\t\treturn\n\t}\n\n\tprintln(board.ID())\n\tprintln(board.Name())\n\tprintln(board.Aliases())\n\tprintln(board.Readonly())\n\tprintln(board.ThreadCount())\n\tprintln(board.MemberCount())\n\tprintln(board.Creator())\n\tprintln(board.CreatedAt())\n\tprintln(board.UpdatedAt())\n}\n\n// Output:\n// 2\n// test123\n// (nil []string)\n// false\n// 0\n// 1\n// g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\n// 1234567890\n// 0\n"},{"name":"z_hub_0_b_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_hub_0_b_filetest\n\n// Test non default board values\npackage z_hub_0_b_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nvar boardID boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"))\n\tboardID = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\n\t// Invite member\n\tboards2.InviteMember(cross(cur), boardID, \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\", \"admin\")\n\n\t// Rename board\n\tboards2.RenameBoard(cross(cur), \"test123\", \"foo123\")\n\n\t// Create a thread\n\tboards2.CreateThread(cross(cur), boardID, \"Title\", \"Body\")\n\n\t// Freeze board\n\tboards2.FreezeBoard(cross(cur), boardID)\n}\n\nfunc main(cur realm) {\n\tboard, found := boards2.GetBoard(uint64(boardID))\n\tif !found {\n\t\treturn\n\t}\n\n\tprintln(board.ID())\n\tprintln(board.Name())\n\tprintln(board.Aliases())\n\tprintln(board.Readonly())\n\tprintln(board.ThreadCount())\n\tprintln(board.MemberCount())\n\tprintln(board.Creator())\n\tprintln(board.CreatedAt())\n\tprintln(board.UpdatedAt())\n}\n\n// Output:\n// 2\n// foo123\n// slice[(\"test123\" string)]\n// true\n// 1\n// 2\n// g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\n// 1234567890\n// 1234567890\n"},{"name":"z_hub_0_c_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_hub_0_c_filetest\n\n// Test that reads are open to any caller\npackage z_hub_0_c_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar boardID boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboardID = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n}\n\nfunc main(cur realm) {\n\t// The read API performs no caller authorization: an EOA and a realm\n\t// outside the Boards2 namespace must both be able to read.\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\t_, found := boards2.GetBoard(uint64(boardID))\n\tprintln(\"user realm:\", found)\n\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/unrelated/caller\"))\n\t_, found = boards2.GetBoard(uint64(boardID))\n\tprintln(\"unrelated realm:\", found)\n\n\tprintln(\"threads:\", len(boards2.GetThreads(uint64(boardID), 0, 10)))\n\tprintln(\"members:\", len(boards2.GetMembers(uint64(boardID), 0, 10)))\n}\n\n// Output:\n// user realm: true\n// unrelated realm: true\n// threads: 0\n// members: 1\n"},{"name":"z_hub_1_a_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_hub_1_a_filetest\n\n// Test default thread values\npackage z_hub_1_a_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nvar (\n\tboardID  boards.ID\n\tthreadID boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"))\n\tboardID = boards2.CreateBoard(cross(cur), \"origin123\", false, false)\n\tthreadID = boards2.CreateThread(cross(cur), boardID, \"Title\", \"Body\")\n}\n\nfunc main(cur realm) {\n\tthread, found := boards2.GetThread(uint64(boardID), uint64(threadID))\n\tif !found {\n\t\treturn\n\t}\n\n\tprintln(thread.ID())\n\tprintln(thread.OriginalBoardID())\n\tprintln(thread.OriginalThreadID())\n\tprintln(thread.BoardID())\n\tprintln(thread.Title())\n\tprintln(thread.Body())\n\tprintln(thread.Hidden())\n\tprintln(thread.Readonly())\n\tprintln(thread.CommentCount())\n\tprintln(thread.RepostCount())\n\tprintln(thread.FlagCount())\n\tprintln(thread.Creator())\n\tprintln(thread.CreatedAt())\n\tprintln(thread.UpdatedAt())\n}\n\n// Output:\n// 1\n// 0\n// 0\n// 2\n// Title\n// Body\n// false\n// false\n// 0\n// 0\n// 0\n// g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\n// 1234567890\n// 0\n"},{"name":"z_hub_1_b_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_hub_1_b_filetest\n\n// Test non default thread values\npackage z_hub_1_b_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nvar (\n\tboardID  boards.ID\n\tthreadID boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"))\n\tboardID = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\tthreadID = boards2.CreateThread(cross(cur), boardID, \"Title\", \"Body\")\n\n\t// Create another board and repost thread\n\tdstBoardID := boards2.CreateBoard(cross(cur), \"destination123\", false, false)\n\tboards2.CreateRepost(cross(cur), boardID, threadID, dstBoardID, \"Title\", \"Body\")\n\n\t// Edit thread\n\tboards2.EditThread(cross(cur), boardID, threadID, \"Foo\", \"Bar\")\n\n\t// Add a comment to the thread\n\tboards2.CreateReply(cross(cur), boardID, threadID, 0, \"Comment\")\n\n\t// Freeze thread\n\tboards2.FreezeThread(cross(cur), boardID, threadID)\n\n\t// Flag thread\n\tboards2.FlagThread(cross(cur), boardID, threadID, \"Reason\")\n}\n\nfunc main(cur realm) {\n\tthread, found := boards2.GetThread(uint64(boardID), uint64(threadID))\n\tif !found {\n\t\treturn\n\t}\n\n\tprintln(thread.ID())\n\tprintln(thread.OriginalBoardID())  // Only reposts have an original board ID\n\tprintln(thread.OriginalThreadID()) // Only reposts have an original thread ID\n\tprintln(thread.BoardID())\n\tprintln(thread.Title())\n\tprintln(thread.Body())\n\tprintln(thread.Hidden())\n\tprintln(thread.Readonly())\n\tprintln(thread.CommentCount())\n\tprintln(thread.RepostCount())\n\tprintln(thread.FlagCount())\n\tprintln(thread.Creator())\n\tprintln(thread.CreatedAt())\n\tprintln(thread.UpdatedAt())\n}\n\n// Output:\n// 1\n// 0\n// 0\n// 2\n// Foo\n// Bar\n// true\n// true\n// 1\n// 1\n// 1\n// g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\n// 1234567890\n// 1234567890\n"},{"name":"z_hub_1_c_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_hub_1_c_filetest\n\n// Test non default reposted thread values\npackage z_hub_1_c_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nvar (\n\tboardID  boards.ID\n\tthreadID boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"))\n\tsrcBoardID := boards2.CreateBoard(cross(cur), \"origin123\", false, false)\n\n\t// Create two threads where the second is the one to repost\n\tboards2.CreateThread(cross(cur), srcBoardID, \"Title1\", \"Body1\")\n\tsrcThreadID := boards2.CreateThread(cross(cur), srcBoardID, \"Title2\", \"Body2\") // ID = 2\n\n\t// Create repost\n\tboardID = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\tthreadID = boards2.CreateRepost(cross(cur), srcBoardID, srcThreadID, boardID, \"Title\", \"Body\")\n\n\t// Edit repost\n\tboards2.EditThread(cross(cur), boardID, threadID, \"Foo\", \"Bar\")\n\n\t// Add a comment to the repost\n\tboards2.CreateReply(cross(cur), boardID, threadID, 0, \"Comment\")\n\n\t// Freeze repost\n\tboards2.FreezeThread(cross(cur), boardID, threadID)\n\n\t// Flag repost\n\tboards2.FlagThread(cross(cur), boardID, threadID, \"Reason\")\n}\n\nfunc main(cur realm) {\n\tthread, found := boards2.GetThread(uint64(boardID), uint64(threadID))\n\tif !found {\n\t\treturn\n\t}\n\n\tprintln(thread.ID())\n\tprintln(thread.OriginalBoardID())\n\tprintln(thread.OriginalThreadID())\n\tprintln(thread.BoardID())\n\tprintln(thread.Title())\n\tprintln(thread.Body())\n\tprintln(thread.Hidden())\n\tprintln(thread.Readonly())\n\tprintln(thread.CommentCount())\n\tprintln(thread.RepostCount()) // Reposts can't be reposted, so count must be 0\n\tprintln(thread.FlagCount())\n\tprintln(thread.Creator())\n\tprintln(thread.CreatedAt())\n\tprintln(thread.UpdatedAt())\n}\n\n// Output:\n// 1\n// 2\n// 2\n// 3\n// Foo\n// Bar\n// true\n// true\n// 1\n// 0\n// 1\n// g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\n// 1234567890\n// 1234567890\n"},{"name":"z_hub_2_a_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_hub_2_a_filetest\n\n// Test getting boards when no boards exist\npackage z_hub_2_a_filetest\n\nimport (\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nfunc main(cur realm) {\n\tboards := boards2.GetBoards(0, boards2.BoardCount())\n\tprintln(len(boards))\n}\n\n// Output:\n// 1\n"},{"name":"z_hub_2_b_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_hub_2_b_filetest\n\n// Test getting boards\npackage z_hub_2_b_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"))\n\tboards2.CreateBoard(cross(cur), \"aaa123\", false, false)\n\tboards2.CreateBoard(cross(cur), \"bbb123\", false, false)\n\tboards2.CreateBoard(cross(cur), \"ccc123\", false, false)\n}\n\nfunc main(cur realm) {\n\tboards := boards2.GetBoards(0, boards2.BoardCount())\n\n\tfor _, b := range boards {\n\t\tprintln(b.ID(), b.Name())\n\t}\n}\n\n// Output:\n// 1 OpenDiscussions\n// 2 aaa123\n// 3 bbb123\n// 4 ccc123\n"},{"name":"z_hub_3_a_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_hub_3_a_filetest\n\n// Test getting threads from an empty board\npackage z_hub_3_a_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nvar boardID boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"))\n\tboardID = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n}\n\nfunc main(cur realm) {\n\tboards := boards2.GetThreads(uint64(boardID), 0, boards2.BoardCount())\n\tprintln(len(boards))\n}\n\n// Output:\n// 0\n"},{"name":"z_hub_3_b_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_hub_3_b_filetest\n\n// Test getting threads from a board\npackage z_hub_3_b_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nvar board boards2.Board\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"))\n\tboardID := boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\n\t// Create a couple of board threads\n\tboards2.CreateThread(cross(cur), boardID, \"First\", \"Body\")\n\tboards2.CreateThread(cross(cur), boardID, \"Second\", \"Body\")\n\tboards2.CreateThread(cross(cur), boardID, \"Third\", \"Body\")\n\n\t// Get readonly board\n\tboard, _ = boards2.GetBoard(uint64(boardID))\n}\n\nfunc main(cur realm) {\n\tthreads := boards2.GetThreads(board.ID(), 0, board.ThreadCount())\n\n\tfor _, t := range threads {\n\t\tprintln(t.ID(), t.Title())\n\t}\n}\n\n// Output:\n// 1 First\n// 2 Second\n// 3 Third\n"},{"name":"z_hub_4_a_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_hub_4_a_filetest\n\n// Test getting board members\npackage z_hub_4_a_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nvar board boards2.Board\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"))\n\tboardID := boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\n\t// Invite board members\n\tboards2.InviteMember(cross(cur), boardID, \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\", \"owner\")\n\tboards2.InviteMember(cross(cur), boardID, \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\", \"admin\")\n\n\t// Get readonly board\n\tboard, _ = boards2.GetBoard(uint64(boardID))\n}\n\nfunc main(cur realm) {\n\tmembers := boards2.GetMembers(board.ID(), 0, board.MemberCount())\n\n\tfor _, m := range members {\n\t\tprintln(m.Address(), m.Roles())\n\t}\n}\n\n// Output:\n// g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 slice[(\"owner\" string)]\n// g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx slice[(\"owner\" string)]\n// g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj slice[(\"admin\" string)]\n"},{"name":"z_hub_4_b_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_hub_4_b_filetest\n\n// Test getting the realm admin users using a zero board ID\npackage z_hub_4_b_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"))\n\n\t// Invite a realm member, which requires a zero board ID\n\tboards2.InviteMember(cross(cur), 0, \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\", \"admin\")\n}\n\nfunc main(cur realm) {\n\t// A zero board ID must list the realm admin users instead of returning\n\t// nothing, matching how IsMember and the members page resolve it.\n\tmembers := boards2.GetMembers(0, 0, 10)\n\n\tfor _, m := range members {\n\t\tprintln(m.Address(), m.Roles())\n\t}\n}\n\n// Output:\n// g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 slice[(\"admin\" string)]\n// g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx slice[(\"owner\" string)]\n"},{"name":"z_hub_5_a_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_hub_5_a_filetest\n\n// Test getting reposts from a thread without reposts\npackage z_hub_5_a_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nvar thread boards2.Thread\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"))\n\tboardID := boards2.CreateBoard(cross(cur), \"aaa123\", false, false)\n\tthreadID := boards2.CreateThread(cross(cur), boardID, \"Title\", \"Body\")\n\n\t// Get readonly thread\n\tthread, _ = boards2.GetThread(uint64(boardID), uint64(threadID))\n}\n\nfunc main(cur realm) {\n\treposts := boards2.GetReposts(thread.BoardID(), thread.ID(), 0, thread.RepostCount())\n\tprintln(len(reposts))\n}\n\n// Output:\n// 0\n"},{"name":"z_hub_5_b_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_hub_5_b_filetest\n\n// Test getting reposts of a thread\npackage z_hub_5_b_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nvar thread boards2.Thread\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"))\n\tboardID := boards2.CreateBoard(cross(cur), \"aaa123\", false, false)\n\tthreadID := boards2.CreateThread(cross(cur), boardID, \"Title\", \"Body\")\n\n\t// Create first repost\n\tdstBoardID := boards2.CreateBoard(cross(cur), \"bbb123\", false, false)\n\tboards2.CreateRepost(cross(cur), boardID, threadID, dstBoardID, \"First\", \"Body\")\n\n\t// Create second repost\n\tdstBoardID = boards2.CreateBoard(cross(cur), \"ccc123\", false, false)\n\tboards2.CreateRepost(cross(cur), boardID, threadID, dstBoardID, \"Second\", \"Body\")\n\n\t// Get readonly thread\n\tthread, _ = boards2.GetThread(uint64(boardID), uint64(threadID))\n}\n\nfunc main(cur realm) {\n\treposts := boards2.GetReposts(thread.BoardID(), thread.ID(), 0, thread.RepostCount())\n\n\tfor _, t := range reposts {\n\t\tprintln(t.ID(), t.Title())\n\t}\n}\n\n// Output:\n// 1 First\n// 1 Second\n"},{"name":"z_hub_6_a_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_hub_6_a_filetest\n\n// Test getting flags from an unflagged thread\npackage z_hub_6_a_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nvar (\n\tboardID  boards.ID\n\tthreadID boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"))\n\tboardID = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\tthreadID = boards2.CreateThread(cross(cur), boardID, \"Title\", \"Body\")\n}\n\nfunc main(cur realm) {\n\tflags := boards2.GetFlags(uint64(boardID), uint64(threadID), 0, 0, 1)\n\tprintln(len(flags))\n}\n\n// Output:\n// 0\n"},{"name":"z_hub_6_b_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_hub_6_b_filetest\n\n// Test getting flags from an unflagged comment\npackage z_hub_6_b_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nvar (\n\tboardID   boards.ID\n\tthreadID  boards.ID\n\tcommentID boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"))\n\tboardID = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\tthreadID = boards2.CreateThread(cross(cur), boardID, \"Title\", \"Body\")\n\tcommentID = boards2.CreateReply(cross(cur), boardID, threadID, 0, \"Comment\")\n}\n\nfunc main(cur realm) {\n\tflags := boards2.GetFlags(uint64(boardID), uint64(threadID), uint64(commentID), 0, 1)\n\tprintln(len(flags))\n}\n\n// Output:\n// 0\n"},{"name":"z_hub_6_c_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_hub_6_c_filetest\n\n// Test getting flags from a thread\npackage z_hub_6_c_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner     address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tadmin             = \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"\n\tmoderator         = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\"\n)\n\nvar thread boards2.Thread\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboardID := boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\tthreadID := boards2.CreateThread(cross(cur), boardID, \"Title\", \"Body\")\n\n\t// Invite members\n\tboards2.InviteMember(cross(cur), boardID, admin, \"admin\")\n\tboards2.InviteMember(cross(cur), boardID, moderator, \"moderator\")\n\n\t// Update flagging threshold to two flags\n\tboards2.SetFlaggingThreshold(cross(cur), boardID, 2)\n\n\t// Add first flag\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\tboards2.FlagThread(cross(cur), boardID, threadID, \"Reason 1\")\n\n\t// Add second flag\n\ttesting.SetRealm(testing.NewUserRealm(moderator))\n\tboards2.FlagThread(cross(cur), boardID, threadID, \"Reason 2\")\n\n\t// Get readonly thread\n\tthread, _ = boards2.GetThread(uint64(boardID), uint64(threadID))\n}\n\nfunc main(cur realm) {\n\tflags := boards2.GetFlags(thread.BoardID(), thread.ID(), 0, 0, thread.FlagCount())\n\n\tfor _, f := range flags {\n\t\tprintln(f.User(), f.Reason())\n\t}\n}\n\n// Output:\n// g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 Reason 1\n// g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj Reason 2\n"},{"name":"z_hub_6_d_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_hub_6_d_filetest\n\n// Test getting flags from a comment\npackage z_hub_6_d_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner     address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tadmin             = \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"\n\tmoderator         = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\"\n)\n\nvar comment boards2.Comment\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboardID := boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\tthreadID := boards2.CreateThread(cross(cur), boardID, \"Title\", \"Body\")\n\tcommentID := boards2.CreateReply(cross(cur), boardID, threadID, 0, \"Comment\")\n\n\t// Invite member\n\tboards2.InviteMember(cross(cur), boardID, admin, \"admin\")\n\tboards2.InviteMember(cross(cur), boardID, moderator, \"moderator\")\n\n\t// Update flagging threshold to two flags\n\tboards2.SetFlaggingThreshold(cross(cur), boardID, 2)\n\n\t// Add first flag\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\tboards2.FlagReply(cross(cur), boardID, threadID, commentID, \"Reason 1\")\n\n\t// Add second flag\n\ttesting.SetRealm(testing.NewUserRealm(moderator))\n\tboards2.FlagReply(cross(cur), boardID, threadID, commentID, \"Reason 2\")\n\n\t// Get readonly comment\n\tcomment, _ = boards2.GetComment(uint64(boardID), uint64(threadID), uint64(commentID))\n}\n\nfunc main(cur realm) {\n\tflags := boards2.GetFlags(comment.BoardID(), comment.ThreadID(), comment.ID(), 0, comment.FlagCount())\n\n\tfor _, f := range flags {\n\t\tprintln(f.User(), f.Reason())\n\t}\n}\n\n// Output:\n// g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 Reason 1\n// g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj Reason 2\n"},{"name":"z_hub_6_e_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_hub_6_e_filetest\n\n// Test getting flags from a nested reply\npackage z_hub_6_e_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner     address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tadmin             = \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"\n\tmoderator         = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\"\n)\n\nvar (\n\tboardID  boards.ID\n\tthreadID boards.ID\n\treplyID  boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboardID = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\tthreadID = boards2.CreateThread(cross(cur), boardID, \"Title\", \"Body\")\n\tcommentID := boards2.CreateReply(cross(cur), boardID, threadID, 0, \"Comment\")\n\treplyID = boards2.CreateReply(cross(cur), boardID, threadID, commentID, \"Reply\")\n\n\t// Invite members\n\tboards2.InviteMember(cross(cur), boardID, admin, \"admin\")\n\tboards2.InviteMember(cross(cur), boardID, moderator, \"moderator\")\n\n\t// Update flagging threshold to two flags\n\tboards2.SetFlaggingThreshold(cross(cur), boardID, 2)\n\n\t// Flag the nested reply\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\tboards2.FlagReply(cross(cur), boardID, threadID, replyID, \"Reason 1\")\n\n\ttesting.SetRealm(testing.NewUserRealm(moderator))\n\tboards2.FlagReply(cross(cur), boardID, threadID, replyID, \"Reason 2\")\n}\n\nfunc main(cur realm) {\n\tflags := boards2.GetFlags(uint64(boardID), uint64(threadID), uint64(replyID), 0, 10)\n\n\tfor _, f := range flags {\n\t\tprintln(f.User(), f.Reason())\n\t}\n}\n\n// Output:\n// g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 Reason 1\n// g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj Reason 2\n"},{"name":"z_hub_7_a_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_hub_7_a_filetest\n\n// Test getting comments from a thread without comments\npackage z_hub_7_a_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nvar thread boards2.Thread\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"))\n\tboardID := boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\tthreadID := boards2.CreateThread(cross(cur), boardID, \"Title\", \"Body\")\n\n\t// Get readonly thread\n\tthread, _ = boards2.GetThread(uint64(boardID), uint64(threadID))\n}\n\nfunc main(cur realm) {\n\tcomments := boards2.GetComments(thread.BoardID(), thread.ID(), 0, thread.CommentCount())\n\tprintln(len(comments))\n}\n\n// Output:\n// 0\n"},{"name":"z_hub_7_b_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_hub_7_b_filetest\n\n// Test getting comments from a thread\npackage z_hub_7_b_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nvar thread boards2.Thread\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"))\n\tboardID := boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\tthreadID := boards2.CreateThread(cross(cur), boardID, \"Title\", \"Body\")\n\tcommentID := boards2.CreateReply(cross(cur), boardID, threadID, 0, \"Comment 1\")\n\n\t// Create another comment\n\tboards2.CreateReply(cross(cur), boardID, threadID, 0, \"Comment 2\")\n\n\t// Add a reply (it should not be included in the output)\n\tboards2.CreateReply(cross(cur), boardID, threadID, commentID, \"Reply\")\n\n\t// Get readonly thread\n\tthread, _ = boards2.GetThread(uint64(boardID), uint64(threadID))\n}\n\nfunc main(cur realm) {\n\tcomments := boards2.GetComments(thread.BoardID(), thread.ID(), 0, thread.CommentCount())\n\n\tfor _, c := range comments {\n\t\tprintln(c.ID(), c.Body())\n\t}\n}\n\n// Output:\n// 2 Comment 1\n// 3 Comment 2\n"},{"name":"z_hub_8_a_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_hub_8_a_filetest\n\n// Test getting replies from a comment without replies\npackage z_hub_8_a_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nvar comment boards2.Comment\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"))\n\tboardID := boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\tthreadID := boards2.CreateThread(cross(cur), boardID, \"Title\", \"Body\")\n\tcommentID := boards2.CreateReply(cross(cur), boardID, threadID, 0, \"Comment\")\n\n\t// Get readonly comment\n\tcomment, _ = boards2.GetComment(uint64(boardID), uint64(threadID), uint64(commentID))\n}\n\nfunc main(cur realm) {\n\treplies := boards2.GetReplies(comment.BoardID(), comment.ThreadID(), comment.ID(), 0, comment.ReplyCount())\n\tprintln(len(replies))\n}\n\n// Output:\n// 0\n"},{"name":"z_hub_8_b_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_hub_8_b_filetest\n\n// Test getting replies from a comment\npackage z_hub_8_b_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nvar comment boards2.Comment\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"))\n\tboardID := boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\tthreadID := boards2.CreateThread(cross(cur), boardID, \"Title\", \"Body\")\n\tcommentID := boards2.CreateReply(cross(cur), boardID, threadID, 0, \"Comment\")\n\n\t// Create replies\n\tboards2.CreateReply(cross(cur), boardID, threadID, commentID, \"Reply 1\")\n\tsubCommentID := boards2.CreateReply(cross(cur), boardID, threadID, commentID, \"Reply 2\")\n\n\t// Add a sub-reply (it should not be included in the output)\n\tboards2.CreateReply(cross(cur), boardID, threadID, subCommentID, \"Reply 3\")\n\n\t// Get readonly comment\n\tcomment, _ = boards2.GetComment(uint64(boardID), uint64(threadID), uint64(commentID))\n}\n\nfunc main(cur realm) {\n\treplies := boards2.GetReplies(comment.BoardID(), comment.ThreadID(), comment.ID(), 0, comment.ReplyCount())\n\n\tfor _, r := range replies {\n\t\tprintln(r.ID(), r.Body())\n\t}\n}\n\n// Output:\n// 3 Reply 1\n// 4 Reply 2\n"},{"name":"z_hub_8_c_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_hub_8_c_filetest\n\n// Test getting replies from a nested reply\npackage z_hub_8_c_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nvar (\n\tboardID  boards.ID\n\tthreadID boards.ID\n\treplyID  boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"))\n\tboardID = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\tthreadID = boards2.CreateThread(cross(cur), boardID, \"Title\", \"Body\")\n\tcommentID := boards2.CreateReply(cross(cur), boardID, threadID, 0, \"Comment\")\n\treplyID = boards2.CreateReply(cross(cur), boardID, threadID, commentID, \"Reply\")\n\n\t// Create sub-replies of the nested reply\n\tboards2.CreateReply(cross(cur), boardID, threadID, replyID, \"Sub-reply 1\")\n\tboards2.CreateReply(cross(cur), boardID, threadID, replyID, \"Sub-reply 2\")\n}\n\nfunc main(cur realm) {\n\treplies := boards2.GetReplies(uint64(boardID), uint64(threadID), uint64(replyID), 0, 10)\n\n\tfor _, r := range replies {\n\t\tprintln(r.ID(), r.Body())\n\t}\n}\n\n// Output:\n// 4 Sub-reply 1\n// 5 Sub-reply 2\n"},{"name":"z_hub_9_a_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_hub_9_a_filetest\n\n// Test default thread comment values\npackage z_hub_9_a_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nvar (\n\tboardID   boards.ID\n\tthreadID  boards.ID\n\tcommentID boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"))\n\tboardID = boards2.CreateBoard(cross(cur), \"origin123\", false, false)\n\tthreadID = boards2.CreateThread(cross(cur), boardID, \"Title\", \"Body\")\n\tcommentID = boards2.CreateReply(cross(cur), boardID, threadID, 0, \"Comment\")\n}\n\nfunc main(cur realm) {\n\tcomment, found := boards2.GetComment(uint64(boardID), uint64(threadID), uint64(commentID))\n\tif !found {\n\t\treturn\n\t}\n\n\tprintln(comment.ID())\n\tprintln(comment.BoardID())\n\tprintln(comment.ThreadID())\n\tprintln(comment.ParentID())\n\tprintln(comment.Body())\n\tprintln(comment.Hidden())\n\tprintln(comment.ReplyCount())\n\tprintln(comment.FlagCount())\n\tprintln(comment.Creator())\n\tprintln(comment.CreatedAt())\n\tprintln(comment.UpdatedAt())\n}\n\n// Output:\n// 2\n// 2\n// 1\n// 1\n// Comment\n// false\n// 0\n// 0\n// g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\n// 1234567890\n// 0\n"},{"name":"z_hub_9_b_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_hub_9_b_filetest\n\n// Test non default thread comment values\npackage z_hub_9_b_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nvar (\n\tboardID   boards.ID\n\tthreadID  boards.ID\n\tcommentID boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"))\n\tboardID = boards2.CreateBoard(cross(cur), \"origin123\", false, false)\n\tthreadID = boards2.CreateThread(cross(cur), boardID, \"Title\", \"Body\")\n\tcommentID = boards2.CreateReply(cross(cur), boardID, threadID, 0, \"Comment\")\n\n\t// Edit comment\n\tboards2.EditReply(cross(cur), boardID, threadID, commentID, \"Test comment\")\n\n\t// Create a comment reply\n\tboards2.CreateReply(cross(cur), boardID, threadID, commentID, \"Reply\")\n\n\t// Flag comment\n\tboards2.FlagReply(cross(cur), boardID, threadID, commentID, \"Reason\")\n}\n\nfunc main(cur realm) {\n\tcomment, found := boards2.GetComment(uint64(boardID), uint64(threadID), uint64(commentID))\n\tif !found {\n\t\treturn\n\t}\n\n\tprintln(comment.ID())\n\tprintln(comment.BoardID())\n\tprintln(comment.ThreadID())\n\tprintln(comment.ParentID())\n\tprintln(comment.Body())\n\tprintln(comment.Hidden())\n\tprintln(comment.ReplyCount())\n\tprintln(comment.FlagCount())\n\tprintln(comment.Creator())\n\tprintln(comment.CreatedAt())\n\tprintln(comment.UpdatedAt())\n}\n\n// Output:\n// 2\n// 2\n// 1\n// 1\n// Test comment\n// true\n// 1\n// 1\n// g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\n// 1234567890\n// 1234567890\n"},{"name":"z_hub_9_c_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_hub_9_c_filetest\n\n// Test getting a nested reply by ID\npackage z_hub_9_c_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nvar (\n\tboardID  boards.ID\n\tthreadID boards.ID\n\treplyID  boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"))\n\tboardID = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\tthreadID = boards2.CreateThread(cross(cur), boardID, \"Title\", \"Body\")\n\tcommentID := boards2.CreateReply(cross(cur), boardID, threadID, 0, \"Comment\")\n\n\t// Reply to the comment, so the reply is not a top level comment\n\treplyID = boards2.CreateReply(cross(cur), boardID, threadID, commentID, \"Reply\")\n}\n\nfunc main(cur realm) {\n\treply, found := boards2.GetComment(uint64(boardID), uint64(threadID), uint64(replyID))\n\tprintln(found)\n\tprintln(reply.ID())\n\tprintln(reply.ParentID())\n\tprintln(reply.Body())\n}\n\n// Output:\n// true\n// 3\n// 2\n// Reply\n"},{"name":"z_invite_member_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_invite_member_00_filetest\n\npackage z_invite_member_00_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\tbid           = boards.ID(0)                               // Operate on realm DAO instead of individual boards\n\trole          = boards2.RoleOwner\n)\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.InviteMember(cross(cur), bid, user, role)\n\n\t// Check that user is invited\n\tprintln(boards2.HasMemberRole(bid, user, role))\n}\n\n// Output:\n// true\n"},{"name":"z_invite_member_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_invite_member_01_filetest\n\npackage z_invite_member_01_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tadmin address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\tuser  address = \"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\"\n)\n\nvar bid boards.ID // Operate on board DAO\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\n\t// Add an admin member\n\tboards2.InviteMember(cross(cur), bid, admin, boards2.RoleAdmin)\n}\n\nfunc main(cur realm) {\n\t// Next call will be done by the admin member\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\n\tboards2.InviteMember(cross(cur), bid, user, boards2.RoleOwner)\n}\n\n// Error:\n// only owners are allowed to invite other owners\n"},{"name":"z_invite_member_02_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_invite_member_02_filetest\n\npackage z_invite_member_02_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tadmin address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\tuser  address = \"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\"\n\trole          = boards2.RoleAdmin\n)\n\nvar bid boards.ID // Operate on board DAO\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\n\t// Add an admin member\n\tboards2.InviteMember(cross(cur), bid, admin, boards2.RoleAdmin)\n}\n\nfunc main(cur realm) {\n\t// Next call will be done by the admin member\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\n\tboards2.InviteMember(cross(cur), bid, user, role)\n\n\t// Check that user is invited\n\tprintln(boards2.HasMemberRole(bid, user, role))\n}\n\n// Output:\n// true\n"},{"name":"z_invite_member_03_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_invite_member_03_filetest\n\npackage z_invite_member_03_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.InviteMember(cross(cur), 0, user, boards.Role(\"foobar\")) // Operate on realm DAO instead of individual boards\n}\n\n// Error:\n// invalid role: foobar\n"},{"name":"z_invite_member_04_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_invite_member_04_filetest\n\npackage z_invite_member_04_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\trole          = boards2.RoleOwner\n)\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"foo123\", false, false) // Operate on board DAO members\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.InviteMember(cross(cur), bid, user, role)\n\n\t// Check that user is invited\n\tprintln(boards2.HasMemberRole(0, user, role)) // Operate on realm DAO\n\tprintln(boards2.HasMemberRole(bid, user, role))\n}\n\n// Output:\n// false\n// true\n"},{"name":"z_invite_member_05_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_invite_member_05_filetest\n\npackage z_invite_member_05_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\tbid           = boards.ID(0)                               // Operate on realm DAO instead of individual boards\n\trole          = boards2.RoleOwner\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.InviteMember(cross(cur), bid, user, role)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.InviteMember(cross(cur), bid, user, role)\n}\n\n// Error:\n// user is already a member: g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\n"},{"name":"z_invite_member_06_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_invite_member_06_filetest\n\npackage z_invite_member_06_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.InviteMember(cross(cur), 0, \"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\", boards2.RoleGuest)\n}\n\n// Error:\n// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission\n"},{"name":"z_is_banned_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_is_banned_00_filetest\n\npackage z_is_banned_00_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tprintln(boards2.IsBanned(bid, \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"))\n}\n\n// Output:\n// false\n"},{"name":"z_is_banned_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_is_banned_01_filetest\n\npackage z_is_banned_01_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\tboards2.Ban(cross(cur), bid, user, boards2.BanDay, \"Reason\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tprintln(boards2.IsBanned(bid, user))\n}\n\n// Output:\n// true\n"},{"name":"z_is_member_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_is_member_00_filetest\n\npackage z_is_member_00_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner  address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tmember address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\trole           = boards2.RoleGuest\n)\n\nvar bid boards.ID // Operate on board DAO\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\n\tboards2.InviteMember(cross(cur), bid, member, role)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tprintln(boards2.HasMemberRole(bid, member, role))\n\tprintln(boards2.HasMemberRole(bid, member, \"invalid\"))\n\tprintln(boards2.IsMember(bid, member))\n}\n\n// Output:\n// true\n// false\n// true\n"},{"name":"z_is_member_01_filetest.gno","body":"package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\tbid           = boards.ID(0)                               // Operate on realm DAO instead of individual boards\n\trole          = boards2.RoleGuest\n)\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tprintln(boards2.HasMemberRole(bid, user, role))\n\tprintln(boards2.IsMember(bid, user))\n}\n\n// Output:\n// false\n// false\n"},{"name":"z_lock_realm_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_lock_realm_00_filetest\n\npackage z_lock_realm_00_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.LockRealm(cross(cur), false)\n\n\tprintln(boards2.IsRealmLocked())\n\tprintln(boards2.AreRealmMembersLocked())\n}\n\n// Output:\n// true\n// false\n"},{"name":"z_lock_realm_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_lock_realm_01_filetest\n\npackage z_lock_realm_01_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.LockRealm(cross(cur), false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Should fail because realm is already locked\n\tboards2.LockRealm(cross(cur), false)\n}\n\n// Error:\n// realm is locked\n"},{"name":"z_lock_realm_02_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_lock_realm_02_filetest\n\npackage z_lock_realm_02_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\nfunc main(cur realm) {\n\t// Call realm with a user that has not permission to lock the realm\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.LockRealm(cross(cur), false)\n}\n\n// Error:\n// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission\n"},{"name":"z_lock_realm_03_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_lock_realm_03_filetest\n\npackage z_lock_realm_03_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.LockRealm(cross(cur), true)\n\n\tprintln(boards2.IsRealmLocked())\n\tprintln(boards2.AreRealmMembersLocked())\n}\n\n// Output:\n// true\n// true\n"},{"name":"z_lock_realm_04_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_lock_realm_04_filetest\n\npackage z_lock_realm_04_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.LockRealm(cross(cur), true)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Should fail because realm is already locked\n\tboards2.LockRealm(cross(cur), true)\n}\n\n// Error:\n// realm and members are locked\n"},{"name":"z_lock_realm_05_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_lock_realm_05_filetest\n\npackage z_lock_realm_05_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc init(cur realm) {\n\t// Lock the realm without locking realm members\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.LockRealm(cross(cur), false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.LockRealm(cross(cur), true)\n\n\tprintln(boards2.IsRealmLocked())\n\tprintln(boards2.AreRealmMembersLocked())\n}\n\n// Output:\n// true\n// true\n"},{"name":"z_remove_member_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_remove_member_00_filetest\n\npackage z_remove_member_00_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar bid boards.ID // Operate on board DAO\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\n\tboards2.InviteMember(cross(cur), bid, user, boards2.RoleGuest)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.RemoveMember(cross(cur), bid, user)\n\n\t// Check that user is not a member\n\tprintln(boards2.IsMember(bid, user))\n}\n\n// Output:\n// false\n"},{"name":"z_remove_member_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_remove_member_01_filetest\n\npackage z_remove_member_01_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.RemoveMember(cross(cur), 0, \"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\") // Operate on realm DAO instead of individual boards\n}\n\n// Error:\n// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission\n"},{"name":"z_remove_member_02_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_remove_member_02_filetest\n\npackage z_remove_member_02_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.RemoveMember(cross(cur), 0, \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\") // Operate on realm DAO instead of individual boards\n}\n\n// Error:\n// member not found\n"},{"name":"z_remove_member_03_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_remove_member_03_filetest\n\npackage z_remove_member_03_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar bid boards.ID // Operate on board DAO\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\n\tboards2.InviteMember(cross(cur), bid, user, boards2.RoleGuest)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Users must be able to remove themselves without permissions\n\tboards2.RemoveMember(cross(cur), bid, user)\n\n\t// Check that user is not a member\n\tprintln(boards2.IsMember(bid, user))\n}\n\n// Output:\n// false\n"},{"name":"z_rename_board_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_rename_board_00_filetest\n\npackage z_rename_board_00_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner   address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tname            = \"foo123\"\n\tnewName         = \"bar123\"\n)\n\nvar bid boards.ID // Operate on board DAO\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), name, false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.RenameBoard(cross(cur), name, newName)\n\n\t// Ensure board is renamed by the default board owner\n\tbid2, _ := boards2.GetBoardIDFromName(cross(cur), newName)\n\tprintln(\"IDs match =\", bid == bid2)\n}\n\n// Output:\n// IDs match = true\n"},{"name":"z_rename_board_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_rename_board_01_filetest\n\npackage z_rename_board_01_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tname          = \"foo123\"\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.CreateBoard(cross(cur), name, false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.RenameBoard(cross(cur), name, \"\")\n}\n\n// Error:\n// board name is empty\n"},{"name":"z_rename_board_02_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_rename_board_02_filetest\n\npackage z_rename_board_02_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tname          = \"foo123\"\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.CreateBoard(cross(cur), name, false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.RenameBoard(cross(cur), name, name)\n}\n\n// Error:\n// board already exists\n"},{"name":"z_rename_board_03_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_rename_board_03_filetest\n\npackage z_rename_board_03_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.RenameBoard(cross(cur), \"unexisting\", \"foo\")\n}\n\n// Error:\n// board does not exist with name: unexisting\n"},{"name":"z_rename_board_04_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_rename_board_04_filetest\n\npackage z_rename_board_04_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tname          = \"foo123\"\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.CreateBoard(cross(cur), name, false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.RenameBoard(cross(cur), name, \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n}\n\n// Error:\n// addresses are not allowed as board name\n"},{"name":"z_rename_board_05_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_rename_board_05_filetest\n\npackage z_rename_board_05_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner   address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tmember  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\tname            = \"foo123\"\n\tnewName         = \"barbaz123\"\n)\n\nvar bid boards.ID // Operate on board DAO\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tbid = boards2.CreateBoard(cross(cur), name, false, false)\n\tboards2.InviteMember(cross(cur), bid, member, boards2.RoleOwner)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(member))\n\n\tboards2.RenameBoard(cross(cur), name, newName)\n\n\t// Ensure board is renamed by another board owner\n\tbid2, _ := boards2.GetBoardIDFromName(cross(cur), newName)\n\tprintln(\"IDs match =\", bid == bid2)\n}\n\n// Output:\n// IDs match = true\n"},{"name":"z_rename_board_06_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_rename_board_06_filetest\n\npackage z_rename_board_06_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nconst (\n\towner   address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tmember  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\tmember2 address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n\tname            = \"foo123\"\n\tnewName         = \"barbaz123\"\n)\n\nvar bid boards.ID // Operate on board DAO\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), name, false, false)\n\tboards2.InviteMember(cross(cur), bid, member, boards2.RoleOwner)\n\n\t// Test1 is the boards owner and its address has a user already registered\n\t// so a new member must register a user with the new board name.\n\t// uinit.RegisterUser is genesis-only since the security fix.\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), newName, member)\n\ttesting.SetHeight(123)\n\n\t// Invite a new member that doesn't own the user that matches the new board name\n\tboards2.InviteMember(cross(cur), bid, member2, boards2.RoleOwner)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(member2))\n\n\tboards2.RenameBoard(cross(cur), name, newName)\n}\n\n// Error:\n// board name is a user name registered to a different user\n"},{"name":"z_rename_board_07_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_rename_board_07_filetest\n\npackage z_rename_board_07_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tname          = \"foo123\"\n)\n\nvar newName string\n\nfunc init(cur realm) {\n\tnewName = strings.Repeat(\"A\", boards2.MaxBoardNameLength+1)\n\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.CreateBoard(cross(cur), name, false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.RenameBoard(cross(cur), name, newName)\n}\n\n// Error:\n// board name is too long, maximum allowed is 50 characters\n"},{"name":"z_rename_board_08_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_rename_board_08_filetest\n\npackage z_rename_board_08_filetest\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nconst (\n\towner   address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tmember  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\tmember2 address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n\tname            = \"foo123\"\n\tnewName         = \"barbaz123\"\n)\n\nvar bid boards.ID // Operate on board DAO\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), name, false, false)\n\tboards2.InviteMember(cross(cur), bid, member, boards2.RoleOwner)\n\n\t// Test1 is the boards owner and its address has a user already registered\n\t// so a new member must register a user with the new board name.\n\t// uinit.RegisterUser is genesis-only since the security fix.\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), newName, member)\n\ttesting.SetHeight(123)\n\n\t// Invite a new member that doesn't own the user that matches the new board name\n\tboards2.InviteMember(cross(cur), bid, member2, boards2.RoleOwner)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(member2))\n\n\tboards2.RenameBoard(cross(cur), name, newName)\n}\n\n// Error:\n// board name is a user name registered to a different user\n"},{"name":"z_rename_board_09_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_rename_board_09_filetest\n\npackage z_rename_board_09_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\tname          = \"foo123\"\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.CreateBoard(cross(cur), name, false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tboards2.RenameBoard(cross(cur), name, \"barbaz\")\n}\n\n// Error:\n// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission\n"},{"name":"z_request_invite_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_request_invite_00_filetest\n\npackage z_request_invite_00_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"))\n\n\tboards2.RequestInvite(cross(cur), bid)\n\n\tprintln(boards2.Render(\"test123/invites\"))\n}\n\n// Output:\n// # test123 Invite Requests\n// ### These users have requested to be invited to the board\n// | User | Request Date | Actions |\n// | --- | --- | --- |\n// | [g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5](/u/g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5) | 2009-02-13 11:31pm UTC | [accept](/r/gnoland/boards2/v0$help\u0026func=AcceptInvite\u0026boardID=2\u0026user=g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5) • [revoke](/r/gnoland/boards2/v0$help\u0026func=RevokeInvite\u0026boardID=2\u0026user=g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5) |\n"},{"name":"z_request_invite_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_request_invite_01_filetest\n\npackage z_request_invite_01_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/demo/test\"))\n\n\tboards2.RequestInvite(cross(cur), bid)\n}\n\n// Error:\n// caller must be user\n"},{"name":"z_request_invite_02_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_request_invite_02_filetest\n\npackage z_request_invite_02_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.RequestInvite(cross(cur), bid)\n}\n\n// Error:\n// caller is already a member\n"},{"name":"z_request_invite_03_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_request_invite_03_filetest\n\npackage z_request_invite_03_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tboards2.RequestInvite(cross(cur), bid)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tboards2.RequestInvite(cross(cur), bid)\n}\n\n// Error:\n// invite request already exists\n"},{"name":"z_request_invite_04_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_request_invite_04_filetest\n\npackage z_request_invite_04_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tboards2.RequestInvite(cross(cur), 404)\n}\n\n// Error:\n// board does not exist with ID: 404\n"},{"name":"z_revoke_invite_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_revoke_invite_00_filetest\n\npackage z_revoke_invite_00_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tboards2.RequestInvite(cross(cur), bid)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.RevokeInvite(cross(cur), bid, user)\n\n\tprintln(boards2.IsMember(bid, user))\n\tprintln()\n\tprintln(boards2.Render(\"test123/invites\"))\n}\n\n// Output:\n// false\n//\n// # test123 Invite Requests\n// ### Board has no invite requests\n"},{"name":"z_revoke_invite_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_revoke_invite_01_filetest\n\npackage z_revoke_invite_01_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.RevokeInvite(cross(cur), bid, user)\n}\n\n// Error:\n// invite request not found\n"},{"name":"z_revoke_invite_02_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_revoke_invite_02_filetest\n\npackage z_revoke_invite_02_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tboards2.RequestInvite(cross(cur), bid)\n}\n\nfunc main(cur realm) {\n\t// Caller is not a member and has no permission to revoke invites\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tboards2.RevokeInvite(cross(cur), bid, user)\n}\n\n// Error:\n// unauthorized, user g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5 doesn't have the required permission\n"},{"name":"z_set_flagging_threshold_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_set_flagging_threshold_00_filetest\n\npackage z_set_flagging_threshold_00_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.SetFlaggingThreshold(cross(cur), bid, 4)\n\n\t// Ensure that flagging threshold changed\n\tprintln(boards2.GetFlaggingThreshold(bid))\n}\n\n// Output:\n// 4\n"},{"name":"z_set_flagging_threshold_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_set_flagging_threshold_01_filetest\n\npackage z_set_flagging_threshold_01_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address   = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tbid   boards.ID = 404\n)\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.SetFlaggingThreshold(cross(cur), bid, 1)\n}\n\n// Error:\n// board does not exist with ID: 404\n"},{"name":"z_set_flagging_threshold_02_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_set_flagging_threshold_02_filetest\n\npackage z_set_flagging_threshold_02_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.SetFlaggingThreshold(cross(cur), 1, 0)\n}\n\n// Error:\n// invalid flagging threshold\n"},{"name":"z_set_permissions_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_set_permissions_00_filetest\n\npackage z_set_permissions_00_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/exts/permissions/v0\"\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tbid           = boards.ID(0) // Operate on realm instead of individual boards\n)\n\nvar perms boards.Permissions\n\nfunc init() {\n\t// Create a new permissions instance without users\n\tperms = permissions.New()\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.SetPermissions(cross(cur), bid, perms)\n\n\t// Owner that setted new permissions is not a member of the new permissions\n\tprintln(boards2.IsMember(bid, owner))\n}\n\n// Output:\n// false\n"},{"name":"z_set_permissions_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_set_permissions_01_filetest\n\npackage z_set_permissions_01_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/exts/permissions/v0\"\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\tuser address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\tbid          = boards.ID(0)                               // Operate on realm instead of individual boards\n)\n\nvar perms boards.Permissions\n\nfunc init() {\n\t// Create a new permissions instance\n\tperms = permissions.New()\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tboards2.SetPermissions(cross(cur), bid, perms)\n}\n\n// Error:\n// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission\n"},{"name":"z_set_permissions_02_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_set_permissions_02_filetest\n\npackage z_set_permissions_02_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/exts/permissions/v0\"\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tperms boards.Permissions\n\tbid   boards.ID\n)\n\nfunc init(cur realm) {\n\t// Create a new permissions instance without users\n\tperms = permissions.New()\n\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"foobar\", false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.SetPermissions(cross(cur), bid, perms)\n\n\t// Owner that setted new board permissions is not a member of the new permissions\n\tprintln(boards2.IsMember(bid, owner))\n}\n\n// Output:\n// false\n"},{"name":"z_set_realm_notice_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_set_realm_notice_00_filetest\n\npackage z_set_realm_notice_00_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.SetRealmNotice(cross(cur), \"This is a test realm message\")\n\n\tprintln(boards2.Notice)\n}\n\n// Output:\n// This is a test realm message\n"},{"name":"z_set_realm_notice_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_set_realm_notice_01_filetest\n\npackage z_set_realm_notice_01_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc init(cur realm) {\n\t// Set an initial message so it can be cleared\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.SetRealmNotice(cross(cur), \"This is a test realm message\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.SetRealmNotice(cross(cur), \"\")\n\n\tprintln(boards2.Notice == \"\")\n}\n\n// Output:\n// true\n"},{"name":"z_set_realm_notice_02_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_set_realm_notice_02_filetest\n\npackage z_set_realm_notice_02_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\nfunc main(cur realm) {\n\t// Call realm with a user that has not permission to set realm notice\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.SetRealmNotice(cross(cur), \"Foo\")\n}\n\n// Error:\n// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission\n"},{"name":"z_set_realm_notice_03_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_set_realm_notice_03_filetest\n\npackage z_set_realm_notice_03_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.SetRealmNotice(cross(cur), \"This is a test realm message\")\n\n\tprintln(boards2.Render(\"\"))\n}\n\n// Output:\n//\n// \u003e \\[!INFO\\] Notice\n// \u003e This is a test realm message\n//\n// # Boards\n// [Create Board](/r/gnoland/boards2/v0:create-board) • [List Admin Users](/r/gnoland/boards2/v0:admin-users) • [Help](/r/gnoland/boards2/v0:help)\n//\n// ---\n// Sort by: [newest first](/r/gnoland/boards2/v0:?order=desc)\n//\n// ###### [OpenDiscussions](/r/gnoland/boards2/v0:OpenDiscussions)\n// Created by [g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) on 2009-02-13 11:31pm UTC, #1\n// **0 threads**\n"},{"name":"z_set_required_account_amount_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_set_required_account_amount_00_filetest\n\npackage z_set_required_account_amount_00_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.SetRequiredAccountAmount(cross(cur), 1_000_000)\n\n\tprintln(boards2.RequiredAccountAmount)\n}\n\n// Output:\n// 1000000\n"},{"name":"z_set_required_account_amount_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_set_required_account_amount_01_filetest\n\npackage z_set_required_account_amount_01_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.SetRequiredAccountAmount(cross(cur), 0) // Disable\n\n\tprintln(boards2.RequiredAccountAmount)\n}\n\n// Output:\n// 0\n"},{"name":"z_set_required_account_amount_02_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_set_required_account_amount_02_filetest\n\npackage z_set_required_account_amount_02_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.SetRequiredAccountAmount(cross(cur), 1_000_000)\n}\n\n// Error:\n// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission\n"},{"name":"z_ui_admin_users_00_filetest.gno","body":"// Render realm admin users view.\npackage main\n\nimport (\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nfunc main() {\n\tprintln(boards2.Render(\"admin-users\"))\n}\n\n// Output:\n// # Admin Users\n// ### These are the admin users of the realm\n// | Member | Role | Actions |\n// | --- | --- | --- |\n// | [g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) | owner | [remove](/r/gnoland/boards2/v0$help\u0026func=RemoveMember\u0026boardID=0\u0026member=g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) • [change role](/r/gnoland/boards2/v0$help\u0026func=ChangeMemberRole\u0026boardID=0\u0026member=g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\u0026role=) |\n"},{"name":"z_ui_board_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ui_board_00_filetest\n\n// Render default board view.\npackage z_ui_board_00_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner     address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tboardName         = \"TestBoard\"\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a board and then add 3 threads\n\tboardID := boards2.CreateBoard(cross(cur), boardName, false, false)\n\n\t// Create thread \"A\" with a single comment\n\tthreadID := boards2.CreateThread(cross(cur), boardID, \"A\", \"Body\")\n\tboards2.CreateReply(cross(cur), boardID, threadID, 0, \"Body\")\n\n\t// The other 2 threads are created without comments\n\tboards2.CreateThread(cross(cur), boardID, \"B\", \"Body\")\n\tthreadID = boards2.CreateThread(cross(cur), boardID, \"C\", \"Body\")\n\n\t// Repost thread \"C\" into a different board\n\tdstBoardID := boards2.CreateBoard(cross(cur), \"Bar\", false, false)\n\tboards2.CreateRepost(cross(cur), boardID, threadID, dstBoardID, \"Title\", \"Body\")\n}\n\nfunc main() {\n\tprintln(boards2.Render(boardName))\n}\n\n// Output:\n// # [Boards](/r/gnoland/boards2/v0) › TestBoard\n// Created by [g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) on 2009-02-13 11:31pm UTC, #2\n// ↳ [Create Thread](/r/gnoland/boards2/v0:TestBoard/create-thread) • [Request Invite](/r/gnoland/boards2/v0$help\u0026func=RequestInvite\u0026boardID=2) • [Manage Board](?menu=manageBoard)\n//\n// ---\n// Sort by: [newest first](/r/gnoland/boards2/v0:TestBoard?order=desc)\n//\n// ###### [A](/r/gnoland/boards2/v0:TestBoard/1)\n// Created by [g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) `owner` on 2009-02-13 11:31pm UTC\n// **1 replies • 0 reposts**\n//\n// ###### [B](/r/gnoland/boards2/v0:TestBoard/3)\n// Created by [g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) `owner` on 2009-02-13 11:31pm UTC\n// **0 replies • 0 reposts**\n//\n// ###### [C](/r/gnoland/boards2/v0:TestBoard/4)\n// Created by [g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) `owner` on 2009-02-13 11:31pm UTC\n// **0 replies • 1 reposts**\n"},{"name":"z_ui_board_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ui_board_01_filetest\n\n// Render board sorting threads from newest to oldest.\npackage z_ui_board_01_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner     address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tboardName         = \"TestBoard\"\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a board and then add 3 threads\n\tboardID := boards2.CreateBoard(cross(cur), boardName, false, false)\n\n\tboards2.CreateThread(cross(cur), boardID, \"A\", \"Body\")\n\tboards2.CreateThread(cross(cur), boardID, \"B\", \"Body\")\n\tboards2.CreateThread(cross(cur), boardID, \"C\", \"Body\")\n}\n\nfunc main() {\n\tpath := boardName + \"?order=desc\"\n\tprintln(boards2.Render(path))\n}\n\n// Output:\n// # [Boards](/r/gnoland/boards2/v0) › TestBoard\n// Created by [g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) on 2009-02-13 11:31pm UTC, #2\n// ↳ [Create Thread](/r/gnoland/boards2/v0:TestBoard/create-thread) • [Request Invite](/r/gnoland/boards2/v0$help\u0026func=RequestInvite\u0026boardID=2) • [Manage Board](?menu=manageBoard)\n//\n// ---\n// Sort by: [oldest first](/r/gnoland/boards2/v0:TestBoard?order=asc)\n//\n// ###### [C](/r/gnoland/boards2/v0:TestBoard/3)\n// Created by [g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) `owner` on 2009-02-13 11:31pm UTC\n// **0 replies • 0 reposts**\n//\n// ###### [B](/r/gnoland/boards2/v0:TestBoard/2)\n// Created by [g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) `owner` on 2009-02-13 11:31pm UTC\n// **0 replies • 0 reposts**\n//\n// ###### [A](/r/gnoland/boards2/v0:TestBoard/1)\n// Created by [g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) `owner` on 2009-02-13 11:31pm UTC\n// **0 replies • 0 reposts**\n"},{"name":"z_ui_board_02_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ui_board_02_filetest\n\n// Render board view with the manage board menu expanded.\npackage z_ui_board_02_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner     address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tboardName         = \"TestBoard\"\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a board and a thread\n\tboardID := boards2.CreateBoard(cross(cur), boardName, false, false)\n\tboards2.CreateThread(cross(cur), boardID, \"A\", \"Body\")\n}\n\nfunc main() {\n\tpath := boardName + \"?menu=manageBoard\"\n\tprintln(boards2.Render(path))\n}\n\n// Output:\n// # [Boards](/r/gnoland/boards2/v0) › TestBoard\n// Created by [g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) on 2009-02-13 11:31pm UTC, #2\n// ↳ [Create Thread](/r/gnoland/boards2/v0:TestBoard/create-thread) • [Request Invite](/r/gnoland/boards2/v0$help\u0026func=RequestInvite\u0026boardID=2) • **Manage Board**\n// └─ [Invite Member](/r/gnoland/boards2/v0:TestBoard/invite-member) • [List Invite Requests](/r/gnoland/boards2/v0:TestBoard/invites) • [List Members](/r/gnoland/boards2/v0:TestBoard/members) • [List Banned Users](/r/gnoland/boards2/v0:TestBoard/banned-users) • [Freeze Board](/r/gnoland/boards2/v0$help\u0026func=FreezeBoard\u0026boardID=2)\n//\n// ---\n// Sort by: [newest first](/r/gnoland/boards2/v0:TestBoard?menu=manageBoard\u0026order=desc)\n//\n// ###### [A](/r/gnoland/boards2/v0:TestBoard/1)\n// Created by [g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) `owner` on 2009-02-13 11:31pm UTC\n// **0 replies • 0 reposts**\n"},{"name":"z_ui_board_03_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ui_board_03_filetest\n\n// Render readonly board.\npackage z_ui_board_03_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner     address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tboardName         = \"TestBoard\"\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a readonly board and then add a thread\n\tboardID := boards2.CreateBoard(cross(cur), boardName, false, false)\n\tboards2.CreateThread(cross(cur), boardID, \"A\", \"Body\")\n\tboards2.FreezeBoard(cross(cur), boardID)\n}\n\nfunc main() {\n\tprintln(boards2.Render(boardName))\n}\n\n// Output:\n// # [Boards](/r/gnoland/boards2/v0) › TestBoard\n// \u003e [!WARNING] Info\n// \u003e Creating new threads and commenting are disabled within this board\n//\n// Created by [g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) on 2009-02-13 11:31pm UTC, #2\n// [List Members](/r/gnoland/boards2/v0:TestBoard/members) • [Unfreeze Board](/r/gnoland/boards2/v0$help\u0026func=UnfreezeBoard\u0026boardID=2\u0026replyID=\u0026threadID=)\n//\n// ---\n// Sort by: [newest first](/r/gnoland/boards2/v0:TestBoard?order=desc)\n//\n// ###### [A](/r/gnoland/boards2/v0:TestBoard/1)\n// Created by [g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) `owner` on 2009-02-13 11:31pm UTC\n// **0 replies • 0 reposts**\n"},{"name":"z_ui_board_04_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ui_board_04_filetest\n\n// Render default board view when there are no threads.\npackage z_ui_board_04_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner     address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tboardName         = \"TestBoard\"\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateBoard(cross(cur), boardName, false, false)\n}\n\nfunc main() {\n\tprintln(boards2.Render(boardName))\n}\n\n// Output:\n// # [Boards](/r/gnoland/boards2/v0) › TestBoard\n// Created by [g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) on 2009-02-13 11:31pm UTC, #2\n// ↳ [Create Thread](/r/gnoland/boards2/v0:TestBoard/create-thread) • [Request Invite](/r/gnoland/boards2/v0$help\u0026func=RequestInvite\u0026boardID=2) • [Manage Board](?menu=manageBoard)\n//\n// ---\n// ### This board doesn't have any threads\n// Do you want to [start a new conversation](/r/gnoland/boards2/v0:TestBoard/create-thread) in this board?\n"},{"name":"z_ui_board_05_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ui_board_05_filetest\n\n// An invalid ?page= (out of range, zero, negative, or non-numeric) clamps to\n// the last valid page instead of aborting the render or echoing a broken\n// \"page N of M\" picker (see newClampedPager). Notably a NEGATIVE page is not\n// rejected by pager.New, so it must be caught by the clamp.\npackage z_ui_board_05_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner     address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tboardName         = \"test-board\"\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboardID := boards2.CreateBoard(cross(cur), boardName, false, false)\n\t// pageSizeDefault is 6, so 7 threads span 2 pages; the last page is 2.\n\tfor i := 0; i \u003c 7; i++ {\n\t\tboards2.CreateThread(cross(cur), boardID, \"T\", \"Body\")\n\t}\n}\n\nfunc main() {\n\t// Each value is invalid: too high, zero, negative, non-numeric. All must\n\t// clamp to the last page (\"page 2 of 2\"); if any slipped through, the\n\t// picker would echo the bad number (e.g. \"page -5 of 2\") and this fails.\n\tok := true\n\tfor _, q := range []string{\"?page=99\", \"?page=0\", \"?page=-5\", \"?page=abc\"} {\n\t\tif !strings.Contains(boards2.Render(boardName+q), \"page 2 of 2\") {\n\t\t\tok = false\n\t\t}\n\t}\n\tprintln(ok)\n}\n\n// Output:\n// true\n"},{"name":"z_ui_board_members_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ui_board_members_00_filetest\n\n// Render board members view.\npackage z_ui_board_members_00_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner     address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tboardName         = \"BoardName\"\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateBoard(cross(cur), boardName, false, false)\n}\n\nfunc main() {\n\tpath := boardName + \"/members\"\n\tprintln(boards2.Render(path))\n}\n\n// Output:\n// # BoardName Members\n// ### These are the board members\n// | Member | Role | Actions |\n// | --- | --- | --- |\n// | [g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) | owner | [remove](/r/gnoland/boards2/v0$help\u0026func=RemoveMember\u0026boardID=2\u0026member=g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) • [change role](/r/gnoland/boards2/v0$help\u0026func=ChangeMemberRole\u0026boardID=2\u0026member=g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\u0026role=) |\n"},{"name":"z_ui_home_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ui_home_00_filetest\n\n// Render default realm view.\n// Default realm view must render the list of listed boards.\npackage z_ui_home_00_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create board \"AAA\" with a single thread\n\tboardID := boards2.CreateBoard(cross(cur), \"AAA\", true, false)\n\tboards2.CreateThread(cross(cur), boardID, \"Foo\", \"Bar\")\n\n\t// Create 2 more boards\n\tboards2.CreateBoard(cross(cur), \"BBB\", true, false)\n\tboards2.CreateBoard(cross(cur), \"CCC\", true, false)\n\tboards2.CreateBoard(cross(cur), \"DDD\", false, false) // \u003c-- Unlisted board\n}\n\nfunc main() {\n\tprintln(boards2.Render(\"\"))\n}\n\n// Output:\n// # Boards\n// [Create Board](/r/gnoland/boards2/v0:create-board) • [List Admin Users](/r/gnoland/boards2/v0:admin-users) • [Help](/r/gnoland/boards2/v0:help)\n//\n// ---\n// Sort by: [newest first](/r/gnoland/boards2/v0:?order=desc)\n//\n// ###### [OpenDiscussions](/r/gnoland/boards2/v0:OpenDiscussions)\n// Created by [g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) on 2009-02-13 11:31pm UTC, #1\n// **0 threads**\n//\n// ###### [AAA](/r/gnoland/boards2/v0:AAA)\n// Created by [g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) on 2009-02-13 11:31pm UTC, #2\n// **1 threads**\n//\n// ###### [BBB](/r/gnoland/boards2/v0:BBB)\n// Created by [g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) on 2009-02-13 11:31pm UTC, #3\n// **0 threads**\n//\n// ###### [CCC](/r/gnoland/boards2/v0:CCC)\n// Created by [g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) on 2009-02-13 11:31pm UTC, #4\n// **0 threads**\n"},{"name":"z_ui_home_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ui_home_01_filetest\n\n// Render boards sorted from newest to oldest.\npackage z_ui_home_01_filetest\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateBoard(cross(cur), \"AAA\", true, false)\n\tboards2.CreateBoard(cross(cur), \"BBB\", true, false)\n\tboards2.CreateBoard(cross(cur), \"CCC\", true, false)\n}\n\nfunc main() {\n\tprintln(boards2.Render(\"?order=desc\"))\n}\n\n// Output:\n// # Boards\n// [Create Board](/r/gnoland/boards2/v0:create-board) • [List Admin Users](/r/gnoland/boards2/v0:admin-users) • [Help](/r/gnoland/boards2/v0:help)\n//\n// ---\n// Sort by: [oldest first](/r/gnoland/boards2/v0:?order=asc)\n//\n// ###### [CCC](/r/gnoland/boards2/v0:CCC)\n// Created by [g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) on 2009-02-13 11:31pm UTC, #4\n// **0 threads**\n//\n// ###### [BBB](/r/gnoland/boards2/v0:BBB)\n// Created by [g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) on 2009-02-13 11:31pm UTC, #3\n// **0 threads**\n//\n// ###### [AAA](/r/gnoland/boards2/v0:AAA)\n// Created by [g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) on 2009-02-13 11:31pm UTC, #2\n// **0 threads**\n//\n// ###### [OpenDiscussions](/r/gnoland/boards2/v0:OpenDiscussions)\n// Created by [g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) on 2009-02-13 11:31pm UTC, #1\n// **0 threads**\n"},{"name":"z_ui_home_02_filetest.gno","body":"// Render default realm view when there are no boards.\npackage main\n\nimport (\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nfunc main() {\n\tprintln(boards2.Render(\"\"))\n}\n\n// Output:\n// # Boards\n// [Create Board](/r/gnoland/boards2/v0:create-board) • [List Admin Users](/r/gnoland/boards2/v0:admin-users) • [Help](/r/gnoland/boards2/v0:help)\n//\n// ---\n// Sort by: [newest first](/r/gnoland/boards2/v0:?order=desc)\n//\n// ###### [OpenDiscussions](/r/gnoland/boards2/v0:OpenDiscussions)\n// Created by [g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) on 2009-02-13 11:31pm UTC, #1\n// **0 threads**\n"},{"name":"z_ui_reply_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ui_reply_00_filetest\n\n// Render comment/reply view.\npackage z_ui_reply_00_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner     address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tboardName         = \"test-board\"\n)\n\nvar (\n\tthreadID boards.ID\n\treplyID  boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a board and a thread\n\tboardID := boards2.CreateBoard(cross(cur), boardName, false, false)\n\tthreadID = boards2.CreateThread(cross(cur), boardID, \"Foo\", \"Body\")\n\n\t// Create two comments and a reply to a comment\n\tboards2.CreateReply(cross(cur), boardID, threadID, 0, \"First comment\")\n\n\treplyID = boards2.CreateReply(cross(cur), boardID, threadID, 0, \"Second comment\")\n\tboards2.CreateReply(cross(cur), boardID, threadID, replyID, \"Third comment\")\n}\n\nfunc main() {\n\tpath := boardName + \"/\" + threadID.String() + \"/\" + replyID.String()\n\tprintln(boards2.Render(path))\n}\n\n// Output:\n// ## Foo\n//\n// **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC\n//\n//\n// \u003cgno-foreign\u003e\n// Body\n// \u003c/gno-foreign\u003e\n//\n//\n//\n// ↳ [Flag](/r/gnoland/boards2/v0:test-board/1/flag) • [Repost](/r/gnoland/boards2/v0:test-board/1/repost) • [Comment](/r/gnoland/boards2/v0:test-board/1/reply) [2] • [Edit](/r/gnoland/boards2/v0:test-board/1/edit) • [Delete](/r/gnoland/boards2/v0$help\u0026func=DeleteThread\u0026boardID=2\u0026threadID=1) • [Show all Replies](/r/gnoland/boards2/v0:test-board/1)\n//\n//\n// \u003e\n// \u003e **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC [\\#3](/r/gnoland/boards2/v0:test-board/1/3)\n// \u003e\n// \u003e\n// \u003e \u003cgno-foreign\u003e\n// \u003e Second comment\n// \u003e \u003c/gno-foreign\u003e\n// \u003e\n// \u003e\n// \u003e\n// \u003e ↳ [Flag](/r/gnoland/boards2/v0:test-board/1/3/flag) • [Reply](/r/gnoland/boards2/v0:test-board/1/3/reply) [1] • [Edit](/r/gnoland/boards2/v0:test-board/1/3/edit) • [Delete](/r/gnoland/boards2/v0$help\u0026func=DeleteReply\u0026boardID=2\u0026replyID=3\u0026threadID=1)\n//\n// ---\n// Sort by: [newest first](/r/gnoland/boards2/v0:test-board/1/3?order=desc)\n// \u003e\n// \u003e \u003e\n// \u003e \u003e **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC [\\#4](/r/gnoland/boards2/v0:test-board/1/4)\n// \u003e \u003e\n// \u003e \u003e\n// \u003e \u003e \u003cgno-foreign\u003e\n// \u003e \u003e Third comment\n// \u003e \u003e \u003c/gno-foreign\u003e\n// \u003e \u003e\n// \u003e \u003e\n// \u003e \u003e\n// \u003e \u003e ↳ [Flag](/r/gnoland/boards2/v0:test-board/1/4/flag) • [Reply](/r/gnoland/boards2/v0:test-board/1/4/reply) • [Edit](/r/gnoland/boards2/v0:test-board/1/4/edit) • [Delete](/r/gnoland/boards2/v0$help\u0026func=DeleteReply\u0026boardID=2\u0026replyID=4\u0026threadID=1)\n"},{"name":"z_ui_reply_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ui_reply_01_filetest\n\n// Render comment/reply view of a deleted comment.\npackage z_ui_reply_01_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner     address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tboardName         = \"test-board\"\n)\n\nvar (\n\tthreadID boards.ID\n\treplyID  boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a board and a thread\n\tboardID := boards2.CreateBoard(cross(cur), boardName, false, false)\n\tthreadID = boards2.CreateThread(cross(cur), boardID, \"Foo\", \"Body\")\n\n\t// Create a comments with a reply\n\treplyID = boards2.CreateReply(cross(cur), boardID, threadID, 0, \"Second comment\")\n\tboards2.CreateReply(cross(cur), boardID, threadID, replyID, \"Third comment\")\n\n\t// Delete the comment\n\tboards2.DeleteReply(cross(cur), boardID, threadID, replyID)\n}\n\nfunc main() {\n\tpath := boardName + \"/\" + threadID.String() + \"/\" + replyID.String()\n\tprintln(boards2.Render(path))\n}\n\n// Output:\n// ## Foo\n//\n// **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC\n//\n//\n// \u003cgno-foreign\u003e\n// Body\n// \u003c/gno-foreign\u003e\n//\n//\n//\n// ↳ [Flag](/r/gnoland/boards2/v0:test-board/1/flag) • [Repost](/r/gnoland/boards2/v0:test-board/1/repost) • [Comment](/r/gnoland/boards2/v0:test-board/1/reply) [1] • [Edit](/r/gnoland/boards2/v0:test-board/1/edit) • [Delete](/r/gnoland/boards2/v0$help\u0026func=DeleteThread\u0026boardID=2\u0026threadID=1) • [Show all Replies](/r/gnoland/boards2/v0:test-board/1)\n//\n//\n// \u003e\n// \u003e **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC [\\#2](/r/gnoland/boards2/v0:test-board/1/2)\n// \u003e\n// \u003e\n// \u003e \u003cgno-foreign\u003e\n// \u003e ⚠ This comment has been deleted\n// \u003e \u003c/gno-foreign\u003e\n// \u003e\n// \u003e\n// \u003e\n// \u003e ↳ [Flag](/r/gnoland/boards2/v0:test-board/1/2/flag) • [Reply](/r/gnoland/boards2/v0:test-board/1/2/reply) [1] • [Edit](/r/gnoland/boards2/v0:test-board/1/2/edit) • [Delete](/r/gnoland/boards2/v0$help\u0026func=DeleteReply\u0026boardID=2\u0026replyID=2\u0026threadID=1)\n//\n// ---\n// Sort by: [newest first](/r/gnoland/boards2/v0:test-board/1/2?order=desc)\n// \u003e\n// \u003e \u003e\n// \u003e \u003e **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC [\\#3](/r/gnoland/boards2/v0:test-board/1/3)\n// \u003e \u003e\n// \u003e \u003e\n// \u003e \u003e \u003cgno-foreign\u003e\n// \u003e \u003e Third comment\n// \u003e \u003e \u003c/gno-foreign\u003e\n// \u003e \u003e\n// \u003e \u003e\n// \u003e \u003e\n// \u003e \u003e ↳ [Flag](/r/gnoland/boards2/v0:test-board/1/3/flag) • [Reply](/r/gnoland/boards2/v0:test-board/1/3/reply) • [Edit](/r/gnoland/boards2/v0:test-board/1/3/edit) • [Delete](/r/gnoland/boards2/v0$help\u0026func=DeleteReply\u0026boardID=2\u0026replyID=3\u0026threadID=1)\n"},{"name":"z_ui_reply_02_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ui_reply_02_filetest\n\n// Render comment/reply view of a flagged comment.\npackage z_ui_reply_02_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner     address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tboardName         = \"test-board\"\n)\n\nvar (\n\tthreadID boards.ID\n\treplyID  boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a board and a thread\n\tboardID := boards2.CreateBoard(cross(cur), boardName, false, false)\n\tthreadID = boards2.CreateThread(cross(cur), boardID, \"Foo\", \"Body\")\n\n\t// Create a comments with a reply\n\treplyID = boards2.CreateReply(cross(cur), boardID, threadID, 0, \"Second comment\")\n\tboards2.CreateReply(cross(cur), boardID, threadID, replyID, \"Third comment\")\n\n\t// Flag the comment\n\tboards2.FlagReply(cross(cur), boardID, threadID, replyID, \"Reason\")\n}\n\nfunc main() {\n\tpath := boardName + \"/\" + threadID.String() + \"/\" + replyID.String()\n\tprintln(boards2.Render(path))\n}\n\n// Output:\n// ## Foo\n//\n// **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC\n//\n//\n// \u003cgno-foreign\u003e\n// Body\n// \u003c/gno-foreign\u003e\n//\n//\n//\n// ↳ [Flag](/r/gnoland/boards2/v0:test-board/1/flag) • [Repost](/r/gnoland/boards2/v0:test-board/1/repost) • [Comment](/r/gnoland/boards2/v0:test-board/1/reply) [1] • [Edit](/r/gnoland/boards2/v0:test-board/1/edit) • [Delete](/r/gnoland/boards2/v0$help\u0026func=DeleteThread\u0026boardID=2\u0026threadID=1) • [Show all Replies](/r/gnoland/boards2/v0:test-board/1)\n//\n//\n// \u003e\n// \u003e **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC [\\#2](/r/gnoland/boards2/v0:test-board/1/2)\n// \u003e ⚠ Reply is hidden as it has been flagged as [inappropriate](/r/gnoland/boards2/v0:test-board/1/2/flagging-reasons)\n//\n// ---\n// Sort by: [newest first](/r/gnoland/boards2/v0:test-board/1/2?order=desc)\n// \u003e\n// \u003e \u003e\n// \u003e \u003e **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC [\\#3](/r/gnoland/boards2/v0:test-board/1/3)\n// \u003e \u003e\n// \u003e \u003e\n// \u003e \u003e \u003cgno-foreign\u003e\n// \u003e \u003e Third comment\n// \u003e \u003e \u003c/gno-foreign\u003e\n// \u003e \u003e\n// \u003e \u003e\n// \u003e \u003e\n// \u003e \u003e ↳ [Flag](/r/gnoland/boards2/v0:test-board/1/3/flag) • [Reply](/r/gnoland/boards2/v0:test-board/1/3/reply) • [Edit](/r/gnoland/boards2/v0:test-board/1/3/edit) • [Delete](/r/gnoland/boards2/v0$help\u0026func=DeleteReply\u0026boardID=2\u0026replyID=3\u0026threadID=1)\n"},{"name":"z_ui_reply_03_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ui_reply_03_filetest\n\n// Render the \"reply to a comment\" form: the parent-comment preview is now\n// shown inside a labeled \u003cgno-foreign\u003e sandbox (was md.Blockquote, which\n// gave no sandboxing).\npackage z_ui_reply_03_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner     address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tboardName         = \"test-board\"\n)\n\nvar (\n\tthreadID boards.ID\n\treplyID  boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboardID := boards2.CreateBoard(cross(cur), boardName, false, false)\n\tthreadID = boards2.CreateThread(cross(cur), boardID, \"Foo\", \"Body\")\n\treplyID = boards2.CreateReply(cross(cur), boardID, threadID, 0, \"Parent comment body\")\n}\n\nfunc main() {\n\t// The \".../{reply}/reply\" route renders the parent-preview branch.\n\tpath := boardName + \"/\" + threadID.String() + \"/\" + replyID.String() + \"/reply\"\n\tcontent := boards2.Render(path)\n\tprintln(strings.Contains(content, \"Replying to a comment posted by\") \u0026\u0026\n\t\tstrings.Contains(content, `\u003cgno-foreign label=\"Quoted comment\"\u003e`) \u0026\u0026\n\t\tstrings.Contains(content, \"Parent comment body\"))\n}\n\n// Output:\n// true\n"},{"name":"z_ui_reply_04_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ui_reply_04_filetest\n\n// Re-rooting at a depth-2 reply renders its parent as context with a\n// \"Continue this thread →\" drill-up link (the parent is itself a reply with\n// children), instead of a \"View Thread\" jump to the thread root.\npackage z_ui_reply_04_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid := boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid := boards2.CreateThread(cross(cur), bid, \"Foo\", \"OP body\")\n\taid := boards2.CreateReply(cross(cur), bid, tid, 0, \"comment A\") // #2, parent = thread\n\tboards2.CreateReply(cross(cur), bid, tid, aid, \"reply B\")        // #3, parent = A\n}\n\nfunc main(cur realm) {\n\t// Re-root at reply #3 (B); its parent #2 (A) renders as context and, being\n\t// a reply with children, links \"Continue this thread →\" (to A's re-root).\n\tout := boards2.Render(\"test-board/1/3\")\n\tok := strings.Contains(out, \"Continue this thread →\") \u0026\u0026\n\t\tstrings.Contains(out, \"/r/gnoland/boards2/v0:test-board/1/2\") // → A's re-root, not the thread root\n\tprintln(ok)\n}\n\n// Output:\n// true\n"},{"name":"z_ui_thread_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ui_thread_00_filetest\n\n// Render default thread view.\npackage z_ui_thread_00_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner     address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tboardName         = \"test-board\"\n)\n\nvar threadID boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a board and a thread\n\tboardID := boards2.CreateBoard(cross(cur), boardName, false, false)\n\tthreadID = boards2.CreateThread(cross(cur), boardID, \"Foo\", \"Body\")\n\n\t// Create two comments and a reply to a comment\n\tboards2.CreateReply(cross(cur), boardID, threadID, 0, \"First comment\")\n\n\treplyID := boards2.CreateReply(cross(cur), boardID, threadID, 0, \"Second comment\")\n\tboards2.CreateReply(cross(cur), boardID, threadID, replyID, \"Third comment\")\n}\n\nfunc main() {\n\tpath := boardName + \"/\" + threadID.String()\n\tprintln(boards2.Render(path))\n}\n\n// Output:\n// # [Boards](/r/gnoland/boards2/v0) › [test\\-board](/r/gnoland/boards2/v0:test-board)\n// ## Foo\n//\n// **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC\n//\n//\n// \u003cgno-foreign\u003e\n// Body\n// \u003c/gno-foreign\u003e\n//\n//\n//\n// ↳ [Flag](/r/gnoland/boards2/v0:test-board/1/flag) • [Repost](/r/gnoland/boards2/v0:test-board/1/repost) • [Comment](/r/gnoland/boards2/v0:test-board/1/reply) [2] • [Edit](/r/gnoland/boards2/v0:test-board/1/edit) • [Delete](/r/gnoland/boards2/v0$help\u0026func=DeleteThread\u0026boardID=2\u0026threadID=1)\n//\n// ---\n// Sort by: [newest first](/r/gnoland/boards2/v0:test-board/1?order=desc)\n//\n// \u003e\n// \u003e **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC [\\#2](/r/gnoland/boards2/v0:test-board/1/2)\n// \u003e\n// \u003e\n// \u003e \u003cgno-foreign\u003e\n// \u003e First comment\n// \u003e \u003c/gno-foreign\u003e\n// \u003e\n// \u003e\n// \u003e\n// \u003e ↳ [Flag](/r/gnoland/boards2/v0:test-board/1/2/flag) • [Reply](/r/gnoland/boards2/v0:test-board/1/2/reply) • [Edit](/r/gnoland/boards2/v0:test-board/1/2/edit) • [Delete](/r/gnoland/boards2/v0$help\u0026func=DeleteReply\u0026boardID=2\u0026replyID=2\u0026threadID=1)\n//\n// \u003e\n// \u003e **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC [\\#3](/r/gnoland/boards2/v0:test-board/1/3)\n// \u003e\n// \u003e\n// \u003e \u003cgno-foreign\u003e\n// \u003e Second comment\n// \u003e \u003c/gno-foreign\u003e\n// \u003e\n// \u003e\n// \u003e\n// \u003e ↳ [Flag](/r/gnoland/boards2/v0:test-board/1/3/flag) • [Reply](/r/gnoland/boards2/v0:test-board/1/3/reply) [1] • [Edit](/r/gnoland/boards2/v0:test-board/1/3/edit) • [Delete](/r/gnoland/boards2/v0$help\u0026func=DeleteReply\u0026boardID=2\u0026replyID=3\u0026threadID=1)\n// \u003e\n// \u003e \u003e\n// \u003e \u003e **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC [\\#4](/r/gnoland/boards2/v0:test-board/1/4)\n// \u003e \u003e\n// \u003e \u003e\n// \u003e \u003e \u003cgno-foreign\u003e\n// \u003e \u003e Third comment\n// \u003e \u003e \u003c/gno-foreign\u003e\n// \u003e \u003e\n// \u003e \u003e\n// \u003e \u003e\n// \u003e \u003e ↳ [Flag](/r/gnoland/boards2/v0:test-board/1/4/flag) • [Reply](/r/gnoland/boards2/v0:test-board/1/4/reply) • [Edit](/r/gnoland/boards2/v0:test-board/1/4/edit) • [Delete](/r/gnoland/boards2/v0$help\u0026func=DeleteReply\u0026boardID=2\u0026replyID=4\u0026threadID=1)\n"},{"name":"z_ui_thread_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ui_thread_01_filetest\n\n// Render thread sorting comments from newest to oldest.\npackage z_ui_thread_01_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner     address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tboardName         = \"test-board\"\n)\n\nvar threadID boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a board and a thread\n\tboardID := boards2.CreateBoard(cross(cur), boardName, false, false)\n\tthreadID = boards2.CreateThread(cross(cur), boardID, \"Foo\", \"Body\")\n\n\t// Create two comments and a reply to a comment\n\tboards2.CreateReply(cross(cur), boardID, threadID, 0, \"First comment\")\n\n\treplyID := boards2.CreateReply(cross(cur), boardID, threadID, 0, \"Second comment\")\n\tboards2.CreateReply(cross(cur), boardID, threadID, replyID, \"Third comment\")\n}\n\nfunc main() {\n\tpath := boardName + \"/\" + threadID.String() + \"?order=desc\"\n\tprintln(boards2.Render(path))\n}\n\n// Output:\n// # [Boards](/r/gnoland/boards2/v0) › [test\\-board](/r/gnoland/boards2/v0:test-board)\n// ## Foo\n//\n// **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC\n//\n//\n// \u003cgno-foreign\u003e\n// Body\n// \u003c/gno-foreign\u003e\n//\n//\n//\n// ↳ [Flag](/r/gnoland/boards2/v0:test-board/1/flag) • [Repost](/r/gnoland/boards2/v0:test-board/1/repost) • [Comment](/r/gnoland/boards2/v0:test-board/1/reply) [2] • [Edit](/r/gnoland/boards2/v0:test-board/1/edit) • [Delete](/r/gnoland/boards2/v0$help\u0026func=DeleteThread\u0026boardID=2\u0026threadID=1)\n//\n// ---\n// Sort by: [oldest first](/r/gnoland/boards2/v0:test-board/1?order=asc)\n//\n// \u003e\n// \u003e **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC [\\#3](/r/gnoland/boards2/v0:test-board/1/3)\n// \u003e\n// \u003e\n// \u003e \u003cgno-foreign\u003e\n// \u003e Second comment\n// \u003e \u003c/gno-foreign\u003e\n// \u003e\n// \u003e\n// \u003e\n// \u003e ↳ [Flag](/r/gnoland/boards2/v0:test-board/1/3/flag) • [Reply](/r/gnoland/boards2/v0:test-board/1/3/reply) [1] • [Edit](/r/gnoland/boards2/v0:test-board/1/3/edit) • [Delete](/r/gnoland/boards2/v0$help\u0026func=DeleteReply\u0026boardID=2\u0026replyID=3\u0026threadID=1)\n// \u003e\n// \u003e \u003e\n// \u003e \u003e **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC [\\#4](/r/gnoland/boards2/v0:test-board/1/4)\n// \u003e \u003e\n// \u003e \u003e\n// \u003e \u003e \u003cgno-foreign\u003e\n// \u003e \u003e Third comment\n// \u003e \u003e \u003c/gno-foreign\u003e\n// \u003e \u003e\n// \u003e \u003e\n// \u003e \u003e\n// \u003e \u003e ↳ [Flag](/r/gnoland/boards2/v0:test-board/1/4/flag) • [Reply](/r/gnoland/boards2/v0:test-board/1/4/reply) • [Edit](/r/gnoland/boards2/v0:test-board/1/4/edit) • [Delete](/r/gnoland/boards2/v0$help\u0026func=DeleteReply\u0026boardID=2\u0026replyID=4\u0026threadID=1)\n//\n// \u003e\n// \u003e **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC [\\#2](/r/gnoland/boards2/v0:test-board/1/2)\n// \u003e\n// \u003e\n// \u003e \u003cgno-foreign\u003e\n// \u003e First comment\n// \u003e \u003c/gno-foreign\u003e\n// \u003e\n// \u003e\n// \u003e\n// \u003e ↳ [Flag](/r/gnoland/boards2/v0:test-board/1/2/flag) • [Reply](/r/gnoland/boards2/v0:test-board/1/2/reply) • [Edit](/r/gnoland/boards2/v0:test-board/1/2/edit) • [Delete](/r/gnoland/boards2/v0$help\u0026func=DeleteReply\u0026boardID=2\u0026replyID=2\u0026threadID=1)\n"},{"name":"z_ui_thread_02_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ui_thread_02_filetest\n\n// Render both original thread and thread repost view.\npackage z_ui_thread_02_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner        address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tsrcBoardName         = \"test-board\"\n\tdstBoardName         = \"test-board-2\"\n)\n\nvar (\n\tthreadID       boards.ID\n\trepostThreadID boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a board and a thread\n\tboardID := boards2.CreateBoard(cross(cur), srcBoardName, false, false)\n\tthreadID = boards2.CreateThread(cross(cur), boardID, \"Foo\", \"Body\")\n\n\t// Repost thread into a different board\n\tdstBoardID := boards2.CreateBoard(cross(cur), dstBoardName, false, false)\n\trepostThreadID = boards2.CreateRepost(cross(cur), boardID, threadID, dstBoardID, \"Bar\", \"Body2\")\n}\n\nfunc main() {\n\tpath := srcBoardName + \"/\" + threadID.String()\n\tprintln(boards2.Render(path))\n\n\tprintln(\"\u003e\u003e\u003e\u003c\u003c\u003c\\n\")\n\n\tpath = dstBoardName + \"/\" + repostThreadID.String()\n\tprintln(boards2.Render(path))\n}\n\n// Output:\n// # [Boards](/r/gnoland/boards2/v0) › [test\\-board](/r/gnoland/boards2/v0:test-board)\n// ## Foo\n//\n// **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC\n//\n//\n// \u003cgno-foreign\u003e\n// Body\n// \u003c/gno-foreign\u003e\n//\n//\n//\n// ↳ [Flag](/r/gnoland/boards2/v0:test-board/1/flag) • [Repost](/r/gnoland/boards2/v0:test-board/1/repost) [1] • [Comment](/r/gnoland/boards2/v0:test-board/1/reply) • [Edit](/r/gnoland/boards2/v0:test-board/1/edit) • [Delete](/r/gnoland/boards2/v0$help\u0026func=DeleteThread\u0026boardID=2\u0026threadID=1)\n//\n// \u003e\u003e\u003e\u003c\u003c\u003c\n//\n// # [Boards](/r/gnoland/boards2/v0) › [test\\-board\\-2](/r/gnoland/boards2/v0:test-board-2)\n// ## Bar\n//\n// **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC\n// \u003e [!INFO]- Thread Repost\n// \u003e Original thread is [Foo](/r/gnoland/boards2/v0:test-board/1)\n// \u003e Created by [g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx) on 2009-02-13 11:31pm UTC\n//\n//\n//\n// \u003cgno-foreign\u003e\n// Body2\n// \u003c/gno-foreign\u003e\n//\n//\n//\n// \u003e\n// \u003e\n// \u003e \u003cgno-foreign\u003e\n// \u003e Body\n// \u003e \u003c/gno-foreign\u003e\n// \u003e\n// \u003e\n//\n// ↳ [Flag](/r/gnoland/boards2/v0:test-board-2/1/flag) • [Repost](/r/gnoland/boards2/v0:test-board-2/1/repost) • [Comment](/r/gnoland/boards2/v0:test-board-2/1/reply) • [Edit](/r/gnoland/boards2/v0:test-board-2/1/edit) • [Delete](/r/gnoland/boards2/v0$help\u0026func=DeleteThread\u0026boardID=3\u0026threadID=1)\n"},{"name":"z_ui_thread_03_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ui_thread_03_filetest\n\n// Render thread from a readonly board.\n// Rendered thread action links should be limited to readonly actions.\npackage z_ui_thread_03_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner     address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tboardName         = \"test-board\"\n)\n\nvar threadID boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a readonly board and then add a thread\n\tboardID := boards2.CreateBoard(cross(cur), boardName, false, false)\n\tthreadID = boards2.CreateThread(cross(cur), boardID, \"Foo\", \"Body\")\n\tboards2.FreezeBoard(cross(cur), boardID)\n}\n\nfunc main() {\n\tpath := boardName + \"/\" + threadID.String()\n\tprintln(boards2.Render(path))\n}\n\n// Output:\n// # [Boards](/r/gnoland/boards2/v0) › [test\\-board](/r/gnoland/boards2/v0:test-board)\n// ## Foo\n//\n// **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC\n//\n//\n// \u003cgno-foreign\u003e\n// Body\n// \u003c/gno-foreign\u003e\n//\n//\n//\n// ↳ [Flag](/r/gnoland/boards2/v0:test-board/1/flag) • [Repost](/r/gnoland/boards2/v0:test-board/1/repost)\n"},{"name":"z_ui_thread_04_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ui_thread_04_filetest\n\n// Render thread repost of a deleted thread.\npackage z_ui_thread_04_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner        address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tsrcBoardName         = \"test-board\"\n\tdstBoardName         = \"test-board-2\"\n)\n\nvar repostThreadID boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a board and a thread\n\tboardID := boards2.CreateBoard(cross(cur), srcBoardName, false, false)\n\tthreadID := boards2.CreateThread(cross(cur), boardID, \"Foo\", \"Body\")\n\n\t// Repost thread into a different board\n\tdstBoardID := boards2.CreateBoard(cross(cur), dstBoardName, false, false)\n\trepostThreadID = boards2.CreateRepost(cross(cur), boardID, threadID, dstBoardID, \"Bar\", \"Body2\")\n\n\t// Remove the original thread\n\tboards2.DeleteThread(cross(cur), boardID, threadID)\n}\n\nfunc main() {\n\tpath := dstBoardName + \"/\" + repostThreadID.String()\n\tprintln(boards2.Render(path))\n}\n\n// Output:\n// # [Boards](/r/gnoland/boards2/v0) › [test\\-board\\-2](/r/gnoland/boards2/v0:test-board-2)\n// ## Bar\n//\n// **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC\n//\n//\n// \u003cgno-foreign\u003e\n// Body2\n// \u003c/gno-foreign\u003e\n//\n//\n//\n// \u003e ⚠ Source post has been deleted\n//\n// ↳ [Flag](/r/gnoland/boards2/v0:test-board-2/1/flag) • [Repost](/r/gnoland/boards2/v0:test-board-2/1/repost) • [Comment](/r/gnoland/boards2/v0:test-board-2/1/reply) • [Edit](/r/gnoland/boards2/v0:test-board-2/1/edit) • [Delete](/r/gnoland/boards2/v0$help\u0026func=DeleteThread\u0026boardID=3\u0026threadID=1)\n"},{"name":"z_ui_thread_05_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ui_thread_05_filetest\n\n// Render thread which has been flagged.\npackage z_ui_thread_05_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner     address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tboardName         = \"BoardName\"\n)\n\nvar threadID boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a readonly board and then add a thread\n\tboardID := boards2.CreateBoard(cross(cur), boardName, false, false)\n\tthreadID = boards2.CreateThread(cross(cur), boardID, \"Foo\", \"Body\")\n\n\t// Flag the thread\n\tboards2.SetFlaggingThreshold(cross(cur), boardID, 1)\n\tboards2.FlagThread(cross(cur), boardID, threadID, \"Reason\")\n}\n\nfunc main() {\n\tpath := boardName + \"/\" + threadID.String()\n\tprintln(boards2.Render(path))\n}\n\n// Output:\n// ⚠ Thread has been flagged as [inappropriate](/r/gnoland/boards2/v0:BoardName/1/flagging-reasons)\n"},{"name":"z_ui_thread_06_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ui_thread_06_filetest\n\n// Render thread repost of a flagged thread.\npackage z_ui_thread_06_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner        address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tsrcBoardName         = \"test-board\"\n\tdstBoardName         = \"test-board-2\"\n)\n\nvar repostThreadID boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a board and a thread\n\tboardID := boards2.CreateBoard(cross(cur), srcBoardName, false, false)\n\tthreadID := boards2.CreateThread(cross(cur), boardID, \"Foo\", \"Body\")\n\n\t// Repost thread into a different board\n\tdstBoardID := boards2.CreateBoard(cross(cur), dstBoardName, false, false)\n\trepostThreadID = boards2.CreateRepost(cross(cur), boardID, threadID, dstBoardID, \"Bar\", \"Body2\")\n\n\t// Flag original thread\n\tboards2.SetFlaggingThreshold(cross(cur), boardID, 1)\n\tboards2.FlagThread(cross(cur), boardID, threadID, \"Reason\")\n}\n\nfunc main() {\n\tpath := dstBoardName + \"/\" + repostThreadID.String()\n\tprintln(boards2.Render(path))\n}\n\n// Output:\n// # [Boards](/r/gnoland/boards2/v0) › [test\\-board\\-2](/r/gnoland/boards2/v0:test-board-2)\n// ## Bar\n//\n// **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC\n//\n//\n// \u003cgno-foreign\u003e\n// Body2\n// \u003c/gno-foreign\u003e\n//\n//\n//\n// \u003e ⚠ Source post has been flagged as inappropriate\n//\n// ↳ [Flag](/r/gnoland/boards2/v0:test-board-2/1/flag) • [Repost](/r/gnoland/boards2/v0:test-board-2/1/repost) • [Comment](/r/gnoland/boards2/v0:test-board-2/1/reply) • [Edit](/r/gnoland/boards2/v0:test-board-2/1/edit) • [Delete](/r/gnoland/boards2/v0$help\u0026func=DeleteThread\u0026boardID=3\u0026threadID=1)\n"},{"name":"z_ui_thread_07_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ui_thread_07_filetest\n\n// Render thread with a title that contains Markdown\npackage z_ui_thread_07_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner     address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tboardName         = \"test-board\"\n)\n\nvar threadID boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a board and a thread\n\tboardID := boards2.CreateBoard(cross(cur), boardName, false, false)\n\tthreadID = boards2.CreateThread(cross(cur), boardID, \"[Foo](https://foo.com)\", \"Body\")\n}\n\nfunc main() {\n\tpath := boardName + \"/\" + threadID.String()\n\tprintln(boards2.Render(path))\n}\n\n// Output:\n// # [Boards](/r/gnoland/boards2/v0) › [test\\-board](/r/gnoland/boards2/v0:test-board)\n// ## \\[Foo\\]\\(https://foo\\.com\\)\n//\n// **[g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx](/u/g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx)** `owner` · 2009-02-13 11:31pm UTC\n//\n//\n// \u003cgno-foreign\u003e\n// Body\n// \u003c/gno-foreign\u003e\n//\n//\n//\n// ↳ [Flag](/r/gnoland/boards2/v0:test-board/1/flag) • [Repost](/r/gnoland/boards2/v0:test-board/1/repost) • [Comment](/r/gnoland/boards2/v0:test-board/1/reply) • [Edit](/r/gnoland/boards2/v0:test-board/1/edit) • [Delete](/r/gnoland/boards2/v0$help\u0026func=DeleteThread\u0026boardID=2\u0026threadID=1)\n"},{"name":"z_ui_thread_08_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ui_thread_08_filetest\n\n// A thread large enough to exceed the foreign-block render budget\n// degrades gracefully: rendering stops before gnoweb's per-render\n// foreign-block cap would blank comments, and a \"More replies — view\n// full thread\" notice is shown instead (see maxRenderedBodies). The\n// reply count and assertion are derived from MaxBlocksPerRender so this\n// stays a truncation test if the cap changes.\npackage z_ui_thread_08_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\t\"gno.land/p/nt/markdown/foreign/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"OP body\")\n\n\t// One top-level comment with many direct sub-replies (sub-replies\n\t// are not paginated, so they consume the shared render budget).\n\t// Create MaxBlocksPerRender of them — strictly more than the budget\n\t// (cap minus a margin) — so rendering must truncate.\n\trid := boards2.CreateReply(cross(cur), bid, tid, 0, \"top comment\")\n\tfor i := 0; i \u003c foreign.MaxBlocksPerRender(); i++ {\n\t\tboards2.CreateReply(cross(cur), bid, tid, rid, \"sub reply\")\n\t}\n}\n\nfunc main(cur realm) {\n\tcontent := boards2.Render(\"test-board/1\")\n\tn := strings.Count(content, \"\u003cgno-foreign\u003e\")\n\t// The top comment's many sub-replies are capped inline at pageSizeReplies\n\t// (so n stays well under the foreign-block budget); the rest are reachable\n\t// via the comment's paginated re-rooted view (\"View all N replies\").\n\tcapped := n \u003e 0 \u0026\u0026 n \u003c foreign.MaxBlocksPerRender()/4\n\tprintln(capped \u0026\u0026 strings.Contains(content, \"View all 256 replies\"))\n}\n\n// Output:\n// true\n"},{"name":"z_ui_thread_09_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ui_thread_09_filetest\n\n// The flat \"all comments\" view (?flat=1) paginates ThreadMeta.AllReplies so\n// every comment is reachable, unlike the recursive threaded view which\n// truncates a large thread. Each flat page renders one \u003cgno-foreign\u003e block\n// per comment, bounded by pageSizeFlat (50). With 60 comments the flat view\n// spans 2 pages; the last comment is reachable via newest-first (desc page 1)\n// and via page 2.\npackage z_ui_thread_09_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"OP body\")\n\t// pageSizeFlat is 50, so 60 comments span 2 flat pages.\n\tfor i := 0; i \u003c 60; i++ {\n\t\tboards2.CreateReply(cross(cur), bid, tid, 0, ufmt.Sprintf(\"comment-%d\", i))\n\t}\n}\n\nfunc main(cur realm) {\n\tp1 := boards2.Render(\"test-board/1?flat=1\")              // oldest first, page 1\n\tdesc := boards2.Render(\"test-board/1?flat=1\u0026order=desc\") // newest first, page 1\n\tp2 := boards2.Render(\"test-board/1?flat=1\u0026page=2\")       // oldest first, page 2\n\n\tok := strings.Contains(p1, \"All 60 comments\") \u0026\u0026\n\t\tstrings.Contains(p1, \"page 1 of 2\") \u0026\u0026\n\t\tstrings.Contains(p1, \"comment-0\") \u0026\u0026 // first comment is on page 1\n\t\t!strings.Contains(p1, \"comment-59\") \u0026\u0026 // last comment is NOT on oldest-first page 1\n\t\tstrings.Contains(desc, \"comment-59\") \u0026\u0026 // last comment reachable newest-first\n\t\tstrings.Contains(p2, \"comment-59\") \u0026\u0026 // and on the last page\n\t\tstrings.Contains(p2, \"page 2 of 2\")\n\tprintln(ok)\n}\n\n// Output:\n// true\n"},{"name":"z_ui_thread_10_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ui_thread_10_filetest\n\n// No post or comment renders more than pageSizeReplies (10) direct children\n// inline. A comment with more shows the cap inline plus a \"View all N replies\"\n// link to its re-rooted view, which paginates the children on its own ?page=\n// (a distinct path from the thread, so no pager collision).\npackage z_ui_thread_10_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid := boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid := boards2.CreateThread(cross(cur), bid, \"Foo\", \"OP body\")\n\tcid := boards2.CreateReply(cross(cur), bid, tid, 0, \"parent comment\") // reply #2\n\t// 12 direct children under the comment — past the inline cap of 10.\n\tfor i := 0; i \u003c 12; i++ {\n\t\tboards2.CreateReply(cross(cur), bid, tid, cid, ufmt.Sprintf(\"child-%d\", i))\n\t}\n}\n\nfunc main(cur realm) {\n\tthread := boards2.Render(\"test-board/1\")           // threaded view\n\treroot := boards2.Render(\"test-board/1/2\")         // re-root at the comment, page 1\n\treroot2 := boards2.Render(\"test-board/1/2?page=2\") // re-root, page 2\n\tthreadDesc := boards2.Render(\"test-board/1?order=desc\")\n\n\tok := strings.Contains(thread, \"View all 12 replies\") \u0026\u0026 // breadth cap inline + link\n\t\tstrings.Contains(reroot, \"page 1 of 2\") \u0026\u0026 // re-root paginates the children\n\t\tstrings.Contains(reroot2, \"page 2 of 2\") \u0026\u0026 // and advances to the tail\n\t\t// In a desc thread the \"View all\" link carries the order so the\n\t\t// re-root opens newest-first too; in the default (asc) view it doesn't.\n\t\tstrings.Contains(threadDesc, \"/r/gnoland/boards2/v0:test-board/1/2?order=desc\") \u0026\u0026\n\t\t!strings.Contains(thread, \"test-board/1/2?order=desc\")\n\tprintln(ok)\n}\n\n// Output:\n// true\n"},{"name":"z_ui_thread_11_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ui_thread_11_filetest\n\n// A comment's inline children follow the page's ?order: newest-first when the\n// thread is sorted desc, oldest-first otherwise — not always oldest-first.\npackage z_ui_thread_11_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid := boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid := boards2.CreateThread(cross(cur), bid, \"Foo\", \"OP body\")\n\tcid := boards2.CreateReply(cross(cur), bid, tid, 0, \"parent\") // #2\n\tfor i := 0; i \u003c 3; i++ {\n\t\tboards2.CreateReply(cross(cur), bid, tid, cid, ufmt.Sprintf(\"child-%d\", i)) // #3,#4,#5\n\t}\n}\n\nfunc main(cur realm) {\n\tdesc := boards2.Render(\"test-board/1?order=desc\")\n\tasc := boards2.Render(\"test-board/1\") // default = oldest first\n\n\t// desc view: the comment's children render newest-first (child-2 before child-0).\n\tdescOK := strings.Index(desc, \"child-2\") \u003c strings.Index(desc, \"child-0\")\n\t// asc view: oldest-first (child-0 before child-2).\n\tascOK := strings.Index(asc, \"child-0\") \u003c strings.Index(asc, \"child-2\")\n\tprintln(descOK \u0026\u0026 ascOK)\n}\n\n// Output:\n// true\n"},{"name":"z_ui_thread_12_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ui_thread_12_filetest\n\n// When the render budget is exhausted mid-page in the threaded view, the\n// offset page-picker would offer a \"next page\" that jumps past the replies\n// that were skipped when the budget ran out. Instead the picker is suppressed\n// and the reader is funneled to the complete, reachable flat view.\npackage z_ui_thread_12_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"OP body\")\n\n\t// First top-level reply carries a deep chain that alone exhausts the\n\t// render budget (maxRenderedBodies, 246) on page 1.\n\tprev := boards2.CreateReply(cross(cur), bid, tid, 0, \"top-deep\")\n\tfor i := 0; i \u003c 250; i++ {\n\t\tprev = boards2.CreateReply(cross(cur), bid, tid, prev, \"chain\")\n\t}\n\t// Plus enough more top-level replies that the thread spans 2 pages, so a\n\t// picker WOULD render absent the suppression.\n\tfor i := 0; i \u003c 12; i++ {\n\t\tboards2.CreateReply(cross(cur), bid, tid, 0, \"top-more\")\n\t}\n}\n\nfunc main(cur realm) {\n\tout := boards2.Render(\"test-board/1\")\n\t// Budget truncated page 1: the flat \"view all comments\" link is shown,\n\t// and the content-skipping page picker (\"page 1 of N\") is suppressed.\n\tok := strings.Contains(out, \"view all comments\") \u0026\u0026\n\t\t!strings.Contains(out, \"page 1 of\")\n\tprintln(ok)\n}\n\n// Output:\n// true\n"},{"name":"z_ui_thread_13_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ui_thread_13_filetest\n\n// A non-numeric thread/reply ID segment in the URL is echoed back in the error\n// message; it must be escaped so a crafted URL can't inject markup/HTML into\n// the rendered page (a reflected-injection vector, live under gnoweb -html).\npackage z_ui_thread_13_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.CreateBoard(cross(cur), \"test-board\", false, false)\n}\n\nfunc main(cur realm) {\n\t// board exists, so the route reaches renderThread; \"9\u003cx\" fails Atoi and is\n\t// echoed — escaped, so the raw \"9\u003cx\" never appears (md.EscapeText → \"9\\\u003cx\").\n\tout := boards2.Render(\"test-board/9\u003cx\")\n\tok := strings.Contains(out, \"Invalid thread ID\") \u0026\u0026\n\t\t!strings.Contains(out, \"9\u003cx\")\n\tprintln(ok)\n}\n\n// Output:\n// true\n"},{"name":"z_ui_thread_14_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_ui_thread_14_filetest\n\n// In a frozen thread, a reply must not show Reply/Edit/Delete action links\n// (the backend rejects them as the thread is frozen); only Flag remains —\n// matching how the OP and a readonly board already behave.\npackage z_ui_thread_14_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar (\n\tbid      boards.ID\n\trid, tid boards.ID\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross(cur), bid, \"Foo\", \"OP body\")\n\trid = boards2.CreateReply(cross(cur), bid, tid, 0, \"a reply\") // #2\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.FreezeThread(cross(cur), bid, tid)\n\n\tout := boards2.Render(\"test-board/1\")\n\t// Reply #2 keeps Flag but loses Reply/Edit/Delete in a frozen thread.\n\tok := strings.Contains(out, \"test-board/1/2/flag\") \u0026\u0026\n\t\t!strings.Contains(out, \"test-board/1/2/edit\") \u0026\u0026\n\t\t!strings.Contains(out, \"test-board/1/2/reply\")\n\tprintln(ok)\n}\n\n// Output:\n// true\n"},{"name":"z_unban_00_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_unban_00_filetest\n\npackage z_unban_00_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\tboards2.Ban(cross(cur), bid, user, boards2.BanDay, \"Unpolite behavior\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.Unban(cross(cur), bid, user, \"\")\n\n\tprintln(boards2.IsBanned(bid, user))\n}\n\n// Output:\n// false\n"},{"name":"z_unban_01_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_unban_01_filetest\n\npackage z_unban_01_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst owner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.Unban(cross(cur), bid, \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\", \"\")\n}\n\n// Error:\n// user is not banned\n"},{"name":"z_unban_02_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/boards2/v0/filetests/z_unban_02_filetest\n\npackage z_unban_02_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards/v0\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v0\"\n)\n\nconst (\n\towner address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\tuser  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross(cur), \"test123\", false, false)\n\tboards2.Ban(cross(cur), bid, user, boards2.BanDay, \"Unpolite behavior\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\"))\n\n\t// Try to unban without unbanning permissions\n\tboards2.Unban(cross(cur), bid, user, \"\")\n}\n\n// Error:\n// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"NBK/fU/WzcBHeEU3iwVeGORdczvokhrhkniA/wnljCxHRVkC0SHrEEIzKaE8qgRh8IY9b8NOtYcGvlPrsAtI1Q=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7","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/v0\"\n\t\"gno.land/p/leon/ctg/v0\"\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/moul/mdtable/v0\"\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":"coins_test.gno","body":"package coins\n\nimport (\n\t\"chain\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/leon/ctg/v0\"\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nfunc TestBalanceChecker(t *testing.T) {\n\tdenom1 := \"testtoken1\"\n\tdenom2 := \"testtoken2\"\n\taddr1 := testutils.TestAddress(\"user1\")\n\taddr2 := testutils.TestAddress(\"user2\")\n\n\tcoinsRealm := testing.NewCodeRealm(\"gno.land/r/gnoland/coins\")\n\ttesting.SetRealm(coinsRealm)\n\n\ttesting.IssueCoins(addr1, chain.NewCoins(chain.NewCoin(denom1, 1000000)))\n\ttesting.IssueCoins(addr2, chain.NewCoins(chain.NewCoin(denom1, 501)))\n\n\ttesting.IssueCoins(addr2, chain.NewCoins(chain.NewCoin(denom2, 12345)))\n\n\tgnoAddr, _ := ctg.ConvertCosmosToGno(\"cosmos1s2v4tdskccx2p3yyvzem4mw5nn5fprwcku77hr\")\n\tosmoAddr := \"osmo1fl48vsnmsdzcv85q5d2q4z5ajdha8yu3aq6l09\"\n\tgnoAddr1, _ := ctg.ConvertAnyToGno(osmoAddr)\n\n\ttesting.IssueCoins(gnoAddr1, chain.NewCoins(chain.NewCoin(denom2, 12345)))\n\n\ttests := []struct {\n\t\tname      string\n\t\tpath      string\n\t\tcontains  string\n\t\twantPanic bool\n\t}{\n\t\t{\n\t\t\tname:     \"homepage\",\n\t\t\tpath:     \"\",\n\t\t\tcontains: \"# Gno.land Coins Explorer\",\n\t\t},\n\t\t// TODO: not supported yet\n\t\t// {\n\t\t// \tname:     \"total supply\",\n\t\t// \tpath:     denom,\n\t\t// \texpected: \"Balance: 1500000testtoken\",\n\t\t// },\n\t\t{\n\t\t\tname:     \"addr1's coin balance\",\n\t\t\tpath:     ufmt.Sprintf(\"balances?address=%s\u0026coin=%s\", addr1.String(), denom1),\n\t\t\tcontains: ufmt.Sprintf(\"`%s` has `%d%s`\", addr1.String(), 1000000, denom1),\n\t\t},\n\t\t{\n\t\t\tname:     \"addr2's full balances\",\n\t\t\tpath:     ufmt.Sprintf(\"balances?address=%s\", addr2.String()),\n\t\t\tcontains: ufmt.Sprintf(\"This page shows full coin balances of `%s` at block\", addr2.String()),\n\t\t},\n\t\t{\n\t\t\tname: \"addr2's full balances\",\n\t\t\tpath: ufmt.Sprintf(\"balances?address=%s\", addr2.String()),\n\t\t\tcontains: `| testtoken1 | 501 |\n| testtoken2 | 12345 |`,\n\t\t},\n\t\t{\n\t\t\tname:     \"addr2's coin balance\",\n\t\t\tpath:     ufmt.Sprintf(\"balances?address=%s\u0026coin=%s\", addr2.String(), denom1),\n\t\t\tcontains: ufmt.Sprintf(\"`%s` has `%d%s`\", addr2.String(), 501, denom1),\n\t\t},\n\t\t{\n\t\t\tname:     \"cosmos addr conversion\",\n\t\t\tpath:     \"convert/cosmos1s2v4tdskccx2p3yyvzem4mw5nn5fprwcku77hr\",\n\t\t\tcontains: ufmt.Sprintf(\"`cosmos1s2v4tdskccx2p3yyvzem4mw5nn5fprwcku77hr` on Cosmos matches `%s`\", gnoAddr),\n\t\t},\n\t\t{\n\t\t\tname:     \"balances bech32 auto convert\",\n\t\t\tpath:     ufmt.Sprintf(\"balances?address=%s\u0026coin=%s\", osmoAddr, denom1),\n\t\t\tcontains: \"Automatically converted `osmo1fl48vsnmsdzcv85q5d2q4z5ajdha8yu3aq6l09`\",\n\t\t},\n\t\t{\n\t\t\tname:     \"single coin balance bech32 auto convert\",\n\t\t\tpath:     ufmt.Sprintf(\"balances?address=%s\", osmoAddr),\n\t\t\tcontains: \"Automatically converted `osmo1fl48vsnmsdzcv85q5d2q4z5ajdha8yu3aq6l09`\",\n\t\t},\n\t\t{\n\t\t\t// The \"coin\" parameter is whatever the URL says. banker.GetCoin panics on a\n\t\t\t// malformed denom where chain.Coins.AmountOf returns zero, so this page\n\t\t\t// must be rendered from the balances already read rather than by asking\n\t\t\t// the banker for an unvalidated denom — otherwise any visitor can 500 it.\n\t\t\tname:     \"malformed coin denom renders zero instead of panicking\",\n\t\t\tpath:     ufmt.Sprintf(\"balances?address=%s\u0026coin=UPPERCASE\", addr1.String()),\n\t\t\tcontains: ufmt.Sprintf(\"`%s` has `%d%s`\", addr1.String(), 0, \"UPPERCASE\"),\n\t\t},\n\t\t{\n\t\t\tname:      \"no addr\",\n\t\t\tpath:      \"balances?address=\",\n\t\t\tcontains:  \"Please input a valid address\",\n\t\t\twantPanic: false,\n\t\t},\n\t\t{\n\t\t\tname:      \"no addr\",\n\t\t\tpath:      \"balances?address=\u0026coin=\",\n\t\t\tcontains:  \"Please input a valid address and coin denomination.\",\n\t\t\twantPanic: false,\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid path\",\n\t\t\tpath:      \"invalid\",\n\t\t\tcontains:  \"404\",\n\t\t\twantPanic: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.wantPanic {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r == nil {\n\t\t\t\t\t\tt.Errorf(\"expected panic for %s\", tt.name)\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t}\n\n\t\t\tresult := Render(tt.path)\n\t\t\tif !tt.wantPanic {\n\t\t\t\tif !strings.Contains(result, tt.contains) {\n\t\t\t\t\tt.Errorf(\"expected %s to contain %s\", result, tt.contains)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"V3Levo0UfbSe0d1dOlX2dIcmhm0IVz8BDzyTS/LYG7pp6zM2C5mmEGwd1a2x3VObpenjTUZ/wRtwZ5OwjsINPQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7","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/v0\"\n\t\"gno.land/p/demo/gnorkle/gnorkle/v0\"\n\t\"gno.land/p/demo/gnorkle/message/v0\"\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":"contract_test.gno","body":"package ghverify\n\nimport (\n\t\"chain/runtime/unsafe\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n)\n\nfunc TestVerificationLifecycle(cur realm, t *testing.T) {\n\tdefaultAddress := unsafe.OriginCaller()\n\tuser1Address := address(testutils.TestAddress(\"user 1\"))\n\tuser2Address := address(testutils.TestAddress(\"user 2\"))\n\n\t// Verify request returns no feeds.\n\tresult := GnorkleEntrypoint(cur, \"request\")\n\tif result != \"[]\" {\n\t\tt.Fatalf(\"expected empty request result, got %s\", result)\n\t}\n\n\t// Make a verification request with the created user.\n\ttesting.SetOriginCaller(user1Address)\n\tRequestVerification(cur, \"deelawn\")\n\n\t// A subsequent request from the same address should panic because there is\n\t// already a feed with an ID of this user's address.\n\tvar errMsg string\n\tfunc(cur realm) {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\terrMsg = r.(error).Error()\n\t\t\t}\n\t\t}()\n\t\tRequestVerification(cur, \"deelawn\")\n\t}(cur)\n\tif errMsg != \"feed already exists\" {\n\t\tt.Fatalf(\"expected feed already exists, got %s\", errMsg)\n\t}\n\n\t// Verify the request returns no feeds for this non-whitelisted user.\n\tresult = GnorkleEntrypoint(cur, \"request\")\n\tif result != \"[]\" {\n\t\tt.Fatalf(\"expected empty request result, got %s\", result)\n\t}\n\n\t// Make a verification request with the created user.\n\ttesting.SetOriginCaller(user2Address)\n\tRequestVerification(cur, \"omarsy\")\n\n\t// Set the caller back to the whitelisted user and verify that the feed data\n\t// returned matches what should have been created by the `RequestVerification`\n\t// invocation.\n\ttesting.SetOriginCaller(defaultAddress)\n\tresult = GnorkleEntrypoint(cur, \"request\")\n\texpResult := `[{\"id\":\"` + string(user1Address) + `\",\"type\":\"0\",\"value_type\":\"string\",\"tasks\":[{\"gno_address\":\"` +\n\t\tstring(user1Address) + `\",\"github_handle\":\"deelawn\"}]},` +\n\t\t`{\"id\":\"` + string(user2Address) + `\",\"type\":\"0\",\"value_type\":\"string\",\"tasks\":[{\"gno_address\":\"` +\n\t\tstring(user2Address) + `\",\"github_handle\":\"omarsy\"}]}]`\n\tif result != expResult {\n\t\tt.Fatalf(\"expected request result %s, got %s\", expResult, result)\n\t}\n\n\t// Try to trigger feed ingestion from the non-authorized user.\n\ttesting.SetOriginCaller(user1Address)\n\tfunc(cur realm) {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\terrMsg = r.(error).Error()\n\t\t\t}\n\t\t}()\n\t\tGnorkleEntrypoint(cur, \"ingest,\"+string(user1Address)+\",OK\")\n\t}(cur)\n\tif errMsg != \"caller not whitelisted\" {\n\t\tt.Fatalf(\"expected caller not whitelisted, got %s\", errMsg)\n\t}\n\n\t// Set the caller back to the whitelisted user and transfer contract ownership.\n\ttesting.SetOriginCaller(defaultAddress)\n\tSetOwner(cross(cur), defaultAddress)\n\n\t// Now trigger the feed ingestion from the user and new owner and only whitelisted address.\n\tGnorkleEntrypoint(cur, \"ingest,\"+string(user1Address)+\",OK\")\n\tGnorkleEntrypoint(cur, \"ingest,\"+string(user2Address)+\",OK\")\n\n\t// Verify the ingestion autocommitted the value and triggered the post handler.\n\tdata := Render(\"\")\n\texpResult = `{\"deelawn\": \"` + string(user1Address) + `\",\"omarsy\": \"` + string(user2Address) + `\"}`\n\tif data != expResult {\n\t\tt.Fatalf(\"expected render data %s, got %s\", expResult, data)\n\t}\n\n\t// Finally make sure the feed was cleaned up after the data was committed.\n\tresult = GnorkleEntrypoint(cur, \"request\")\n\tif result != \"[]\" {\n\t\tt.Fatalf(\"expected empty request result, got %s\", result)\n\t}\n\n\t// Check that the accessor functions are working as expected.\n\tif handle := GetHandleByAddress(cur, string(user1Address)); handle != \"deelawn\" {\n\t\tt.Fatalf(\"expected deelawn, got %s\", handle)\n\t}\n\tif address_XXX := GetAddressByHandle(cur, \"deelawn\"); address_XXX != string(user1Address) {\n\t\tt.Fatalf(\"expected %s, got %s\", string(user1Address), address_XXX)\n\t}\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"bY8e0KQYSN6SBk+QHPhuI2ihIWG/rj6twyLQL44XBc0Hnw0Z1H9ngjhkgyXQfQXPHQx56ZSGP4FpvLJgXsRITg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7","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/v0\"\n\t\"gno.land/p/moul/dynreplacer/v0\"\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(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // 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/v0) - 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/v0)**\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.com/invite/gnoland)\n- [Bubble Rumble](https://bubblerumble.net/)\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"},{"name":"home_filetest.gno","body":"package main\n\nimport \"gno.land/r/gnoland/home\"\n\nfunc main() {\n\tprintln(home.Render(\"\"))\n}\n\n// Output:\n// # Welcome to Gno.land\n//\n// We're building Gno.land, set to become the leading open-source smart contract\n// platform, using Gno, an interpreted and fully deterministic variation of the\n// Go programming language for succinct and composable smart contracts.\n//\n// With transparent and timeless code, Gno.land is the next generation of smart\n// contract platforms, serving as the \"GitHub\" of the ecosystem, with realms built\n// using fully transparent, auditable code that anyone can inspect and reuse.\n//\n// Intuitive and easy to use, Gno.land lowers the barrier to web3 and makes\n// censorship-resistant platforms accessible to everyone. If you want to help lay\n// the foundations of a fairer and freer world, join us today.\n//\n// ---\n//\n// ## [Boards](/r/gnoland/boards2/v0) - 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//\n// Explore this ready-to-use Gno dApp, and experience decentralized social media in action.\n//\n// **[Open Boards](/r/gnoland/boards2/v0)**\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// No posts.\n//\n// \u003cgno-columns-sep\u003e\n//\n// ## [Latest Events](/events)\n//\n// No events.\n//\n// \u003c/gno-columns\u003e\n//\n// ---\n//\n// ## [Gno Playground](https://play.gno.land)\n//\n// Gno Playground is a web application designed for building, running, testing, and\n// interacting with your Gno code, enhancing your understanding of the Gno\n// language. With Gno Playground, you can share your code, execute tests, deploy\n// your realms and packages to Gno.land, and explore a multitude of other features.\n//\n// Experience 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//\n// All 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//\n// Official 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//\n// System-level realm packages used by the chain.\n//\n// [Browse](/r/sys)\n//\n// \u003cgno-columns-sep\u003e\n//\n// ### r/demo\n//\n// Demo realm packages showcasing what’s possible.\n//\n// [Browse](/r/demo)\n//\n// \u003cgno-columns-sep\u003e\n//\n// ### p/demo\n//\n// Pure 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.com/invite/gnoland)\n// - [Bubble Rumble](https://bubblerumble.net/)\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 #123\n//\n// \u003e Now, you Gno.\n//\n// \u003c/gno-columns\u003e\n//\n// ---\n//\n// ## Sign up for our newsletter\n//\n// Stay in the Gno by signing up for our newsletter. You'll get the scoop on dev updates, fresh content, and community news.\n//\n// [![Subscribe to stay in the Gno](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNTYiIGhlaWdodD0iNDQiIHZpZXdCb3g9IjAgMCAyNTYgNDQiPjxzdHlsZT50ZXh0e2ZvbnQtZmFtaWx5OnNhbnMtc2VyaWY7Zm9udC1zaXplOjE0cHg7dGV4dC1hbmNob3I6bWlkZGxlO2RvbWluYW50LWJhc2VsaW5lOm1pZGRsZTt9PC9zdHlsZT48cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMjU2IiBoZWlnaHQ9IjQ0IiByeD0iOCIgcnk9IjgiIGZpbGw9IiMyMjZjNTciIC8+PHRleHQgeD0iMTI4IiB5PSIyMiIgZHg9IjAiIGR5PSIwIiByb3RhdGU9IiIgZmlsbD0iI2ZmZmZmZiIgPlN1YnNjcmliZSB0byBzdGF5IGluIHRoZSBHbm88L3RleHQ+PC9zdmc+)](https://land.us18.list-manage.com/subscribe?u=8befe3303cf82796d2c1a1aff\u0026id=271812000b)\n//\n// ---\n//\n// **This is a testnet.** Package names are not guaranteed to be available for production.\n"},{"name":"override_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/home/filetests/override_filetest\n\npackage override_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/r/gnoland/home\"\n)\n\nfunc main(cur realm) {\n\tvar admin = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\")\n\ttesting.SetOriginCaller(admin)\n\thome.AdminSetOverride(cross(cur), \"Hello World!\")\n\tprintln(\"---\")\n\tprintln(home.Render(\"\"))\n\n\tnewAdmin := testutils.TestAddress(\"newAdmin\")\n\thome.AdminTransferOwnership(cross(cur), newAdmin)\n\tif err := revive(func() {\n\t\thome.AdminSetOverride(cross(cur), \"Not admin anymore\")\n\t}); err == nil {\n\t\tpanic(\"AdminSetOverride should have aborted the transaction\")\n\t}\n}\n\n// Output:\n// ---\n// Hello World!\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"FbKGLt/EwXMFWd8GPzrgFcZgcRDMCuM1e3cgkRovqohOmCGMWB+3vJjal/Dgp+4bYVGbuTEYAL1vmJG/HKWskw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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 wraps ugnot into a GRC20 token, one wugnot per ugnot.\n//\n// The token is a gno.land/p/nt/grc20/v1 token, so its public amounts are\n// uint256, and it is registered in gno.land/r/nt/grc20reg/v1. ugnot are chain\n// coins (int64), so the supply can never exceed math.MaxInt64: Deposit widens\n// the sent amount, and Withdraw range-checks the amount back down to int64.\npackage wugnot\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"chain/runtime\"\n\t\"chain/runtime/unsafe\"\n\t\"math\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/grc20/v1\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/nt/grc20reg/v1\"\n)\n\nvar (\n\tToken *grc20.Token\n\tadm   *grc20.PrivateLedger\n)\n\nconst (\n\tugnotMinDeposit  int64   = 1000\n\twugnotMinDeposit uint256 = 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\t// amount \u003e= ugnotMinDeposit \u003e 0, so widening to uint256 is exact.\n\tcheckErr(adm.Mint(caller, uint256(amount)))\n}\n\nfunc Withdraw(cur realm, amount uint256) {\n\truntime.AssertOriginCall()\n\trequire(amount \u003e= wugnotMinDeposit, \"Deposit below minimum: \"+fmtAmount(amount)+\"/\"+fmtAmount(wugnotMinDeposit)+\" wugnot.\")\n\n\tcaller := cur.Previous().Address()\n\tpkgaddr := cur.Address()\n\tcallerBal := Token.BalanceOf(caller)\n\trequire(amount \u003c= callerBal, \"Insufficient balance: \"+fmtAmount(callerBal)+\" available, \"+fmtAmount(amount)+\" needed.\")\n\t// Unreachable while the backing holds, and checked anyway so the int64\n\t// conversion below can never truncate: wugnot is minted only by Deposit,\n\t// for ugnot that has landed at this realm, and burned on every Withdraw,\n\t// so the supply never exceeds this realm's ugnot balance. That balance is\n\t// an int64 chain coin amount, so supply, and any single balance, is at most\n\t// math.MaxInt64, and the balance check above has already refused anything\n\t// larger.\n\trequire(amount \u003c= math.MaxInt64, \"Withdrawal exceeds the ugnot range: \"+fmtAmount(amount)+\" wugnot.\")\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 fmtAmount(balance)\n\tdefault:\n\t\treturn \"404\"\n\t}\n}\n\nfunc TotalSupply() uint256 {\n\treturn Token.TotalSupply()\n}\n\nfunc BalanceOf(owner address) uint256 {\n\treturn Token.BalanceOf(owner)\n}\n\nfunc Allowance(owner, spender address) uint256 {\n\treturn Token.Allowance(owner, spender)\n}\n\nfunc Transfer(cur realm, to address, amount uint256) {\n\tuserTeller := adm.CallerTeller()\n\tcheckErr(userTeller.Transfer(0, cur, to, amount))\n}\n\nfunc Approve(cur realm, spender address, amount uint256) {\n\tuserTeller := adm.CallerTeller()\n\tcheckErr(userTeller.Approve(0, cur, spender, amount))\n}\n\nfunc TransferFrom(cur realm, from, to address, amount uint256) {\n\tuserTeller := adm.CallerTeller()\n\tcheckErr(userTeller.TransferFrom(0, cur, from, to, amount))\n}\n\n// fmtAmount formats a wugnot amount in decimal (ufmt has no uint256 support).\nfunc fmtAmount(amount uint256) string {\n\treturn strconv.FormatUint256(amount, 10)\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"},{"name":"z0_filetest.gno","body":"// PKGPATH: gno.land/r/test\npackage test\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/r/gnoland/wugnot\"\n)\n\nvar (\n\taddr1 = testutils.TestAddress(\"test1\")\n\taddrc = chain.PackageAddress(\"gno.land/r/gnoland/wugnot\")\n)\n\nfunc main(cur realm) {\n\t// issue ugnots\n\ttesting.IssueCoins(addr1, chain.Coins{{\"ugnot\", 100000001}})\n\tprintBalances()\n\t// println(wugnot.Render(\"queues\"))\n\t// println(\"A -\", wugnot.Render(\"\"))\n\n\t// deposit of 123400ugnot from addr1\n\t// origin send must be simulated\n\tcoins := chain.Coins{{\"ugnot\", 123_400}}\n\ttesting.SetOriginCaller(addr1)\n\ttesting.SetOriginSend(coins)\n\tbanker.NewBanker(banker.BankerTypeRealmSend, cur).SendCoins(addr1, addrc, coins)\n\twugnot.Deposit(cross(cur))\n\tprintBalances()\n\n\t// withdraw of 4242ugnot to addr1\n\twugnot.Withdraw(cross(cur), 4242)\n\tprintBalances()\n}\n\nfunc printBalances() {\n\tprintSingleBalance := func(name string, addr address) {\n\t\twugnotBal := wugnot.BalanceOf(addr)\n\t\ttesting.SetOriginCaller(addr)\n\t\trobanker := banker.NewReadonlyBanker()\n\t\tcoins := robanker.GetCoins(addr).AmountOf(\"ugnot\")\n\t\tfmt.Printf(\"| %-13s | addr=%s | wugnot=%-6d | ugnot=%-9d |\\n\",\n\t\t\tname, addr, wugnotBal, coins)\n\t}\n\tprintln(\"-----------\")\n\tprintSingleBalance(\"wugnot\", addrc)\n\tprintSingleBalance(\"addr1\", addr1)\n\tprintln(\"-----------\")\n}\n\n// Output:\n// -----------\n// | wugnot        | addr=g15vj5q08amlvyd0nx6zjgcvwq2d0gt9fcchrvum | wugnot=0      | ugnot=0         |\n// | addr1         | addr=g1w3jhxap3ta047h6lta047h6lta047h6l4mfnm7 | wugnot=0      | ugnot=100000001 |\n// -----------\n// -----------\n// | wugnot        | addr=g15vj5q08amlvyd0nx6zjgcvwq2d0gt9fcchrvum | wugnot=0      | ugnot=123400    |\n// | addr1         | addr=g1w3jhxap3ta047h6lta047h6lta047h6l4mfnm7 | wugnot=123400 | ugnot=99876601  |\n// -----------\n// -----------\n// | wugnot        | addr=g15vj5q08amlvyd0nx6zjgcvwq2d0gt9fcchrvum | wugnot=0      | ugnot=119158    |\n// | addr1         | addr=g1w3jhxap3ta047h6lta047h6lta047h6l4mfnm7 | wugnot=119158 | ugnot=99880843  |\n// -----------\n"},{"name":"z1_filetest.gno","body":"// PKGPATH: gno.land/r/test\npackage test\n\n// Withdraw takes a uint256, but every wugnot comes from deposited ugnot, so an\n// amount beyond the int64 range can never be backed and is refused. It is\n// refused by the balance check: no account can hold more than MaxInt64 wugnot\n// (see Withdraw), so the int64 range check after it cannot be reached.\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/r/gnoland/wugnot\"\n)\n\nvar (\n\taddr1 = testutils.TestAddress(\"test1\")\n\taddrc = chain.PackageAddress(\"gno.land/r/gnoland/wugnot\")\n)\n\nfunc main(cur realm) {\n\ttesting.IssueCoins(addr1, chain.Coins{{\"ugnot\", 10_000}})\n\tcoins := chain.Coins{{\"ugnot\", 5_000}}\n\ttesting.SetOriginCaller(addr1)\n\ttesting.SetOriginSend(coins)\n\tbanker.NewBanker(banker.BankerTypeRealmSend, cur).SendCoins(addr1, addrc, coins)\n\twugnot.Deposit(cross(cur))\n\tprintln(\"balance:\", wugnot.BalanceOf(addr1))\n\tprintln(\"supply:\", wugnot.TotalSupply())\n\n\twugnot.Withdraw(cross(cur), 1\u003c\u003c64)\n}\n\n// Output:\n// balance: 5000\n// supply: 5000\n\n// Error:\n// Insufficient balance: 5000 available, 18446744073709551616 needed.\n"},{"name":"z2_filetest.gno","body":"// PKGPATH: gno.land/r/test\npackage test\n\n// A fully backed round trip at the int64 boundary: depositing MaxInt64 ugnot\n// mints MaxInt64 wugnot, the realm's ugnot backing equals the supply at every\n// step, and withdrawing all of it converts uint256 back to int64 exactly.\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"math\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/r/gnoland/wugnot\"\n)\n\nvar (\n\taddr1 = testutils.TestAddress(\"test1\")\n\taddrc = chain.PackageAddress(\"gno.land/r/gnoland/wugnot\")\n)\n\nfunc main(cur realm) {\n\ttesting.IssueCoins(addr1, chain.Coins{{\"ugnot\", math.MaxInt64}})\n\treport(\"start\")\n\n\tcoins := chain.Coins{{\"ugnot\", math.MaxInt64}}\n\ttesting.SetOriginCaller(addr1)\n\ttesting.SetOriginSend(coins)\n\tbanker.NewBanker(banker.BankerTypeRealmSend, cur).SendCoins(addr1, addrc, coins)\n\twugnot.Deposit(cross(cur))\n\treport(\"after deposit\")\n\n\twugnot.Withdraw(cross(cur), math.MaxInt64)\n\treport(\"after withdraw\")\n}\n\nfunc report(step string) {\n\trb := banker.NewReadonlyBanker()\n\tbacking := rb.GetCoin(addrc, \"ugnot\")\n\tsupply := wugnot.TotalSupply()\n\tprintln(step+\":\",\n\t\t\"user ugnot\", rb.GetCoin(addr1, \"ugnot\"),\n\t\t\"user wugnot\", wugnot.BalanceOf(addr1),\n\t\t\"backing\", backing,\n\t\t\"supply\", supply,\n\t\t\"backed\", uint256(backing) == supply)\n}\n\n// Output:\n// start: user ugnot 9223372036854775807 user wugnot 0 backing 0 supply 0 backed true\n// after deposit: user ugnot 0 user wugnot 9223372036854775807 backing 9223372036854775807 supply 9223372036854775807 backed true\n// after withdraw: user ugnot 9223372036854775807 user wugnot 0 backing 0 supply 0 backed true\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"cKGJLMoAO2DJZLyhBxJWQTUCEnqKZSYroyhVqK9uZxx0hlpXfPTd9xYLukt+BtPDHVM/dMoNWzrU4Vt8S5G3yw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1g73v2anukg4ej7axwqpthsatzrxjsh0wk797da","package":{"name":"memberstore","path":"gno.land/r/gov/dao/memberstore/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/gov/dao/memberstore/v0\"\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/moul/md/v0\"\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/mux/v0\"\n\t\"gno.land/p/nt/svg/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/memberstore/v0: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/memberstore/v0)\")\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 v0\"))\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":"memberstore_test.gno","body":"package memberstore\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestPower(t *testing.T) {\n\tms := NewMembersByTier()\n\taddMembers(ms, 100, T1)\n\taddMembers(ms, 100, T2)\n\taddMembers(ms, 100, T3)\n\n\ttiers.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tpo := value.(Tier).PowerHandler(ms, tiers)\n\t\tif key == T1 \u0026\u0026 po != 3.0 {\n\t\t\tt.Fatal(\"wrong value for T1\")\n\t\t}\n\t\tif key == T2 \u0026\u0026 po != 2.0 {\n\t\t\tt.Fatal(\"wrong value for T2\")\n\t\t}\n\t\tif key == T3 \u0026\u0026 po != 1.0 {\n\t\t\tt.Fatal(\"wrong value for T3\")\n\t\t}\n\n\t\treturn false\n\t})\n\n\tms = NewMembersByTier()\n\taddMembers(ms, 100, T1)\n\taddMembers(ms, 50, T2)\n\taddMembers(ms, 10, T3)\n\n\ttiers.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tpo := value.(Tier).PowerHandler(ms, tiers)\n\t\tif key == T1 \u0026\u0026 po != 3.0 {\n\t\t\tt.Fatal(\"wrong value for T1\")\n\t\t}\n\t\tif key == T2 \u0026\u0026 po != 2.0 {\n\t\t\tt.Fatal(\"wrong value for T2\")\n\t\t}\n\t\tif key == T3 \u0026\u0026 po != 1.0 {\n\t\t\tt.Fatal(\"wrong value for T3\")\n\t\t}\n\n\t\treturn false\n\t})\n\n\tms = NewMembersByTier()\n\taddMembers(ms, 100, T1)\n\taddMembers(ms, 200, T2)\n\taddMembers(ms, 100, T3)\n\n\ttiers.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tpo := value.(Tier).PowerHandler(ms, tiers)\n\t\tif key == T1 \u0026\u0026 po != 3.0 {\n\t\t\tt.Fatal(\"wrong value for T1\")\n\t\t}\n\t\tif key == T2 \u0026\u0026 po != 1.0 {\n\t\t\tt.Fatal(\"wrong value for T2\")\n\t\t}\n\t\tif key == T3 \u0026\u0026 po != 1.0 {\n\t\t\tt.Fatal(\"wrong value for T3\")\n\t\t}\n\n\t\treturn false\n\t})\n\n\tms = NewMembersByTier()\n\taddMembers(ms, 100, T1)\n\taddMembers(ms, 200, T2)\n\taddMembers(ms, 1000, T3)\n\n\ttiers.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tpo := value.(Tier).PowerHandler(ms, tiers)\n\t\tif key == T1 \u0026\u0026 po != 3.0 {\n\t\t\tt.Fatal(\"wrong value for T1\")\n\t\t}\n\t\tif key == T2 \u0026\u0026 po != 1.0 {\n\t\t\tt.Fatal(\"wrong value for T2\")\n\t\t}\n\t\tif key == T3 \u0026\u0026 po != 0.1 {\n\t\t\tt.Fatal(\"wrong value for T3\")\n\t\t}\n\n\t\treturn false\n\t})\n}\n\nfunc TestCreateMembers(t *testing.T) {\n\tms := NewMembersByTier()\n\tprintln(\"adding members...\")\n\taddMembers(ms, 10, \"T1\")\n\tprintln(\"added T1\")\n\taddMembers(ms, 100, \"T2\")\n\tprintln(\"added T2\")\n\taddMembers(ms, 1000, \"T3\")\n\tprintln(\"added T3\")\n\n\tm, tier := ms.GetMember(address(\"11T3\"))\n\turequire.Equal(t, \"T3\", tier)\n\n\tm, tier = ms.GetMember(address(\"2000T1\"))\n\turequire.Equal(t, \"\", tier)\n\tif m != nil {\n\t\tt.Fatal(\"member must be nil if not found\")\n\t}\n\n\ttier = ms.RemoveMember(address(\"1T1\"))\n\turequire.Equal(t, \"T1\", tier)\n}\n\nfunc addMembers(ms MembersByTier, c int, tier string) {\n\t// mt := avl.NewTree() XXX\n\tms.SetTier(tier)\n\tfor i := 0; i \u003c c; i++ {\n\t\taddr := address(strconv.Itoa(i) + tier)\n\t\tif err := ms.SetMember(tier, addr, \u0026Member{}); err != nil {\n\t\t\tpanic(err.Error())\n\t\t}\n\t}\n}\n\nfunc TestRenderMembersToleratesMalformedPath(t *testing.T) {\n\tms := NewMembersByTier()\n\taddMembers(ms, 3, T1)\n\t// A control byte makes url.Parse reject the path; RenderMembers must\n\t// degrade to the first page rather than nil-deref / panic (Render is an\n\t// unauthenticated, read-only path).\n\tout := RenderMembers(\"?\\x01\", ms)\n\turequire.True(t, len(out) \u003e 0, \"malformed path must render, not panic\")\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/v0\"\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/v0\"\n\t\"gno.land/p/nt/bptree/pager/v0\"\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/samcrew/tablesort/v0\"\n\t\"gno.land/p/samcrew/urlfilter/v0\"\n)\n\n// RenderMembers returns the members list with tier filters and pagination.\nfunc RenderMembers(path string, members MembersByTier) string {\n\tu, err := url.Parse(path)\n\tif err != nil {\n\t\t// A path url.Parse rejects (e.g. a control byte) is the caller's own\n\t\t// malformed input. Degrade to the unfiltered first page instead of\n\t\t// nil-derefing in ApplyFilters or panicking in MustGetPageByPath below\n\t\t// — this is a read-only, unauthenticated render.\n\t\tu, _ = url.Parse(\"\")\n\t\tpath = \"\"\n\t}\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/memberstore/v0\") + \"\\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"},{"name":"z0_filetest.gno","body":"// PKGPATH: gno.land/r/test/exploit\npackage exploit\n\nimport (\n\t\"gno.land/r/gov/dao/memberstore/v0\"\n)\n\nfunc main() {\n\t// After the fix, memberstore.Tiers is no longer accessible (lowercase 'tiers')\n\t// External realms can only use the safe accessor functions:\n\t// - memberstore.GetTier(name) - read-only tier access\n\t// - memberstore.IterateTiers(fn) - read-only iteration\n\t// - memberstore.GetTierPower(name, members) - calculated power\n\n\t// Verify we can still READ tier data via the safe accessor\n\tt3, ok := memberstore.GetTier(memberstore.T3)\n\tif !ok {\n\t\tpanic(\"T3 tier not found\")\n\t}\n\tprintln(\"T3 BasePower (read-only):\", t3.BasePower)\n\tprintln(\"T3 InvitationPoints (read-only):\", t3.InvitationPoints)\n\n\t// The following lines would cause a compile error if uncommented:\n\t// memberstore.Tiers.Set(...) // ERROR: Tiers is not exported (lowercase)\n\n\t// Iterate over tiers (read-only)\n\tprintln(\"All tiers:\")\n\tmemberstore.IterateTiers(func(name string, tier memberstore.Tier) bool {\n\t\tprintln(\"  -\", name, \"BasePower:\", tier.BasePower)\n\t\treturn false\n\t})\n\n\tprintln(\"Security fix verified: external realms cannot modify tiers\")\n}\n\n// Output:\n// T3 BasePower (read-only): 1\n// T3 InvitationPoints (read-only): 1\n// All tiers:\n//   - T1 BasePower: 3\n//   - T2 BasePower: 2\n//   - T3 BasePower: 1\n// Security fix verified: external realms cannot modify tiers\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"yicxnBrKkqdw5Q1bIz22wKEG9fTtPl5h5P1hp2/h16Q8Vl6UWf6VKglteh6DrwHqd8SZ36zhHpVlkpnMSSVFkg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"grc20reg","path":"gno.land/r/nt/grc20reg/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/nt/grc20reg/v0\"\ngno = \"0.9\"\n"},{"name":"grc20reg.gno","body":"package grc20reg\n\nimport (\n\t\"chain\"\n\t\"strings\"\n\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/nt/avl/rotree/v0\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/fqname/v0\"\n\t\"gno.land/p/nt/grc20/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// Write wrappers: a registered token can be moved through the registry without\n// importing the token's realm, which is the point of a registry. What makes\n// that safe is the calling convention, so it is worth stating once here rather\n// than three times below.\n//\n// These are NOT crossing functions. `_ int, rlm realm` is the only shape that\n// gives a non-crossing realm parameter — a realm parameter in first position\n// must be named `cur`, which makes the function crossing — and the distinction\n// is load-bearing, not stylistic:\n//\n//   - Crossing (`func Transfer(cur realm, …)`) mints a fresh `cur` for THIS\n//     realm. RealmTeller would then bind the actor to the registry's own\n//     address and the registry would spend its own balance. Useless at best.\n//   - Non-crossing (`func Transfer(_ int, rlm realm, …)`) declaring-borrows to\n//     the registry without a realm-context change, so `rlm` is still the\n//     caller's own live token and the actor is the caller.\n//\n// The safety comes from RealmTeller's IsCurrent() assertion. The actor is\n// rlm.Address() on a token that must be the live crossing frame, so it is\n// provably the immediate caller: a stale or foreign token is refused with\n// ErrSpoofedRealm. Debiting anyone else would mean holding their live `cur`,\n// which means executing inside their frame — authority they handed over\n// deliberately, and the same trust model RealmTeller already carries.\n//\n// This is deliberately not grc20.CallerTeller. \"Act as whoever called me\" is\n// the confused deputy: the debited account ends up chosen by whoever the hub\n// can be induced to serve, and an intermediate realm frame silently changes who\n// pays. CallerTeller is confined to the token's own realm for that reason and\n// is not reachable from a *Token. \"Act as the realm that called me, verified\n// current\" has nothing to induce — the caller cannot name a victim, only\n// itself.\n//\n// Realm-only by construction: MsgCall cannot build a realm argument\n// (convertArgToGno rejects non-primitive parameter types), so a signing user\n// cannot reach these at all and there is no in-band case to guard against.\n// Users move their own tokens through the token realm's own entry points\n// (wugnot.Transfer, foo20.Transfer, …).\n\n// Transfer moves `amount` out of the CALLING REALM's own balance.\n//\n// Call it non-crossing, forwarding your own `cur`:\n//\n//\tgrc20reg.Transfer(0, cur, \"gno.land/r/demo/defi/foo20.FOO\", to, 100)\nfunc Transfer(_ int, rlm realm, tokenKey string, to address, amount int64) {\n\tcheckErr(MustGet(tokenKey).RealmTeller(0, rlm).Transfer(0, rlm, to, amount))\n}\n\n// Approve sets an allowance owned by the CALLING REALM, letting `spender` draw\n// on the calling realm's balance. It does not touch the signing user's\n// allowances.\nfunc Approve(_ int, rlm realm, tokenKey string, spender address, amount int64) {\n\tcheckErr(MustGet(tokenKey).RealmTeller(0, rlm).Approve(0, rlm, spender, amount))\n}\n\n// TransferFrom spends an allowance with the CALLING REALM as the spender.\n//\n// Note the allowance direction this implies: `from` must have approved the\n// calling realm, not the signing user. That is the supported way for a realm to\n// move a user's funds — the user grants the realm an allowance, and the realm\n// draws on it as itself, so every debit is one the owner authorized against\n// that specific realm.\nfunc TransferFrom(_ int, rlm realm, tokenKey string, from, to address, amount int64) {\n\tcheckErr(MustGet(tokenKey).RealmTeller(0, rlm).TransferFrom(0, rlm, from, to, amount))\n}\n\nfunc checkErr(err error) {\n\tif err != nil {\n\t\tpanic(err)\n\t}\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/nt/grc20reg/v0:\" + 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\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"},{"name":"grc20reg_test.gno","body":"package grc20reg\n\nimport (\n\t\"chain\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/grc20/v0\"\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestRegistry(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/demo/foo\"))\n\ttoken, ledger := grc20.NewToken(\"TestToken\", \"TST\", 4, 0, cur)\n\tledger.Mint(cur.Address(), 1234567)\n\t// register\n\tkey := Register(cross(cur), token, \"mySlug\")\n\tregToken := Get(key)\n\turequire.True(t, regToken != nil, \"expected to find a token\") // fixme: use urequire.NotNil\n\turequire.Equal(t, regToken.GetSymbol(), \"TST\")\n\n\texpected := `- **TestToken** - [gno.land/r/demo/foo](/r/demo/foo).TST - [info](/r/nt/grc20reg/v0:gno.land/r/demo/foo.TST)\n`\n\tgot := Render(\"\")\n\turequire.True(t, strings.Contains(got, expected))\n\t// 404\n\tinvalidToken := Get(\"0xdeadbeef\")\n\turequire.True(t, invalidToken == nil)\n\n\tgot = Render(\"\")\n\turequire.True(t, strings.Contains(got, expected))\n\n\texpected = `# TestToken\n- symbol: **TST**\n- realm: [gno.land/r/demo/foo](/r/demo/foo).TST\n- decimals: 4\n- total supply: 1234567\n`\n\tgot = Render(key)\n\turequire.Equal(t, expected, got)\n\n\t// The registry keys by rlmPath.symbol, so a second token with the same\n\t// symbol in the same realm is rejected even though its Token.ID() differs\n\t// (distinct trailing sequence id). See TestRegisterRejectsOverwrite.\n\tsecond, _ := grc20.NewToken(\"Second\", \"TST\", 4, 1, cur)\n\turequire.NotEqual(t, token.ID(), second.ID()) // ids are decoupled from symbol\n\turequire.AbortsContains(t, cur, \"token already registered\", func() {\n\t\tRegister(cross(cur), second, \"\")\n\t})\n}\n\n// TestRegistryLookupGrantsNoSpendAuthority pins the property that replaced the\n// former Transfer/Approve/TransferFrom wrappers: a registry lookup yields a\n// *Token, and a *Token alone carries no authority to debit anybody. The\n// frame-relative teller is reachable only from the ledger, which never leaves\n// the token's own realm, so this realm cannot act on behalf of its caller —\n// that shape was the confused deputy.\n//\n// The supported route for a realm that must move a user's funds is the one\n// exercised below: the user grants an allowance, and the realm spends it as\n// itself through a RealmTeller, which is eagerly bound to its own address.\nfunc TestRegistryLookupGrantsNoSpendAuthority(cur realm, t *testing.T) {\n\tconst (\n\t\ttokenPath    = \"gno.land/r/demo/token\"\n\t\tconsumerPath = \"gno.land/r/demo/grc20reg_consumer\"\n\t)\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\tconsumer := chain.PackageAddress(consumerPath)\n\n\ttesting.SetRealm(testing.NewCodeRealm(tokenPath))\n\ttoken, ledger := grc20.NewToken(\"TestToken\", \"TST\", 4, 0, cur)\n\turequire.NoError(t, ledger.Mint(alice, 1_000))\n\ttokenKey := Register(cross(cur), token, \"\")\n\tuassert.Equal(t, \"TestToken\", MustGet(tokenKey).GetName())\n\tuassert.Equal(t, int64(1_000), MustGet(tokenKey).BalanceOf(alice))\n\tuassert.Equal(t, int64(0), MustGet(tokenKey).Allowance(alice, consumer))\n\n\t// alice approves the consumer realm on the token. On chain she does this\n\t// through the token realm's own entry point; the ledger stands in for it.\n\turequire.NoError(t, ledger.ImpersonateTeller(alice).Approve(0, cur, consumer, 300))\n\tuassert.Equal(t, int64(300), MustGet(tokenKey).Allowance(alice, consumer))\n\n\t// The consumer spends that allowance as ITSELF, over a token it found in\n\t// the registry and does not own. The actor is fixed at construction, so it\n\t// cannot be redirected by whoever calls in.\n\ttesting.SetRealm(testing.NewCodeRealm(consumerPath))\n\tspender := MustGet(tokenKey).RealmTeller(0, cur)\n\turequire.NoError(t, spender.TransferFrom(0, cur, alice, bob, 200))\n\tuassert.Equal(t, int64(800), MustGet(tokenKey).BalanceOf(alice))\n\tuassert.Equal(t, int64(200), MustGet(tokenKey).BalanceOf(bob))\n\tuassert.Equal(t, int64(100), MustGet(tokenKey).Allowance(alice, consumer))\n\n\t// Beyond the allowance it stops: the balance is not reachable directly.\n\tuassert.ErrorContains(t,\n\t\tspender.TransferFrom(0, cur, alice, bob, 101),\n\t\t\"insufficient allowance\")\n\tuassert.Equal(t, int64(800), MustGet(tokenKey).BalanceOf(alice))\n\tuassert.Equal(t, int64(200), MustGet(tokenKey).BalanceOf(bob))\n}\n\n// TestRegistryConsumerCannotRedirectItsActor is the negative half of the\n// property above: a realm holding a registered token pays out of its own\n// balance, not the signing user's. The actor is bound at construction, so the\n// transaction's origin caller has no bearing on who is debited — which is what\n// made the frame-relative teller in a foreign realm a phishing primitive.\nfunc TestRegistryConsumerCannotRedirectItsActor(cur realm, t *testing.T) {\n\tconst (\n\t\ttokenPath    = \"gno.land/r/demo/token_actor_binding\"\n\t\tconsumerPath = \"gno.land/r/demo/grc20reg_actor_consumer\"\n\t)\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\tconsumer := chain.PackageAddress(consumerPath)\n\n\ttesting.SetRealm(testing.NewCodeRealm(tokenPath))\n\ttoken, ledger := grc20.NewToken(\"ActorBinding\", \"ACTB\", 4, 0, cur)\n\turequire.NoError(t, ledger.Mint(alice, 1_000))\n\turequire.NoError(t, ledger.Mint(consumer, 500))\n\ttokenKey := Register(cross(cur), token, \"\")\n\n\t// alice signs the transaction, and holds a balance the consumer would love\n\t// to spend. The consumer pays out of its own instead.\n\ttesting.SetOriginCaller(alice)\n\ttesting.SetRealm(testing.NewCodeRealm(consumerPath))\n\turequire.NoError(t, MustGet(tokenKey).RealmTeller(0, cur).Transfer(0, cur, bob, 100))\n\tuassert.Equal(t, int64(1_000), MustGet(tokenKey).BalanceOf(alice))\n\tuassert.Equal(t, int64(400), MustGet(tokenKey).BalanceOf(consumer))\n\tuassert.Equal(t, int64(100), MustGet(tokenKey).BalanceOf(bob))\n}\n\nfunc TestRegisterRejectsOverwrite(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/demo/grc20reg_overwrite\"))\n\ttoken, ledger := grc20.NewToken(\"Bar\", \"BAR\", 4, 0, cur)\n\tledger.Mint(cur.Address(), 11)\n\tkey := Register(cross(cur), token, \"\")\n\n\turequire.Equal(t, \"BAR\", Get(key).GetSymbol())\n\turequire.Equal(t, int64(11), Get(key).BalanceOf(cur.Address()))\n\n\treplacement, _ := grc20.NewToken(\"Replacement\", \"BAR\", 6, 0, cur)\n\turequire.AbortsContains(t, cur, \"token already registered\", func() {\n\t\tRegister(cross(cur), replacement, \"\")\n\t})\n}\n\nfunc TestRegisterRejectsAliasedTokenPaths(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/demo/grc20reg_alias\"))\n\ttoken, _ := grc20.NewToken(\"Aliased Token\", \"ALIAS\", 4, 0, cur)\n\tRegister(cross(cur), token, \"first\")\n\n\turequire.AbortsContains(t, cur, \"token already registered\", func() {\n\t\tRegister(cross(cur), token, \"second\")\n\t})\n}\n\nfunc TestRegisterRejectsTokenFromDifferentRealm(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/demo/grc20reg_id_source\"))\n\ttoken, _ := grc20.NewToken(\"Mismatch Token\", \"MISMATCH\", 4, 0, cur)\n\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/demo/grc20reg_id_target\"))\n\turequire.AbortsContains(t, cur, \"token must be registered from its own realm\", func() {\n\t\tRegister(cross(cur), token, \"\")\n\t})\n}\n\nfunc TestValidateSlug(cur realm, t *testing.T) {\n\t// Valid slugs — should not panic\n\tvalid := []string{\"mytoken\", \"my-token\", \"my_token\", \"Token123\", \"a\", \"A-B_c\", strings.Repeat(\"a\", maxSlugLen)}\n\tfor _, slug := range valid {\n\t\tvalidateSlug(slug) // no panic = pass\n\t}\n}\n\nfunc TestValidateSlugPanicsOnTooLong(cur realm, t *testing.T) {\n\tdefer func() { recover() }()\n\tvalidateSlug(strings.Repeat(\"a\", maxSlugLen+1))\n\tt.Errorf(\"should have panicked\")\n}\n\nfunc TestValidateSlugPanicsOnSpace(cur realm, t *testing.T) {\n\tdefer func() { recover() }()\n\tvalidateSlug(\"has space\")\n\tt.Errorf(\"should have panicked\")\n}\n\nfunc TestValidateSlugPanicsOnDot(cur realm, t *testing.T) {\n\tdefer func() { recover() }()\n\tvalidateSlug(\"has.dot\")\n\tt.Errorf(\"should have panicked\")\n}\n\nfunc TestValidateSlugPanicsOnSlash(cur realm, t *testing.T) {\n\tdefer func() { recover() }()\n\tvalidateSlug(\"has/slash\")\n\tt.Errorf(\"should have panicked\")\n}\n\nfunc TestValidateSlugPanicsOnBrackets(cur realm, t *testing.T) {\n\tdefer func() { recover() }()\n\tvalidateSlug(\"[brackets]\")\n\tt.Errorf(\"should have panicked\")\n}\n\nfunc TestValidateSlugPanicsOnParens(cur realm, t *testing.T) {\n\tdefer func() { recover() }()\n\tvalidateSlug(\"(parens)\")\n\tt.Errorf(\"should have panicked\")\n}\n\nfunc TestValidateSlugPanicsOnInjection(cur realm, t *testing.T) {\n\tdefer func() { recover() }()\n\tvalidateSlug(`) [Claim](https://evil.com`)\n\tt.Errorf(\"should have panicked\")\n}\n\nfunc TestRegisterRejectsNilToken(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/demo/grc20reg_nil\"))\n\turequire.AbortsContains(t, cur, \"nil token\", func() {\n\t\tRegister(cross(cur), nil, \"\")\n\t})\n}\n\n// TestWrappersBindActorToCallingRealm pins the property that makes the write\n// wrappers safe: they are non-crossing, so `rlm` reaches RealmTeller as the\n// caller's own live token and the debit lands on the caller. A crossing wrapper\n// would mint a fresh `cur` for grc20reg and spend the registry's balance\n// instead, which is why the convention is not a stylistic choice.\n//\n// Both the consumer and the registry hold a balance, so whichever one is\n// debited is observable rather than inferred.\nfunc TestWrappersBindActorToCallingRealm(cur realm, t *testing.T) {\n\tconst (\n\t\ttokenPath    = \"gno.land/r/demo/token_wrapper_actor\"\n\t\tconsumerPath = \"gno.land/r/demo/grc20reg_wrapper_consumer\"\n\t)\n\tbob := testutils.TestAddress(\"bob\")\n\tconsumer := chain.PackageAddress(consumerPath)\n\tregistry := chain.PackageAddress(\"gno.land/r/nt/grc20reg/v0\")\n\n\ttesting.SetRealm(testing.NewCodeRealm(tokenPath))\n\ttoken, ledger := grc20.NewToken(\"WrapperActor\", \"WRPA\", 4, 0, cur)\n\turequire.NoError(t, ledger.Mint(consumer, 1_000))\n\turequire.NoError(t, ledger.Mint(registry, 1_000))\n\ttokenKey := Register(cross(cur), token, \"\")\n\n\ttesting.SetRealm(testing.NewCodeRealm(consumerPath))\n\tTransfer(0, cur, tokenKey, bob, 100)\n\n\tuassert.Equal(t, int64(900), MustGet(tokenKey).BalanceOf(consumer))\n\tuassert.Equal(t, int64(1_000), MustGet(tokenKey).BalanceOf(registry))\n\tuassert.Equal(t, int64(100), MustGet(tokenKey).BalanceOf(bob))\n}\n\n// TestWrapperActorIgnoresSigningUser is the negative half: the signing user has\n// a balance the calling realm would like to spend, and does not lose it. The\n// actor comes from rlm.Address(), so the transaction's origin has no say in who\n// pays — a hub cannot be induced to debit its caller's caller.\nfunc TestWrapperActorIgnoresSigningUser(cur realm, t *testing.T) {\n\tconst (\n\t\ttokenPath    = \"gno.land/r/demo/token_wrapper_origin\"\n\t\tconsumerPath = \"gno.land/r/demo/grc20reg_wrapper_origin_consumer\"\n\t)\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\tconsumer := chain.PackageAddress(consumerPath)\n\n\ttesting.SetRealm(testing.NewCodeRealm(tokenPath))\n\ttoken, ledger := grc20.NewToken(\"WrapperOrigin\", \"WRPO\", 4, 0, cur)\n\turequire.NoError(t, ledger.Mint(alice, 1_000))\n\turequire.NoError(t, ledger.Mint(consumer, 500))\n\ttokenKey := Register(cross(cur), token, \"\")\n\n\ttesting.SetOriginCaller(alice)\n\ttesting.SetRealm(testing.NewCodeRealm(consumerPath))\n\tTransfer(0, cur, tokenKey, bob, 100)\n\n\tuassert.Equal(t, int64(1_000), MustGet(tokenKey).BalanceOf(alice))\n\tuassert.Equal(t, int64(400), MustGet(tokenKey).BalanceOf(consumer))\n}\n\n// TestTransferFromSpendsAllowanceGrantedToCallingRealm records the semantic\n// shift that comes with binding the actor to the caller: the owner must have\n// approved the CALLING REALM, not the signing user. An allowance granted to\n// anyone else does not authorize the wrapper.\nfunc TestTransferFromSpendsAllowanceGrantedToCallingRealm(cur realm, t *testing.T) {\n\tconst (\n\t\ttokenPath    = \"gno.land/r/demo/token_wrapper_allowance\"\n\t\tconsumerPath = \"gno.land/r/demo/grc20reg_wrapper_allowance_consumer\"\n\t)\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\tconsumer := chain.PackageAddress(consumerPath)\n\n\ttesting.SetRealm(testing.NewCodeRealm(tokenPath))\n\ttoken, ledger := grc20.NewToken(\"WrapperAllowance\", \"WRPL\", 4, 0, cur)\n\turequire.NoError(t, ledger.Mint(alice, 1_000))\n\ttokenKey := Register(cross(cur), token, \"\")\n\n\t// No allowance yet: the wrapper cannot touch alice's balance.\n\ttesting.SetRealm(testing.NewCodeRealm(consumerPath))\n\tuassert.PanicsContains(t, cur, \"insufficient allowance\", func() {\n\t\tTransferFrom(0, cur, tokenKey, alice, bob, 100)\n\t})\n\tuassert.Equal(t, int64(1_000), MustGet(tokenKey).BalanceOf(alice))\n\n\t// alice approves the consuming realm itself, and only then does it work.\n\turequire.NoError(t, ledger.Approve(alice, consumer, 100))\n\n\ttesting.SetRealm(testing.NewCodeRealm(consumerPath))\n\tTransferFrom(0, cur, tokenKey, alice, bob, 100)\n\tuassert.Equal(t, int64(900), MustGet(tokenKey).BalanceOf(alice))\n\tuassert.Equal(t, int64(100), MustGet(tokenKey).BalanceOf(bob))\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"ckaGxnVfe4xyQz0ynAHLdb9KZ5OwGokogtfFKrdKD3oOyt0LK5XwQU7K7aE1xehYuC9K3DPy8rQp/i94Cvfj3w=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"treasury","path":"gno.land/r/gov/dao/treasury/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/gov/dao/treasury/v0\"\ngno = \"0.9\"\n"},{"name":"treasury.gno","body":"package treasury\n\nimport (\n\t\"chain/banker\"\n\n\t\"gno.land/p/nt/grc20/v0\"\n\tt \"gno.land/p/nt/treasury/v0\"\n\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/nt/grc20reg/v0\"\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/treasury/v0:.../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\t// Crossing function, so cur is a live minted cur and this is redundant\n\t// today; kept to match the documented rule that caller identity is\n\t// derived from cur.Previous() only under an IsCurrent() guard.\n\tif !cur.IsCurrent() {\n\t\tpanic(\"realm value is not the caller's live cur\")\n\t}\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\t// Copied, not aliased: the grc20Lister closure re-reads tokenKeys on every\n\t// Balances() and every GRC20 payment, so storing the caller's array as\n\t// given would leave it a permanent write handle on this realm's token set.\n\ttokenKeys = make([]string, len(keys))\n\tcopy(tokenKeys, keys)\n}\n\n// Send sends a payment using the treasury instance.\nfunc Send(cur realm, payment t.Payment) {\n\t// See SetTokenKeys: redundant under the crossing-function guarantee, kept\n\t// for conformance with the IsCurrent()-before-Previous() rule.\n\tif !cur.IsCurrent() {\n\t\tpanic(\"realm value is not the caller's live cur\")\n\t}\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"TxkMCa01HukRqAob/fwolztx4m/lILIgVFW5uzmZ91pYmTT/ixbMOry35ZEw3L2wdk+ABvDo3T+hoPfsKydG1w=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1g73v2anukg4ej7axwqpthsatzrxjsh0wk797da","package":{"name":"impl","path":"gno.land/r/gov/dao/impl/v0","files":[{"name":"clamp.gno","body":"package impl\n\n// Attacker-controlled strings are clamped before they reach the sanitizer, so\n// escaping costs a bounded constant rather than scaling with input.\n//\n// Sanitizing runs ~11,310 gas/byte (InlineCode) against ~31 for the raw\n// concatenation it replaced, and Render is reachable unauthenticated through\n// vm/qrender under maxGasQuery = 3_000_000_000. ExecutorCreationRealm is\n// dispatched through the public dao.Executor interface, so a hostile executor\n// computes it per call while storing almost nothing. Measured on the real\n// render path: a 250KB value costs 2,839,117,770 gas unclamped against\n// 18,749,984 clamped. 250KB is just under the cap; about 265KB crosses it\n// (3,008,818,394), and past that the page cannot be rendered at all by\n// anyone. Choosing the larger number costs the attacker nothing, since the\n// value is computed per call and almost nothing is stored. Removing that\n// amplification is the point of this file.\n//\n// It does NOT make the page safe, and nothing here should be read as claiming\n// so. The executor's method body runs inside the same query and is unbounded:\n// an executor that simply burns CPU before returning a short string still\n// renders the proposal page permanently un-queryable, for a few hundred bytes\n// of on-chain storage. That predates this change — Render has always called\n// ExecutorString() and ExecutorCreationRealm() through the public interface —\n// and bounding it needs a gas budget around executor dispatch, not a clamp.\nconst (\n\t// A realm path. The longest deployable realm path in examples/ is 35 bytes\n\t// (gno.land/r/gov/dao/treasury/test/v0), so this leaves about seven times\n\t// the room anything real needs. A hostile executor returns any length it\n\t// likes, which is the reason to clamp at all. It is headroom, not a\n\t// guarantee: the package-path grammar puts no ceiling on how many segments\n\t// a path may have, so a deeply nested realm could still be cut. That costs\n\t// a truncation marker on the page and nothing else.\n\tmaxRenderedRealm = 256\n\t// \"execution failed: \" plus an executor's error message. Also bounds what\n\t// govdao.gno stores, so the realm never holds a reason it cannot show.\n\tmaxRenderedReason = 1024\n\t// A strconv error that echoes the caller's own path segment. Everything in\n\t// it is fixed text except the quoted segment, whose length the caller picks.\n\tmaxRenderedError = 256\n\t// A proposal title. The longest in examples/ is about 40 bytes, so this is\n\t// roughly nine times anything real, and long for a heading. Titles are\n\t// escaped on both the proposal page and the list page, and the list page\n\t// escapes one per proposal shown, so this is the bound that keeps a single\n\t// oversized title from pricing the whole list out of the query cap.\n\tmaxRenderedTitle = 512\n\t// A Payment's String(). The canonical impls render a coin set or an\n\t// amount plus a token key, then \" to \" and a bech32 address; a coin set\n\t// is bounded by the number of denoms a proposer cares to list and each\n\t// denom's length is unbounded, which is the reason to clamp at all.\n\tmaxRenderedPayment = 512\n)\n\n// Always clamp first — before escaping, and before any other pass over the\n// value. Trimming used to run before the clamp, which meant it walked every\n// byte the executor returned: 250KB of spaces cost 1,368,719,824 gas to render\n// nothing, and 560KB cost 3,048,904,520, past the query cap. Clamping first\n// bounds that scan and brought the same 560KB down to 16,657,792.\n//\n// Never clamp after escaping either. The escapers size their wrapper\n// from the string they are handed — InlineCode picks a fence long enough to\n// outscan the backticks it can see. Cutting a value that has already been\n// escaped can slice the closing fence off and leave the span hanging open,\n// which is worse than not clamping at all. Both call sites read\n// InlineCode(clampField(...)) for that reason, and the enormous-value case in\n// filetests/executor_disclosure_filetest.gno fails if the two are swapped.\n//\n// clampField cuts s to at most max bytes, backing off to a rune boundary so a\n// well-formed multi-byte character is not split, and marks the result so a\n// reader can tell it was cut. Input that is already invalid UTF-8 can still\n// leave a dangling lead byte; the sanitizer tolerates that. The marker avoids markdown punctuation:\n// these values are escaped downstream, and parentheses would come back as\n// \"\\(truncated\\)\".\nfunc clampField(s string, max int) string {\n\tif len(s) \u003c= max {\n\t\treturn s\n\t}\n\n\tend := max\n\t// UTF-8 continuation bytes are 0b10xxxxxx.\n\tfor end \u003e 0 \u0026\u0026 s[end]\u00260xC0 == 0x80 {\n\t\tend--\n\t}\n\n\treturn s[:end] + \"… truncated\"\n}\n"},{"name":"clamp_test.gno","body":"package impl\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\n// Sanitizing costs ~11,310 gas/byte, and rendering is reachable unauthenticated\n// through vm/qrender under a 3,000,000,000 gas cap. ExecutorCreationRealm is\n// dispatched through the public dao.Executor interface, so a hostile executor\n// computes it per call and stores almost nothing: unclamped, a ~250KB value\n// costs over 3G gas in one render and bricks the proposal page for everyone.\n// Measured on the real render path: 2,839,117,770 gas unclamped versus\n// 18,749,984 clamped. About 265KB of input crosses the query cap entirely.\nfunc TestClampFieldBoundsSanitizerInput(t *testing.T) {\n\tuassert.Equal(t, \"short\", clampField(\"short\", maxRenderedRealm),\n\t\t\"a value within the bound must pass through untouched\")\n\n\thuge := strings.Repeat(\"a\", 250000)\n\tgot := clampField(huge, maxRenderedRealm)\n\tuassert.True(t, len(got) \u003c maxRenderedRealm+32,\n\t\t\"the clamped value must be bounded by the limit, not by the input\")\n\tuassert.True(t, strings.HasSuffix(got, \"… truncated\"),\n\t\t\"a clamped value must say it was cut rather than look authored short\")\n}\n\n// Cutting at a byte offset can land inside a multi-byte rune; the sanitizer\n// tolerates invalid UTF-8, but handing it a split rune is sloppy and would\n// render a replacement character mid-path.\nfunc TestClampFieldCutsOnRuneBoundary(t *testing.T) {\n\t// 3-byte runes, so a 256-byte cut lands mid-rune (256 = 85*3 + 1).\n\tgot := clampField(strings.Repeat(\"世\", 200), maxRenderedRealm)\n\tbody := strings.TrimSuffix(got, \"… truncated\")\n\n\tuassert.True(t, len(body)%3 == 0,\n\t\t\"the cut must land on a rune boundary\")\n\tuassert.Equal(t, strings.Repeat(\"世\", len(body)/3), body,\n\t\t\"every retained rune must be intact\")\n}\n\n// Boundary and malformed input. clampField does byte arithmetic and backs off\n// over UTF-8 continuation bytes, so the interesting cases are the ones where\n// that loop has nowhere to back off to.\nfunc TestClampFieldBoundaries(t *testing.T) {\n\texact := strings.Repeat(\"a\", maxRenderedRealm)\n\tuassert.Equal(t, exact, clampField(exact, maxRenderedRealm),\n\t\t\"a value exactly at the bound must not be marked truncated\")\n\n\tover := strings.Repeat(\"a\", maxRenderedRealm+1)\n\tuassert.True(t, strings.HasSuffix(clampField(over, maxRenderedRealm), \"… truncated\"),\n\t\t\"one byte over the bound must be cut\")\n\n\t// Already-invalid UTF-8: every byte is a continuation, so the backoff\n\t// walks to zero. Must degrade to the marker rather than panic or loop.\n\tcont := strings.Repeat(\"\\x80\", maxRenderedRealm+10)\n\tuassert.Equal(t, \"… truncated\", clampField(cont, maxRenderedRealm),\n\t\t\"an all-continuation string must degrade to just the marker\")\n\n\tuassert.Equal(t, \"… truncated\", clampField(\"abc\", 0),\n\t\t\"a zero bound must still terminate\")\n}\n"},{"name":"denied_reason_test.gno","body":"package impl\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\n)\n\n// DeniedReason is \"execution failed: \" + err.Error() (govdao.gno), and that\n// error comes from the proposal's executor callback — which any third-party\n// realm can supply (see govdao_execute_reject_proposal.txtar, where the\n// executor lives in gno.land/r/test/request). Rendered raw it let a failing\n// proposal write markdown straight beneath the \"PROPOSAL HAS BEEN DENIED\"\n// line, including a forged heading and vote tally.\n//\n// Operates on proposalStatus.String directly rather than through a real\n// proposal, so it does not disturb the proposal ids and counts that the other\n// tests in this package assert against shared realm state.\nfunc TestDeniedReasonCannotForgePageStructure(cur realm, t *testing.T) {\n\tps := newProposalStatus([]string{memberstore.T1})\n\tps.Denied = true\n\tps.DeniedReason = \"execution failed: boom\\n\\n### Stats\\n\\n- **PROPOSAL HAS BEEN ACCEPTED**\\n\\n---\\n\u003cdiv\u003eswallow the tally\"\n\n\tout := ps.String(0, cur)\n\n\tuassert.False(t, strings.Contains(out, \"\\n### Stats\"),\n\t\t\"an injected heading must not survive into the rendered stats block\")\n\tuassert.False(t, strings.Contains(out, \"\\n- **PROPOSAL HAS BEEN ACCEPTED**\"),\n\t\t\"an injected list item must not forge an acceptance line\")\n\tuassert.False(t, strings.Contains(out, \"\\n---\\n\"),\n\t\t\"an injected horizontal rule must not survive\")\n\n\t// Emphasis and raw HTML are residue that sanitize.Block would have left\n\t// live; InlineText escapes both. A reader must not see a bold\n\t// \"ACCEPTED\" on a denied proposal, and a \u003cdiv\u003e must not open an HTML\n\t// block that swallows the vote tally rendered after it.\n\tuassert.False(t, strings.Contains(out, \"**PROPOSAL HAS BEEN ACCEPTED**\"),\n\t\t\"injected bold text must not forge an acceptance line\")\n\tuassert.False(t, strings.Contains(out, \"\u003cdiv\u003e\"),\n\t\t\"injected raw HTML must not open a block that swallows the tally\")\n\n\t// Folding is the property InlineText adds over the alternatives: the whole\n\t// reason must land on the REASON: line, so nothing after a newline can be\n\t// read as new top-level markdown. Asserted by requiring the payload's LAST\n\t// segment on the same line as its first.\n\treason := out[strings.Index(out, \"REASON: \"):]\n\tfirstLine := reason[:strings.Index(reason, \"\\n\")]\n\tuassert.True(t, strings.Contains(firstLine, \"swallow the tally\"),\n\t\t\"the entire reason must be folded onto the REASON line\")\n}\n\n// ...and ordinary error text stays readable. InlineText backslash-escapes\n// markdown punctuation, so the raw output carries \"Boom\\!\" — but a\n// backslash-escaped punctuation mark renders as the bare character, so a\n// reader sees \"Boom!\". Words and spacing are untouched.\nfunc TestDeniedReasonKeepsPlainTextIntact(cur realm, t *testing.T) {\n\tps := newProposalStatus([]string{memberstore.T1})\n\tps.Denied = true\n\tps.DeniedReason = \"execution failed: Boom!\"\n\n\tuassert.True(t, strings.Contains(ps.String(0, cur), `REASON: execution failed: Boom\\!`),\n\t\t\"a plain denial reason must stay on the REASON line, escaped but readable\")\n}\n\n// The reason reaches an unauthenticated render path and InlineText costs\n// ~6,990 gas/byte, so it must be bounded before it is escaped, not after.\n// Asserts the clamp at the call site, not clampField in isolation.\n//\n// The payload is exclamation marks, not letters, and that choice is the whole\n// test. InlineText escapes \"!\" to \"\\!\" but leaves letters alone, so with a\n// letter payload the escaped text is the same length as the raw text and both\n// orderings produce identical output — the test would pass either way and\n// prove nothing. With punctuation, escaping doubles the length, and the two\n// orderings become tellable apart.\nfunc TestDeniedReasonIsClampedBeforeSanitizing(cur realm, t *testing.T) {\n\tps := newProposalStatus([]string{memberstore.T1})\n\tps.Denied = true\n\tps.DeniedReason = strings.Repeat(\"!\", 50000)\n\n\tout := ps.String(0, cur)\n\n\t// The clamp bounds what goes INTO the escaper, so a full maxRenderedReason\n\t// characters survive and the escaped output is about twice that. Clamping\n\t// the escaped text instead would leave only half as many, and the escaper\n\t// would still have processed all 50,000 characters — exactly the cost the\n\t// clamp exists to avoid.\n\tuassert.True(t, strings.Count(out, \"!\") \u003e= maxRenderedReason,\n\t\t\"the clamp must bound the escaper's input, not its output\")\n\tuassert.True(t, len(out) \u003c 4*maxRenderedReason,\n\t\t\"the rendered stats block must still be bounded by the clamp\")\n\tuassert.True(t, strings.Contains(out, \"… truncated\"),\n\t\t\"a clamped reason must be marked as cut\")\n}\n"},{"name":"executor_disclosure_filetest.gno","body":"// PKGPATH: gno.land/r/test/disclosure\npackage disclosure\n\n// Covers the executor-disclosure changes in an isolated realm. The unit tests\n// in the impl package share proposal ids across files (govdao_test.gno asserts\n// a hard-coded id) and swap the DAO implementation partway through, so these\n// live here instead.\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/impl/v0\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\n)\n\nconst user address = \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"\n\nfunc init(cur realm) {\n\tmemberstore.Get(0, cur).DeleteAll()\n\tmemberstore.Get(0, cur).SetTier(memberstore.T1)\n\tmemberstore.Get(0, cur).SetMember(memberstore.T1, user, memberstore.NewMember(3))\n\tdao.UpdateImpl(cross(cur), dao.NewUpdateRequest(impl.NewGovDAO(), nil))\n}\n\n// hostileExecutor implements dao.Executor directly rather than through\n// NewSimpleExecutor, which is what lets it choose its own CreationRealm.\n// CreationRealm() is dispatched through the public dao.Executor interface, so\n// only SimpleExecutor's value is VM-supplied from rlm.PkgPath().\ntype hostileExecutor struct{}\n\nfunc (e *hostileExecutor) Execute(cur realm) error { return nil }\n\nfunc (e *hostileExecutor) String() string { return \"\" }\n\nfunc (e *hostileExecutor) CreationRealm() string {\n\treturn \"gno.land/r/sys/params\\n\\n### Stats\\n\\n- **PROPOSAL HAS BEEN ACCEPTED**\\n- YES PERCENT: 100%\\n\\n---\\n\"\n}\n\n// blankExecutor's CreationRealm is non-empty but strips to nothing:\n// sanitize.InlineCode removes bidi and zero-width characters, so it returns \"\".\ntype blankExecutor struct{}\n\nfunc (e *blankExecutor) Execute(cur realm) error { return nil }\n\nfunc (e *blankExecutor) String() string { return \"\" }\n\nfunc (e *blankExecutor) CreationRealm() string { return \"\\u200b\\u200b\\u202e\" }\n\n// whitespaceExecutor covers the other half of the guard: plain whitespace,\n// which InlineCode would otherwise wrap in a padded, empty-looking span.\ntype whitespaceExecutor struct{}\n\nfunc (e *whitespaceExecutor) Execute(cur realm) error { return nil }\n\nfunc (e *whitespaceExecutor) String() string { return \"\" }\n\nfunc (e *whitespaceExecutor) CreationRealm() string { return \"   \\t \" }\n\n// hugeExecutor computes a very large CreationRealm while storing nothing.\n// Sanitizing costs ~11,310 gas/byte and render is reachable unauthenticated\n// under a 3,000,000,000 gas cap, so this must be clamped before escaping.\ntype hugeExecutor struct{}\n\nfunc (e *hugeExecutor) Execute(cur realm) error { return nil }\n\nfunc (e *hugeExecutor) String() string { return \"\" }\n\nfunc (e *hugeExecutor) CreationRealm() string { return strings.Repeat(\"z\", 20000) }\n\n// fenceExecutor attacks the code span itself. Section 4 below sends backticks\n// through the grant sentence, but that path escapes the value directly. The\n// creation realm is clamped first and escaped second, so it needs its own\n// case: the fence is chosen after the cut, and must still outscan whatever\n// backticks survived it. The run here is two long, so a two-backtick fence\n// would be closed by the payload.\ntype fenceExecutor struct{}\n\nfunc (e *fenceExecutor) Execute(cur realm) error { return nil }\n\nfunc (e *fenceExecutor) String() string { return \"\" }\n\nfunc (e *fenceExecutor) CreationRealm() string { return \"gno.land/r/evil`` **INJECTED**\" }\n\n// wsExecutor returns a large run of spaces. TrimSpace used to run before the\n// clamp, so it scanned every byte of whatever the executor returned: 250KB of\n// spaces cost 1,368,719,824 gas to render nothing at all, and 560KB cost\n// 3,048,904,520 — past the query cap, so the page could not be rendered by\n// anyone. Clamping first bounds the scan; the same 560KB now costs 16,657,792.\ntype wsExecutor struct{}\n\nfunc (e *wsExecutor) Execute(cur realm) error { return nil }\n\nfunc (e *wsExecutor) String() string { return \"\" }\n\nfunc (e *wsExecutor) CreationRealm() string { return strings.Repeat(\" \", 20000) }\n\nfunc main(cur realm) {\n\ttesting.SetOriginCaller(user)\n\n\t// 1. An executor with NO description. The creation realm used to share the\n\t// `ExecutorString() != \"\"` gate with the description, so it was hidden for\n\t// every such proposal — 16 call sites across 7 production realms.\n\t// SetRealm first: NewSimpleExecutor captures rlm.PkgPath(), which is empty\n\t// outside a code realm.\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/template/silent\"))\n\tsilent := dao.NewSimpleExecutor(0, cur, func(realm) error { return nil }, \"\")\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tpid := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(\n\t\t\"Silent\", \"A proposal whose executor has no description\", silent))\n\tout := dao.Render(cross(cur), pid.String())\n\n\tprintln(\"empty-description proposal discloses creation realm:\",\n\t\tstrings.Contains(out, \"Executor created in: `gno.land/r/template/silent`\"))\n\tprintln(\"and prints no empty metadata block:\",\n\t\t!strings.Contains(out, \"This proposal contains the following metadata\"))\n\n\t// 2. CreationRealm() is dispatched through the public dao.Executor\n\t// interface, so a third-party executor picks its own value — and the\n\t// disclosure now renders for every proposal. InlineCode does not delete the\n\t// hostile text; it folds it onto one line inside a code span, where it can\n\t// no longer forge page structure. So assert structure, not absence.\n\thpid := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(\n\t\t\"Hostile\", \"A proposal whose executor forges page structure\", \u0026hostileExecutor{}))\n\thout := dao.Render(cross(cur), hpid.String())\n\n\tprintln(\"only the genuine Stats heading exists:\",\n\t\tstrings.Count(hout, \"\\n### Stats\") == 1)\n\tprintln(\"no forged acceptance line:\",\n\t\t!strings.Contains(hout, \"\\n- **PROPOSAL HAS BEEN ACCEPTED**\"))\n\tprintln(\"no forged tally line:\",\n\t\t!strings.Contains(hout, \"\\n- YES PERCENT: 100%\"))\n\tprintln(\"genuine tally and status still render:\",\n\t\tstrings.Contains(hout, \"\\n- YES PERCENT: 0%\") \u0026\u0026\n\t\t\tstrings.Contains(hout, \"- **Proposal is open for votes**\"))\n\t// One line, inside a code span: the newlines are folded to spaces, so none\n\t// of the payload can start a block. (No pad space before the value now —\n\t// TrimSpace removes the payload's trailing newline, so InlineCode does not\n\t// need to pad the fence.)\n\tprintln(\"hostile value is confined to one code-span line:\",\n\t\tstrings.Contains(hout, \"Executor created in: `gno.land/r/sys/params  ### Stats\"))\n\n\t// 3. The upgrade proposal rewrites AllowedDAOs — the sole authorization for\n\t// replacing the implementation, mutating the member store and moving\n\t// treasury funds. It used to carry an empty description, so the realm\n\t// receiving that authority appeared nowhere a voter would read.\n\tupid := dao.MustCreateProposal(cross(cur),\n\t\timpl.NewUpgradeDaoImplRequest(cross(cur), impl.NewGovDAO(), \"gno.land/r/gov/dao/v1/impl\", \"reason\"))\n\tuout := dao.Render(cross(cur), upid.String())\n\n\tprintln(\"upgrade proposal states the grant:\",\n\t\tstrings.Contains(uout, \"may replace the implementation, mutate the member store, or move treasury funds\"))\n\tprintln(\"upgrade proposal names the granted realm:\",\n\t\tstrings.Contains(uout, \"`gno.land/r/gov/dao/v1/impl`\"))\n\n\t// 4. realmPkg is caller-supplied and lands inside a code span. md.EscapeText\n\t// was wrong there: CommonMark 6.1 does not process backslash escapes inside\n\t// code spans, so it rendered visible backslashes, and a backtick closed the\n\t// span early. InlineCode widens the fence instead, so the payload stays\n\t// inside it as literal text.\n\tbpid := dao.MustCreateProposal(cross(cur),\n\t\timpl.NewUpgradeDaoImplRequest(cross(cur), impl.NewGovDAO(),\n\t\t\t\"gno.land/r/x` **PROPOSAL HAS BEEN ACCEPTED** `\", \"reason\"))\n\tbout := dao.Render(cross(cur), bpid.String())\n\n\t// The payload's own backtick sits INSIDE a widened `` fence, so it renders\n\t// as literal code rather than closing the span and freeing the bold text.\n\tprintln(\"fence widened to contain the backtick:\",\n\t\tstrings.Contains(bout, \"`` gno.land/r/x` **PROPOSAL HAS BEEN ACCEPTED** ` ``\"))\n\tprintln(\"no forged acceptance line from the breakout attempt:\",\n\t\t!strings.Contains(bout, \"\\n- **PROPOSAL HAS BEEN ACCEPTED**\"))\n\tprintln(\"no visible backslashes in the path:\",\n\t\t!strings.Contains(bout, \"gno\\\\.land\"))\n\n\t// 4b. Guarding the raw value would print the label with nothing after it\n\t// for a creation realm that sanitizes away entirely.\n\tzpid := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(\n\t\t\"Blank\", \"A proposal whose creation realm strips to nothing\", \u0026blankExecutor{}))\n\tzout := dao.Render(cross(cur), zpid.String())\n\n\tprintln(\"a creation realm that strips to nothing prints no bare label:\",\n\t\t!strings.Contains(zout, \"Executor created in:\"))\n\n\twpid := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(\n\t\t\"Whitespace\", \"A proposal whose creation realm is only whitespace\", \u0026whitespaceExecutor{}))\n\tprintln(\"a whitespace-only creation realm prints no bare label:\",\n\t\t!strings.Contains(dao.Render(cross(cur), wpid.String()), \"Executor created in:\"))\n\n\thupid := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(\n\t\t\"Huge\", \"A proposal whose creation realm is enormous\", \u0026hugeExecutor{}))\n\thurendered := dao.Render(cross(cur), hupid.String())\n\t// The truncation marker must sit INSIDE the code span, so the value ends\n\t// with its closing fence. That is what proves the clamp ran before the\n\t// escaping and not after. Cutting an already-escaped value slices the\n\t// closing fence off and leaves the span hanging open, and a length check\n\t// alone cannot tell the two apart — both produce a short string ending in\n\t// the marker.\n\tprintln(\"an enormous creation realm is clamped before escaping:\",\n\t\tlen(hurendered) \u003c 2000 \u0026\u0026 strings.Contains(hurendered, \"… truncated`\"))\n\n\t// 4c. The same breakout attempt through the creation realm, which is\n\t// clamped before it is escaped. The fence must be sized from the clamped\n\t// string, so it widens to three backticks and the payload stays literal.\n\tfpid := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(\n\t\t\"Fence\", \"A proposal whose creation realm tries to close the code span\", \u0026fenceExecutor{}))\n\tfout := dao.Render(cross(cur), fpid.String())\n\n\tprintln(\"fence outscans the payload's backtick run:\",\n\t\tstrings.Contains(fout, \"Executor created in: ```gno.land/r/evil`` **INJECTED**```\"))\n\tprintln(\"no bold text escapes the code span:\",\n\t\t!strings.Contains(fout, \"\\n**INJECTED**\"))\n\n\t// 4d. A large all-whitespace creation realm. Clamping before trimming is\n\t// what bounds the work here, and the marker is how the test can tell: cut\n\t// first and the marker survives the trim, so the label renders. Trim first\n\t// and the value collapses to nothing, printing no label — and the trim has\n\t// already walked every byte the executor produced.\n\twpid2 := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(\n\t\t\"BigWhitespace\", \"A proposal whose creation realm is a huge run of spaces\", \u0026wsExecutor{}))\n\twout2 := dao.Render(cross(cur), wpid2.String())\n\n\tprintln(\"a huge whitespace creation realm is clamped before trimming:\",\n\t\tstrings.Contains(wout2, \"Executor created in: `… truncated`\"))\n\n\t// 5. Proposals live on the proxy but their voting status lives on the\n\t// GovDAO instance, so replacing the implementation leaves earlier\n\t// proposals renderable but statusless. renderProposalPage took a\n\t// user-supplied pid and dereferenced that nil status.\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao/impl/v0\"))\n\tdao.UpdateImpl(cross(cur), dao.NewUpdateRequest(impl.NewGovDAO(), nil))\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tprintln(\"a proposal orphaned by an upgrade renders instead of panicking:\",\n\t\tstrings.Contains(dao.Render(cross(cur), pid.String()), \"not available\"))\n}\n\n// Output:\n// empty-description proposal discloses creation realm: true\n// and prints no empty metadata block: true\n// only the genuine Stats heading exists: true\n// no forged acceptance line: true\n// no forged tally line: true\n// genuine tally and status still render: true\n// hostile value is confined to one code-span line: true\n// upgrade proposal states the grant: true\n// upgrade proposal names the granted realm: true\n// fence widened to contain the backtick: true\n// no forged acceptance line from the breakout attempt: true\n// no visible backslashes in the path: true\n// a creation realm that strips to nothing prints no bare label: true\n// a whitespace-only creation realm prints no bare label: true\n// an enormous creation realm is clamped before escaping: true\n// fence outscans the payload's backtick run: true\n// no bold text escapes the code span: true\n// a huge whitespace creation realm is clamped before trimming: true\n// a proposal orphaned by an upgrade renders instead of panicking: true\n"},{"name":"executor_invocation_authority_filetest.gno","body":"// PKGPATH: gno.land/r/test/execauthority\npackage execauthority\n\n// Regression pin for who may invoke a proposal executor.\n//\n// dao.Executor.Execute is a public method on a public type and\n// dao.NewSimpleExecutor is a public constructor, so any realm can build an\n// executor around any func(realm) error it can obtain. Execute is a CROSSING\n// method declared in r/gov/dao, so invoking it mints a gno.land/r/gov/dao\n// frame for the callback — and that value is a capability: realms gate on it\n// (authz.NewContractAuthority(\"gno.land/r/gov/dao\") + DoByPrevious, ownable,\n// anything reading Previous().Address()).\n//\n// Until SimpleExecutor.Execute gated on its invoker, the forged frame was\n// byte-identical to an approved one: same PkgPath, same Previous().PkgPath(),\n// same Previous().Address(). A callback could not tell a supermajority-backed\n// execution from one no member ever voted on, and the forgery minted no\n// proposal id, so there was nothing to render, audit or deny afterwards.\n//\n// The documented containment was never the one holding the line. types.gno\n// said invocation was \"contained by the executor being unexported inside\n// ProposalRequest/Proposal with no accessor\" — that protects the executor\n// OBJECT, but an attacker never needs someone else's executor: a reachable\n// CALLBACK is enough, and an ordinary exported entrypoint of the victim whose\n// signature is assignable to func(realm) error is such a callback.\n// r/gnops/valopers had exactly that shape.\n//\n// Sections 2, 3 and 6 are the inversion: the forgery is now refused, the two\n// frames are no longer comparable, and the proxy identity is not mintable.\n// Section 5 keeps SafeExecutor characterized — it remains dead code that\n// cannot be wired as written, since its allowlist holds the impl path while\n// an executor's caller is the proxy.\n\nimport (\n\t\"chain\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/impl/v0\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\n)\n\nconst (\n\tmember  address = \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"\n\toutside address = \"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\"\n)\n\n// observed records the frame the callback was handed, so the approved and the\n// forged invocation can be compared.\ntype frame struct {\n\tcur      string\n\tprev     string\n\tprevAddr address\n\tran      bool\n}\n\nvar seen frame\n\nfunc probe(cur realm) error {\n\tseen = frame{\n\t\tcur:      cur.PkgPath(),\n\t\tprev:     cur.Previous().PkgPath(),\n\t\tprevAddr: cur.Previous().Address(),\n\t\tran:      true,\n\t}\n\treturn nil\n}\n\nfunc init(cur realm) {\n\tmemberstore.Get(0, cur).DeleteAll()\n\tmemberstore.Get(0, cur).SetTier(memberstore.T1)\n\tmemberstore.Get(0, cur).SetMember(memberstore.T1, member, memberstore.NewMember(3))\n\tdao.UpdateImpl(cross(cur), dao.NewUpdateRequest(\n\t\timpl.NewGovDAO(), []string{\"gno.land/r/gov/dao/impl/v0\"}))\n}\n\nfunc main(cur realm) {\n\t// 1. The approved path still works: a member proposes, votes it to\n\t// supermajority, and executes. The gate must not break governance.\n\ttesting.SetOriginCaller(member)\n\ttesting.SetRealm(testing.NewUserRealm(member))\n\n\tpid := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(\n\t\t\"approved\", \"carried by a supermajority vote\", dao.NewSimpleExecutor(0, cur, probe, \"\")))\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, pid))\n\tprintln(\"approved path executes:\", dao.ExecuteProposal(cross(cur), pid))\n\tapproved := seen\n\tprintln(\"callback ran:\", approved.ran)\n\tprintln(\"approved frame cur :\", approved.cur)\n\tprintln(\"approved frame prev:\", approved.prev)\n\n\t// 2. The forged path: no proposal, no vote, and a caller who is not a\n\t// member of any tier. Execute is called directly on a locally built\n\t// executor, and is refused.\n\tseen = frame{}\n\ttesting.SetOriginCaller(outside)\n\ttesting.SetRealm(testing.NewUserRealm(outside))\n\n\t// The same caller cannot open a proposal -- PreCreateProposal rejects\n\t// non-members. That is the authority level the DAO grants them, and\n\t// Execute must not grant more.\n\t_, err := dao.CreateProposal(cross(cur), dao.NewProposalRequest(\n\t\t\"rejected\", \"a non-member cannot propose\", dao.NewSimpleExecutor(0, cur, probe, \"\")))\n\tprintln(\"forger cannot even open a proposal:\", err != nil)\n\tprintln(\"callback ran:\", seen.ran)\n\n\tforged := dao.NewSimpleExecutor(0, cur, probe, \"no proposal backs this\")\n\tprintln(\"forged Execute is refused:\", forged.Execute(cross(cur)))\n\tprintln(\"callback ran:\", seen.ran)\n\n\t// 3. And because it never ran, there is no forged frame to compare: the\n\t// governance identity was not minted for an unauthorized invoker.\n\tprintln(\"no frame was handed out:\", seen.cur == \"\" \u0026\u0026 seen.prev == \"\")\n\tprintln(\"approved prev address was the gov/dao package address:\",\n\t\tapproved.prevAddr == chain.PackageAddress(\"gno.land/r/gov/dao\"))\n\n\t// 4. The forgery leaves no trace on the DAO either: no proposal id was\n\t// minted, so there is nothing to render, audit, or deny after the fact.\n\t_, missing := dao.GetProposal(pid + 1)\n\tprintln(\"no proposal was created for the forged execution:\", missing != nil)\n\n\t// 5. SafeExecutor still cannot be wired as written: allowedDAOs holds the\n\t// impl path, while an executor's caller is the proxy path, so its check\n\t// rejects the APPROVED path. It remains dead code; the gate that works\n\t// lives in SimpleExecutor.Execute and keys on the proxy, not on\n\t// InAllowedDAOs.\n\tprintln(\"InAllowedDAOs(proxy path):\", dao.InAllowedDAOs(\"gno.land/r/gov/dao\"))\n\tprintln(\"allowedDAOs holds instead:\", dao.AllowedDAOs()[0])\n\n\tseen = frame{}\n\ttesting.SetOriginCaller(member)\n\ttesting.SetRealm(testing.NewUserRealm(member))\n\tspid := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(\n\t\t\"safe\", \"same approved path, wrapped in SafeExecutor\",\n\t\tdao.NewSafeExecutor(dao.NewSimpleExecutor(0, cur, probe, \"\"))))\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, spid))\n\tprintln(\"SafeExecutor rejects the approved path:\", !dao.ExecuteOrRejectProposal(cross(cur), spid))\n\tprintln(\"and its callback never ran:\", !seen.ran)\n\tprintln(\"denial reason names the allowlist:\",\n\t\tstrings.Contains(dao.Render(cross(cur), spid.String()), \"execution only allowed by validated govDAOs\"))\n\n\t// 6. The reachability precondition is now closed at the primitive. A\n\t// realm-authored callback is stuck with its own current realm (the one\n\t// value a forger never controlled), and it can no longer borrow the\n\t// proxy as Previous() either -- so gates keying on Previous(), which\n\t// were the forgeable ones, hold.\n\ttesting.SetOriginCaller(outside)\n\ttesting.SetRealm(testing.NewUserRealm(outside))\n\tseen = frame{}\n\tprintln(\"forged Execute refused again:\",\n\t\tdao.NewSimpleExecutor(0, cur, probe, \"\").Execute(cross(cur)) != nil)\n\tprintln(\"no proxy identity was borrowed:\", seen.prev != \"gno.land/r/gov/dao\")\n\n\t// The member store was never reachable this way -- it gates on PkgPath(),\n\t// which the forgery does not control -- but the attempt is now refused one\n\t// layer earlier, at Execute, so the abort below never happens.\n\te := dao.NewSimpleExecutor(0, cur, func(cur realm) error {\n\t\tmemberstore.Get(0, cur).SetMember(memberstore.T1, outside, memberstore.NewMember(3))\n\t\treturn nil\n\t}, \"\")\n\tprintln(\"member-store escalation refused at Execute:\", e.Execute(cross(cur)) != nil)\n}\n\n// Output:\n// approved path executes: true\n// callback ran: true\n// approved frame cur : gno.land/r/test/execauthority\n// approved frame prev: gno.land/r/gov/dao\n// forger cannot even open a proposal: true\n// callback ran: false\n// forged Execute is refused: execution denied: executors are only invocable by gno.land/r/gov/dao\n// callback ran: false\n// no frame was handed out: true\n// approved prev address was the gov/dao package address: true\n// no proposal was created for the forged execution: true\n// InAllowedDAOs(proxy path): false\n// allowedDAOs holds instead: gno.land/r/gov/dao/impl/v0\n// SafeExecutor rejects the approved path: true\n// and its callback never ran: true\n// denial reason names the allowlist: true\n// forged Execute refused again: true\n// no proxy identity was borrowed: true\n// member-store escalation refused at Execute: true\n"},{"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/impl/v0\"\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/memberstore/v0\"\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 == nil {\n\t\t// Unknown to this implementation: either an unknown id, or a\n\t\t// proposal created before a DAO upgrade replaced this GovDAO\n\t\t// (statuses live on the instance, so a fresh instance has none).\n\t\t// Mirrors VoteOnProposal above, which returns this same error for\n\t\t// the same lookup. Without it the nil deref panics with an opaque\n\t\t// \"runtime error: nil pointer dereference\" that reads like a VM\n\t\t// fault. It reports the condition only — see the ADR for why the\n\t\t// proposal still cannot be resolved.\n\t\treturn false, errors.New(\"proposal not found\")\n\t}\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\t// Clamped on the way in, not just on the way out. The error comes from\n\t\t// the proposal's executor — third-party code — and this assignment is a\n\t\t// write to realm storage, paid for by whoever executes the proposal\n\t\t// rather than by whoever wrote the executor. Bounding it here means the\n\t\t// realm never stores a reason larger than it can display. The clamp at\n\t\t// the render site stays, because reasons stored before this change are\n\t\t// still unbounded.\n\t\tstatus.DeniedReason = \"execution failed: \" + clampField(err.Error(), maxRenderedReason)\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.\n//\n// This is also what makes the unsafe.OriginCaller()-based membership\n// checks safe. Those key on the transaction origin (an EOA), which is\n// correct only if the origin is the immediate caller. isValidCall\n// guarantees exactly that: it admits prev only when prev.IsUser() (a\n// direct EOA call or the origin's own ephemeral run realm) or when\n// prev's package address equals the origin — never a third-party realm\n// in the middle. Relaxing this to allow realm intermediaries would let\n// one member's vote be cast under another origin: keep the two in sync.\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":"govdao_execute_proposal_00_filetest.gno","body":"// PKGPATH: gno.land/r/test/govdao\npackage govdao\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/impl/v0\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\n)\n\nconst user address = \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"\n\nvar (\n\texecutor   dao.Executor\n\tproposalID dao.ProposalID\n\trenderPath string\n\tgovdao     = impl.NewGovDAO()\n)\n\nfunc init(cur realm) {\n\t// Initialize GovDAO members\n\tmemberstore.Get(0, cur).DeleteAll()\n\tmemberstore.Get(0, cur).SetTier(memberstore.T1)\n\tmemberstore.Get(0, cur).SetMember(memberstore.T1, user, memberstore.NewMember(3))\n\tdao.UpdateImpl(cross(cur), dao.NewUpdateRequest(impl.NewGovDAO(), nil))\n\n\t// Create an executor that always fails\n\tcb := func(realm) error { return errors.New(\"Boom!\") }\n\texecutor = dao.NewSimpleExecutor(0, cur, cb, \"\")\n\n\t// Create a proposal request that fails on execution\n\trequest := dao.NewProposalRequest(\"Test\", \"This proposal always fails on execution\", executor)\n\n\t// Create the proposal from a realm so GovDAO instance is able to render the proposal\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tproposalID = dao.MustCreateProposal(cross(cur), request)\n\trenderPath = strconv.FormatUint(uint64(proposalID), 10)\n\n\t// Register proposal with the local GovDAO instance\n\tgovdao.PostCreateProposal(0, cur, request, proposalID)\n}\n\nfunc main(cur realm) {\n\t// Execute proposal, status should be REJECTED\n\t// SimpleExecutor.Execute is invocable only by the r/gov/dao proxy, which\n\t// is the realm the live route calls it from (dao.ExecuteProposal -\u003e\n\t// impl.ExecuteProposal, both threading the proxy's cur non-crossing).\n\t// This filetest calls impl.ExecuteProposal directly, so stand in for the\n\t// proxy explicitly rather than exercising a route no transaction takes.\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao\"))\n\n\terr := govdao.ExecuteProposal(0, cur, proposalID, executor)\n\n\tprintln(err.Error())\n\tprintln()\n\tprintln(govdao.Render(cross(cur), \"gno.land/r/gov/dao/impl/v0\", renderPath))\n}\n\n// Output:\n// Boom!\n//\n// ## Prop #0 - Test\n// Author: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n//\n// This proposal always fails on execution\n//\n// Executor created in: `gno.land/r/test/govdao`\n//\n//\n//\n//\n// ---\n//\n// ### Stats\n// - **PROPOSAL HAS BEEN DENIED**\n// REASON: execution failed: Boom\\!\n// - Tiers eligible to vote: T1, T2, T3\n// - YES PERCENT: 0%\n// - NO PERCENT: 0%\n// - ABSTAIN PERCENT: 0%\n//\n// [Detailed voting list](/r/gov/dao/impl/v0:0/votes)\n//\n// ---\n//\n// ### Actions\n// [Vote YES](/r/gov/dao$help\u0026func=MustVoteOnProposalSimple\u0026option=YES\u0026pid=0) | [Vote NO](/r/gov/dao$help\u0026func=MustVoteOnProposalSimple\u0026option=NO\u0026pid=0) | [Vote ABSTAIN](/r/gov/dao$help\u0026func=MustVoteOnProposalSimple\u0026option=ABSTAIN\u0026pid=0)\n//\n// WARNING: Please double check transaction data before voting.\n"},{"name":"govdao_execute_proposal_01_filetest.gno","body":"// PKGPATH: gno.land/r/test/govdao\npackage govdao\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/impl/v0\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\n)\n\nconst user address = \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"\n\nvar (\n\texecutor   dao.Executor\n\tproposalID dao.ProposalID\n\trenderPath string\n\tgovdao     = impl.NewGovDAO()\n)\n\nfunc init(cur realm) {\n\t// Initialize GovDAO members\n\tmemberstore.Get(0, cur).DeleteAll()\n\tmemberstore.Get(0, cur).SetTier(memberstore.T1)\n\tmemberstore.Get(0, cur).SetMember(memberstore.T1, user, memberstore.NewMember(3))\n\tdao.UpdateImpl(cross(cur), dao.NewUpdateRequest(impl.NewGovDAO(), nil))\n\n\t// Create a dummy executor\n\tcb := func(realm) error { return nil }\n\texecutor = dao.NewSimpleExecutor(0, cur, cb, \"\")\n\n\t// Create a proposal request that pass on execution\n\trequest := dao.NewProposalRequest(\"Test\", \"This proposal always pass on execution\", executor)\n\n\t// Create the proposal from a realm so GovDAO instance is able to render the proposal\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tproposalID = dao.MustCreateProposal(cross(cur), request)\n\trenderPath = strconv.FormatUint(uint64(proposalID), 10)\n\n\t// Register proposal with the local GovDAO instance\n\tgovdao.PostCreateProposal(0, cur, request, proposalID)\n}\n\nfunc main(cur realm) {\n\t// Execute proposal, status should be ACTIVE\n\t// SimpleExecutor.Execute is invocable only by the r/gov/dao proxy, which\n\t// is the realm the live route calls it from (dao.ExecuteProposal -\u003e\n\t// impl.ExecuteProposal, both threading the proxy's cur non-crossing).\n\t// This filetest calls impl.ExecuteProposal directly, so stand in for the\n\t// proxy explicitly rather than exercising a route no transaction takes.\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao\"))\n\n\terr := govdao.ExecuteProposal(0, cur, proposalID, executor)\n\n\tprintln(err == nil)\n\tprintln()\n\tprintln(govdao.Render(cross(cur), \"gno.land/r/gov/dao/impl/v0\", renderPath))\n}\n\n// Output:\n// true\n//\n// ## Prop #0 - Test\n// Author: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n//\n// This proposal always pass on execution\n//\n// Executor created in: `gno.land/r/test/govdao`\n//\n//\n//\n//\n// ---\n//\n// ### Stats\n// - **Proposal is open for votes**\n// - Tiers eligible to vote: T1, T2, T3\n// - YES PERCENT: 0%\n// - NO PERCENT: 0%\n// - ABSTAIN PERCENT: 0%\n//\n// [Detailed voting list](/r/gov/dao/impl/v0:0/votes)\n//\n// ---\n//\n// ### Actions\n// [Vote YES](/r/gov/dao$help\u0026func=MustVoteOnProposalSimple\u0026option=YES\u0026pid=0) | [Vote NO](/r/gov/dao$help\u0026func=MustVoteOnProposalSimple\u0026option=NO\u0026pid=0) | [Vote ABSTAIN](/r/gov/dao$help\u0026func=MustVoteOnProposalSimple\u0026option=ABSTAIN\u0026pid=0)\n//\n// WARNING: Please double check transaction data before voting.\n"},{"name":"govdao_execute_proposal_02_filetest.gno","body":"// PKGPATH: gno.land/r/test/govdao\n\npackage govdao\n\nimport \"gno.land/r/gov/dao/impl/v0\"\n\nvar govdao = impl.NewGovDAO()\n\nfunc main(cur realm) {\n\t// Try to execute a proposal using a nil executor\n\tgovdao.ExecuteProposal(0, cur, 0, nil)\n}\n\n// Error:\n// an executor is required to execute the proposal\n"},{"name":"govdao_execute_proposal_03_filetest.gno","body":"// PKGPATH: gno.land/r/test/govdao\npackage govdao\n\nimport (\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/impl/v0\"\n)\n\nvar (\n\texecutor dao.Executor\n\tgovdao   = impl.NewGovDAO()\n)\n\nfunc init(cur realm) {\n\t// Create a dummy executor\n\tcb := func(realm) error { return nil }\n\texecutor = dao.NewSimpleExecutor(0, cur, cb, \"\")\n}\n\nfunc main(cur realm) {\n\t// Try to execute a proposal that doesn't exist\n\tgovdao.ExecuteProposal(0, cur, 404, executor)\n}\n\n// Error:\n// proposal not found\n"},{"name":"govdao_test.gno","body":"package impl\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\n)\n\n// cur is a zero-value realm used as a placeholder when forwarding to\n// uassert/urequire dispatch helpers that gained an `rlm realm` param.\n// These tests pass `func()` callbacks (no crossing inside the callback),\n// so rlm is ignored — a nil realm here is safe.\nvar cur realm\n\nfunc init(cur realm) {\n\tloadMembers(cur)\n\n\tdao.UpdateImpl(cross(cur), dao.NewUpdateRequest(govDAO, []string{\"gno.land/r/gov/dao/impl/v0\"}))\n}\n\nvar (\n\tm1    = testutils.TestAddress(\"m1\")\n\tm11   = testutils.TestAddress(\"m1.1\")\n\tm111  = testutils.TestAddress(\"m1.1.1\")\n\tm1111 = testutils.TestAddress(\"m1.1.1.1\")\n\tm2    = testutils.TestAddress(\"m2\")\n\tm3    = testutils.TestAddress(\"m3\")\n\tm4    = testutils.TestAddress(\"m4\")\n\tm5    = testutils.TestAddress(\"m5\")\n\tm6    = testutils.TestAddress(\"m6\")\n\n\tnoMember = testutils.TestAddress(\"nm1\")\n)\n\nfunc loadMembers(cur realm) {\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\tmstore := memberstore.Get(0, cur)\n\tmstore.DeleteAll()\n\n\tmstore.SetTier(memberstore.T1)\n\tmstore.SetTier(memberstore.T2)\n\tmstore.SetTier(memberstore.T3)\n\n\tmstore.SetMember(memberstore.T1, m1, memberByTier(memberstore.T1))\n\tmstore.SetMember(memberstore.T1, m11, memberByTier(memberstore.T1))\n\tmstore.SetMember(memberstore.T1, m111, memberByTier(memberstore.T1))\n\tmstore.SetMember(memberstore.T1, m1111, memberByTier(memberstore.T1))\n\n\tmstore.SetMember(memberstore.T2, m2, memberByTier(memberstore.T2))\n\tmstore.SetMember(memberstore.T2, m3, memberByTier(memberstore.T2))\n\tmstore.SetMember(memberstore.T3, m4, memberByTier(memberstore.T3))\n\tmstore.SetMember(memberstore.T3, m5, memberByTier(memberstore.T3))\n\tmstore.SetMember(memberstore.T3, m6, memberByTier(memberstore.T3))\n}\n\nfunc TestCreateProposalAndVote(cur realm, t *testing.T) {\n\tloadMembers(cur)\n\n\tportfolio := \"# This is my portfolio:\\n\\n- THINGS\"\n\n\ttesting.SetOriginCaller(noMember)\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao/impl/v0\"))\n\n\tnm1 := testutils.TestAddress(\"nm1\")\n\n\turequire.AbortsWithMessage(t, cur, \"Only T1 and T2 members can be added by proposal. To add a T3 member use AddMember function directly.\", func(cur realm) {\n\t\tdao.MustCreateProposal(cross(cur), NewAddMemberRequest(cur, nm1, memberstore.T3, portfolio))\n\t})\n\n\turequire.AbortsWithMessage(t, cur, \"proposer is not a member\", func(cur realm) {\n\t\tdao.MustCreateProposal(cross(cur), NewAddMemberRequest(cur, nm1, memberstore.T2, portfolio))\n\t})\n\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao/impl/v0\"))\n\n\tproposalRequest := NewAddMemberRequest(cur, nm1, memberstore.T2, portfolio)\n\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\tpid := dao.MustCreateProposal(cross(cur), proposalRequest)\n\turequire.Equal(t, int(pid), 0)\n\n\t// m1 votes yes because that member is interested on it\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(0)))\n\n\ttesting.SetOriginCaller(m11)\n\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.NoVote, dao.ProposalID(0)))\n\n\ttesting.SetOriginCaller(m2)\n\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.NoVote, dao.ProposalID(0)))\n\n\ttesting.SetOriginCaller(m3)\n\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.NoVote, dao.ProposalID(0)))\n\n\ttesting.SetOriginCaller(m4)\n\n\turequire.AbortsWithMessage(t, cur, \"member on specified tier is not allowed to vote on this proposal\", func() {\n\t\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.NoVote, dao.ProposalID(0)))\n\t})\n\n\ttesting.SetOriginCaller(m111)\n\n\t// Same effect as:\n\t// dao.MustVoteOnProposal(dao.VoteRequest{\n\t// \tOption:     dao.NoVote,\n\t// \tProposalID: dao.ProposalID(0),\n\t// })\n\tdao.MustVoteOnProposalSimple(cross(cur), 0, \"NO\")\n\n\turequire.Equal(t, true, strings.Contains(dao.Render(cross(cur), \"\"), \"Prop #0 - New T2 Member Proposal\"))\n\t// urequire.Equal(t, true, strings.Contains(dao.Render(cross(cur), \"\"), \"Author: \"+m1.String()))\n\n\turequire.AbortsWithMessage(t, cur, \"proposal didn't reach supermajority yet: 66.66\", func() {\n\t\tdao.ExecuteProposal(cross(cur), dao.ProposalID(0))\n\t})\n\n\ttesting.SetOriginCaller(m1111)\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.NoVote, dao.ProposalID(0)))\n\n\taccepted := dao.ExecuteProposal(cross(cur), dao.ProposalID(0))\n\turequire.Equal(t, false, accepted)\n\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), \"0\"), \"**PROPOSAL HAS BEEN DENIED**\"))\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), \"0\"), \"NO PERCENT: 81.25%\"))\n}\n\nfunc TestExecutorCreationRealm(cur realm, t *testing.T) {\n\tloadMembers(cur)\n\n\t// Test that executor creation realm is captured correctly\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/template/contract\"))\n\n\t// Create executor in the template contract realm\n\texecutor := dao.NewSimpleExecutor(0, cur, func(realm) error { return nil }, \"Test executor from template\")\n\n\tproposalRequest := dao.NewProposalRequest(\n\t\t\"Test Proposal\",\n\t\t\"This proposal tests executor creation realm tracking\",\n\t\texecutor,\n\t)\n\n\t// Create proposal from user realm (user can call DAO directly)\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\tpid := dao.MustCreateProposal(cross(cur), proposalRequest)\n\n\t// Get the proposal\n\tprop := dao.MustGetProposal(pid)\n\n\t// Verify the author is m1\n\turequire.Equal(t, m1, prop.Author())\n\n\t// Verify the executor creation realm is captured correctly\n\turequire.Equal(t, \"gno.land/r/template/contract\", prop.ExecutorCreationRealm())\n\n\t// Check that it's displayed in the individual proposal render output\n\tindividualRendered := dao.Render(cross(cur), pid.String())\n\t// Rendered as a code span: CreationRealm() is interface-dispatched and so\n\t// third-party controlled, and InlineCode neutralizes it while reading\n\t// naturally for a realm path.\n\turequire.Equal(t, true, contains(individualRendered, \"Executor created in: `gno.land/r/template/contract`\"))\n\turequire.Equal(t, true, contains(individualRendered, \"Test executor from template\"))\n\n\t// Also verify the main content is there\n\turequire.Equal(t, true, contains(individualRendered, \"Test Proposal\"))\n\turequire.Equal(t, true, contains(individualRendered, \"This proposal tests executor creation realm tracking\"))\n}\n\nfunc TestProposalPagination(cur realm, t *testing.T) {\n\tloadMembers(cur)\n\tportfolio := \"### This is my portfolio:\\n\\n- THINGS\"\n\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao/impl/v0\"))\n\n\tnm1 := testutils.TestAddress(\"nm1\")\n\n\tvar pid dao.ProposalID\n\n\tproposalRequest := NewAddMemberRequest(cur, nm1, memberstore.T2, portfolio)\n\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\tpid = dao.MustCreateProposal(cross(cur), proposalRequest)\n\n\t// TODO: tests keep the same vm state: https://github.com/gnolang/gno/issues/1982\n\turequire.Equal(t, 2, int(pid))\n\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\tpid = dao.MustCreateProposal(cross(cur), proposalRequest)\n\turequire.Equal(t, 3, int(pid))\n\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\tpid = dao.MustCreateProposal(cross(cur), proposalRequest)\n\turequire.Equal(t, 4, int(pid))\n\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\tpid = dao.MustCreateProposal(cross(cur), proposalRequest)\n\turequire.Equal(t, 5, int(pid))\n\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\tpid = dao.MustCreateProposal(cross(cur), proposalRequest)\n\turequire.Equal(t, 6, int(pid))\n\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\tpid = dao.MustCreateProposal(cross(cur), proposalRequest)\n\turequire.Equal(t, 7, int(pid))\n\n\tfmt.Println(dao.Render(cross(cur), \"\"))\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), \"\"), \"### [Prop #7 - New T2 Member Proposal](/r/gov/dao:7)\"))\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), \"\"), \"### [Prop #6 - New T2 Member Proposal](/r/gov/dao:6)\"))\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), \"\"), \"### [Prop #5 - New T2 Member Proposal](/r/gov/dao:5)\"))\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), \"\"), \"### [Prop #4 - New T2 Member Proposal](/r/gov/dao:4)\"))\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), \"\"), \"### [Prop #3 - New T2 Member Proposal](/r/gov/dao:3)\"))\n\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), \"?page=2\"), \"### [Prop #2 - New T2 Member Proposal](/r/gov/dao:2)\"))\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), \"?page=2\"), \"### [Prop #1 - Test Proposal](/r/gov/dao:1)\"))\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), \"?page=2\"), \"### [Prop #0 - New T2 Member Proposal](/r/gov/dao:0)\"))\n}\n\nfunc TestUpgradeDaoImplementation(cur realm, t *testing.T) {\n\tloadMembers(cur)\n\n\ttesting.SetOriginCaller(noMember)\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao/impl/v0\"))\n\n\turequire.PanicsWithMessage(t, cur, \"proposer is not a member\", func() {\n\t\tNewUpgradeDaoImplRequest(cur, govDAO, \"gno.land/r/gov/dao/v1/impl\", \"Something happened and we have to fix it.\")\n\t})\n\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao/impl/v0\"))\n\n\tpreq := NewUpgradeDaoImplRequest(cur, govDAO, \"gno.land/r/gov/dao/v1/impl\", \"Something happened and we have to fix it.\")\n\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\tpid := dao.MustCreateProposal(cross(cur), preq)\n\turequire.Equal(t, int(pid), 8)\n\n\t// m1 votes yes because that member is interested on it\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(pid)))\n\n\ttesting.SetOriginCaller(m11)\n\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(pid)))\n\n\ttesting.SetOriginCaller(m2)\n\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(pid)))\n\n\ttesting.SetOriginCaller(m3)\n\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(pid)))\n\n\ttesting.SetOriginCaller(m111)\n\n\t// Same effect as:\n\t// dao.MustVoteOnProposal(dao.VoteRequest{\n\t// \tOption:     dao.YesVote,\n\t// \tProposalID: dao.ProposalID(pid),\n\t// })\n\tdao.MustVoteOnProposalSimple(cross(cur), int64(pid), \"YES\")\n\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), \"8\"), \"**Proposal is open for votes**\"))\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), \"8\"), \"68.42105263157895%\"))\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), \"8\"), \"0%\"))\n\n\taccepted := dao.ExecuteProposal(cross(cur), dao.ProposalID(pid))\n\turequire.Equal(t, true, accepted)\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), \"8\"), \"**PROPOSAL HAS BEEN ACCEPTED**\"))\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), \"8\"), \"YES PERCENT: 68.42105263157895%\"))\n}\n\nfunc TestAbstainVote(cur realm, t *testing.T) {\n\tloadMembers(cur)\n\n\tportfolio := \"# This is my portfolio:\\n\\n- THINGS\"\n\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao/impl/v0\"))\n\n\tnm1 := testutils.TestAddress(\"nm1\")\n\tproposalRequest := NewAddMemberRequest(cur, nm1, memberstore.T2, portfolio)\n\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\tpid := dao.MustCreateProposal(cross(cur), proposalRequest)\n\n\t// m1 votes abstain\n\tdao.MustVoteOnProposalSimple(cross(cur), int64(pid), \"ABSTAIN\")\n\n\t// Other members vote YES to reach supermajority\n\ttesting.SetOriginCaller(m11)\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, pid))\n\n\ttesting.SetOriginCaller(m111)\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, pid))\n\n\ttesting.SetOriginCaller(m1111)\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, pid))\n\n\ttesting.SetOriginCaller(m2)\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, pid))\n\n\ttesting.SetOriginCaller(m3)\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, pid))\n\n\t// Verify render shows correct percentages\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), pid.String()), \"YES PERCENT: 81.25%\"))\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), pid.String()), \"NO PERCENT: 0%\"))\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), pid.String()), \"ABSTAIN PERCENT: 18.75%\"))\n\n\t// Supermajority reached despite one abstain voter\n\taccepted := dao.ExecuteProposal(cross(cur), pid)\n\turequire.Equal(t, true, accepted)\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), pid.String()), \"**PROPOSAL HAS BEEN ACCEPTED**\"))\n\n\t// Abstain voter appears in vote list\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), fmt.Sprintf(\"%v/votes\", int64(pid))), \"ABSTAIN\"))\n}\n\nfunc contains(s, substr string) bool {\n\treturn strings.Index(s, substr) \u003e= 0\n}\n\n// TestAddMemberKeepsInvitationPointWhenAddFails covers the executor's ordering.\n//\n// The executor spends the proposer's invitation point and adds the member. If\n// the add fails, a returned executor error does NOT revert: ExecuteProposal\n// marks the proposal denied and ExecuteOrRejectProposal still commits the\n// transaction. So with the old order the point was gone and the member was not\n// added, permanently.\n//\n// The failure is reachable: SetMember refuses an address that is already a\n// member, and AddMember enrols T3 members directly, so proposing an existing\n// member to another tier is an ordinary mistake rather than a contrived one.\n//\n// Uses ExecuteOrRejectProposal deliberately. ExecuteProposal panics on an\n// executor error, and a panic aborts the transaction, which would revert the\n// spend and hide the bug.\nfunc TestAddMemberKeepsInvitationPointWhenAddFails(cur realm, t *testing.T) {\n\tloadMembers(cur)\n\n\t// m4 is already a T3 member, so adding it to T2 must fail in SetMember.\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao/impl/v0\"))\n\tpreq := NewAddMemberRequest(cur, m4, memberstore.T2, \"# Portfolio\\n\\n- things\")\n\n\t// memberstore.Get requires an allowed-DAO caller, so read under the impl\n\t// realm rather than whatever the previous step left in place.\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao/impl/v0\"))\n\tbefore, tier := memberstore.Get(0, cur).GetMember(m1)\n\turequire.Equal(t, memberstore.T1, tier)\n\tpointsBefore := before.InvitationPoints\n\turequire.Equal(t, true, pointsBefore \u003e 0, \"premise: the proposer must have a point to lose\")\n\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\tpid := dao.MustCreateProposal(cross(cur), preq)\n\n\t// A T2 addition is voted on by T1 and T2. Every one of them votes yes, so\n\t// the proposal certainly passes and the executor certainly runs -- if it did\n\t// not, this test would prove nothing.\n\tfor _, voter := range []address{m1, m11, m111, m1111, m2, m3} {\n\t\ttesting.SetOriginCaller(voter)\n\t\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(pid)))\n\t}\n\n\taccepted := dao.ExecuteOrRejectProposal(cross(cur), dao.ProposalID(pid))\n\turequire.Equal(t, false, accepted, \"the add must fail, since m4 is already a member\")\n\n\t// Proves the executor ran and returned an error, rather than the vote\n\t// failing to reach a threshold -- only GovDAO.ExecuteProposal writes this.\n\trendered := dao.Render(cross(cur), fmt.Sprintf(\"%d\", int(pid)))\n\turequire.Equal(t, true, contains(rendered, \"execution failed\"),\n\t\t\"the proposal must have been denied by the executor, not by the vote\")\n\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao/impl/v0\"))\n\tafter, _ := memberstore.Get(0, cur).GetMember(m1)\n\turequire.Equal(t, pointsBefore, after.InvitationPoints,\n\t\t\"a failed add must not spend the proposer's invitation point\")\n}\n\n// TestPromoteToTierMissingFromStoreKeepsTheMember covers the remove-then-add\n// hazard in the promotion executor.\n//\n// The executor removes the member and re-adds them at the new tier. SetMember\n// returns an error rather than panicking when the store has no bucket for that\n// tier, and a returned executor error does not revert -- the proposal is marked\n// rejected and the transaction still commits -- so the member would be removed\n// and not re-added.\n//\n// The two tier registries are separate. GetTier reads the global definitions,\n// which always hold T1/T2/T3; SetMember checks whether this store has a bucket,\n// and DeleteAll empties the buckets while leaving the definitions intact. So a\n// destination can pass the first check and fail the second, which is what this\n// sets up.\nfunc TestPromoteToTierMissingFromStoreKeepsTheMember(cur realm, t *testing.T) {\n\tloadMembers(cur)\n\n\t// Drop the T3 bucket while the global definition of T3 survives.\n\tmstore := memberstore.Get(0, cur)\n\tmstore.DeleteAll()\n\tmstore.SetTier(memberstore.T1)\n\tmstore.SetTier(memberstore.T2)\n\tmstore.SetMember(memberstore.T1, m1, memberByTier(memberstore.T1))\n\tmstore.SetMember(memberstore.T1, m11, memberByTier(memberstore.T1))\n\tmstore.SetMember(memberstore.T1, m111, memberByTier(memberstore.T1))\n\tmstore.SetMember(memberstore.T1, m1111, memberByTier(memberstore.T1))\n\tmstore.SetMember(memberstore.T2, m2, memberByTier(memberstore.T2))\n\tmstore.SetMember(memberstore.T2, m3, memberByTier(memberstore.T2))\n\n\t// Premise: T3 is defined globally but absent from this store.\n\t_, defined := memberstore.GetTier(memberstore.T3)\n\turequire.Equal(t, true, defined, \"premise: T3 must still be a defined tier\")\n\turequire.Equal(t, false, mstore.Has(memberstore.T3),\n\t\t\"premise: this store must have no T3 bucket, or the test proves nothing\")\n\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao/impl/v0\"))\n\tpreq := NewPromoteMemberRequest(cur, m1, memberstore.T1, memberstore.T3)\n\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\tpid := dao.MustCreateProposal(cross(cur), preq)\n\n\tfor _, voter := range []address{m1, m11, m111, m1111, m2, m3} {\n\t\ttesting.SetOriginCaller(voter)\n\t\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(pid)))\n\t}\n\n\t// The executor refuses before removing anything, and returns rather than\n\t// panicking. That rejects the proposal and closes it, and since nothing was\n\t// written, committing the rejection loses nothing.\n\t//\n\t// So the assertion below is about the chain, not the harness: the member\n\t// survives because no write happened, not because a panic reverted one.\n\t// Under the earlier panic-after-removal shape it held only because gno test\n\t// does not revert an abort, which proved nothing about production.\n\taccepted := dao.ExecuteOrRejectProposal(cross(cur), dao.ProposalID(pid))\n\turequire.Equal(t, false, accepted, \"the promotion must be rejected\")\n\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao/impl/v0\"))\n\tafter, tier := memberstore.Get(0, cur).GetMember(m1)\n\turequire.Equal(t, true, after != nil, \"the member must survive a refused promotion\")\n\turequire.Equal(t, memberstore.T1, tier, \"and must still be on their original tier\")\n\n\tloadMembers(cur)\n}\n"},{"name":"impl.gno","body":"package impl\n\nimport (\n\t\"gno.land/r/gov/dao/memberstore/v0\"\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\t// AGENTS.md: check IsCurrent() before cur.Previous() in a crossing function.\n\tif !cur.IsCurrent() {\n\t\tpanic(\"AddMember: realm value is not the caller's live cur\")\n\t}\n\t// address args are not VM-validated (the raw MsgCall string is stored), so\n\t// reject a non-bech32 addr here — matching InitWithUsers. This keeps the\n\t// member store free of unauthenticable keys and of markdown/pipe/HTML\n\t// metachars that would otherwise inject into the members render table.\n\tif !addr.IsValid() {\n\t\tpanic(\"invalid member address: \" + addr.String())\n\t}\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/loader/v0\" {\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/v0\"\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/nt/markdown/sanitize/v0\"\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/memberstore/v0\"\n\t\"gno.land/r/gov/dao/treasury/v0\"\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\t// Rejected here as well as in UpdateImpl so the mistake surfaces when the\n\t// proposal is written, not when it executes. An empty realmPkg would be\n\t// stored as an allowlist entry matching every user realm's empty\n\t// PkgPath(), and would render as a blank name in the grant sentence below.\n\tif strings.TrimSpace(realmPkg) == \"\" {\n\t\tpanic(\"realmPkg must be the realm path being granted govDAO authority\")\n\t}\n\t// Padding is rejected for the same reason UpdateImpl rejects it: the entry\n\t// is stored as given and matched whole, so a padded path grants nobody\n\t// anything. Caught here so the author sees it, not the voters.\n\tif realmPkg != strings.TrimSpace(realmPkg) {\n\t\tpanic(\"realmPkg must not have leading or trailing spaces\")\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\tcb := func(cur realm) error {\n\t\t// dao.UpdateImpl() must be cross-called from v0/impl but\n\t\t// what calls this cb function is r/gov/dao.\n\t\t// therefore we must cross back into v0/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/impl/v0\", realmPkg}))\n\t\treturn nil\n\t}\n\n\t// State the grant: this executor rewrites AllowedDAOs, and previously\n\t// rendered nothing at all. realmPkg is caller-supplied and lands in a code\n\t// span, so it is wrapped with sanitize.InlineCode — which emits its own\n\t// fence, so do not add backticks around it. (md.EscapeText is wrong here:\n\t// CommonMark 6.1 does not process backslash escapes inside code spans.)\n\t// Nothing ties realmPkg to newDao, which is why the text says so.\n\te := dao.NewSimpleExecutor(0, cur, cb, ufmt.Sprintf(\n\t\t\"Replaces the govDAO implementation with the DAO carried by this proposal.\\n\\n\"+\n\t\t\t\"After execution, only `gno.land/r/gov/dao/impl/v0` and %s may replace the \"+\n\t\t\t\"implementation, mutate the member store, or move treasury funds.\\n\\n\"+\n\t\t\t\"This grant is stated by the proposer: the incoming implementation is passed \"+\n\t\t\t\"as a value and is not verified to belong to the realm named above.\",\n\t\tsanitize.InlineCode(realmPkg)))\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// Reject a non-bech32 address at proposal-build time (see AddMember): keeps\n\t// an unauthenticable / injection-bearing key out of the member store.\n\tif !addr.IsValid() {\n\t\tpanic(\"invalid member address: \" + addr.String())\n\t}\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\t// Add the member first, spend the proposer's invitation point second.\n\t\t//\n\t\t// SetMember RETURNS an error when the address is already a member --\n\t\t// easy to reach, since AddMember enrols T3 members directly. A returned\n\t\t// executor error does not revert: ExecuteOrRejectProposal marks the\n\t\t// proposal rejected and the transaction still commits. So with the old\n\t\t// order the point was spent and the member was not added.\n\t\t//\n\t\t// RemoveInvitationPoint can still fail, despite the build-time check\n\t\t// above: it panics at zero, and AddMember spends points from this same\n\t\t// captured member, so a proposer can drain their own between creation\n\t\t// and execution. That panics and reverts, which loses nothing.\n\t\tif err := memberstore.Get(0, cur).SetMember(tier, addr, memberByTier(tier)); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tmember.RemoveInvitationPoint()\n\n\t\treturn nil\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\t// Fail on an unknown destination tier before touching the member.\n\t\t//\n\t\t// Everything that can be checked before a write is checked here, and\n\t\t// RETURNS. A returned executor error rejects the proposal and closes it,\n\t\t// and since nothing has been mutated yet that commits nothing.\n\t\t//\n\t\t// After RemoveMember the policy inverts: a return would commit a member\n\t\t// who was removed and not re-added, so everything below panics instead,\n\t\t// which reverts the whole transaction.\n\t\t// A tier dropped from the global table. NewChangeTiersRequest replaces\n\t\t// that table wholesale, so a proposal listing only T1 and T2 removes T3\n\t\t// for good. memberByTier would not notice: it switches on the constant\n\t\t// and ignores GetTier's ok, so the promotion would succeed and hand the\n\t\t// member zero invitation points. Untested -- reaching it needs a passed\n\t\t// tier-change proposal, which rewrites state every other test shares.\n\t\tif _, ok := memberstore.GetTier(toTier); !ok {\n\t\t\treturn ufmt.Errorf(\"unknown destination tier: %s\", toTier)\n\t\t}\n\n\t\tmbt := memberstore.Get(0, cur)\n\n\t\t// SetMember consults the store's own index, not the global tier table\n\t\t// above: DeleteAll empties the buckets and leaves the definitions.\n\t\tif !mbt.Has(toTier) {\n\t\t\treturn ufmt.Errorf(\"destination tier is missing from the member store: %s\", toTier)\n\t\t}\n\n\t\tprevTier := mbt.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\tif err := mbt.SetMember(toTier, addr, memberByTier(toTier)); err != nil {\n\t\t\t// Unreachable: both of SetMember's failures are ruled out above.\n\t\t\t// Panicking is what makes it safe to be wrong about that.\n\t\t\tpanic(\"promotion failed after removal: \" + err.Error())\n\t\t}\n\n\t\treturn nil\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\t// A foreign impl renders a Payment line no Banker will process, so it would\n\t// fail only at execution, after the vote. Rejected here so the proposer\n\t// sees it, not the voters. See IsCanonicalPayment.\n\tif !trs_pkg.IsCanonicalPayment(payment) {\n\t\tpanic(\"payment must be built by treasury.NewCoinsPayment or treasury.NewGRC20Payment\")\n\t}\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\t// Both are caller-supplied and land in the same body as the Payment line a\n\t// member votes on, which render.gno emits raw. InlineText folds newlines to\n\t// spaces, so neither can start a line of its own and forge that label.\n\t// Clamp before escaping, per clamp.gno.\n\tsafeReason := sanitize.InlineText(clampField(reason, maxRenderedReason))\n\tsafePayment := sanitize.InlineText(clampField(payment.String(), maxRenderedPayment))\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\tsafeReason,\n\t\t\tsafePayment,\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\tsafeReason,\n\t\t\tsafePayment,\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\t// Copied for the same reason NewCoinsPayment copies its coins: the bullet\n\t// list below is rendered once at creation, but the executor closure\n\t// re-reads the slice at execution, so the caller could otherwise install a\n\t// token set the board never saw.\n\tkeys := make([]string, len(newTokenKeys))\n\tcopy(keys, newTokenKeys)\n\n\tcb := func(cur realm) error {\n\t\treturn panictoerr.PanicToError(func() {\n\t\t\t// NOTE:: Consider checking if the keys are already registered\n\t\t\t// in the grc20reg before updating the treasury tokens keys.\n\t\t\ttreasury.SetTokenKeys(cross(cur), keys)\n\t\t})\n\t}\n\n\tbulletList := md.BulletList(keys)\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/v0\"\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/nt/bptree/pager/v0\"\n\t\"gno.land/p/nt/markdown/sanitize/v0\"\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/memberstore/v0)\\n\"\n\tout += \"## Proposals\\n\"\n\tpage, perr := ren.pssPager.GetPageByPath(url)\n\tif perr != nil {\n\t\t// A query url.Parse rejects (e.g. a control byte) is the caller's own\n\t\t// malformed input; render the default first page instead of aborting\n\t\t// this read-only render. ParseQuery already tolerates bad page/size\n\t\t// values, so every currently-working input is unaffected.\n\t\tpage = ren.pssPager.GetPage(1)\n\t}\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\t// err echoes the caller's raw pid segment (strconv quotes it but leaves\n\t\t// markdown/HTML metachars); escape+clamp it like every other untrusted\n\t\t// slot on this page, since Render is reachable unauthenticated.\n\t\treturn ufmt.Sprintf(\"# Error: Invalid proposal ID format.\\n\\n\\n%s\\n\\n\", md.EscapeText(clampField(err.Error(), maxRenderedError)))\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\t// pid is user-supplied and the proposal itself lives on the proxy, so\n\t// GetProposal can succeed for a proposal this implementation has no status\n\t// for — the same post-upgrade case PreExecuteProposal guards (statuses live\n\t// on the instance, so a fresh instance has none). Without this the page\n\t// panics at ps.String below. renderProposalListItem needs no such guard:\n\t// its pids are iterated out of pss itself, so a status always exists.\n\tps := d.pss.GetStatus(dao.ProposalID(pid))\n\tif ps == nil {\n\t\treturn ufmt.Sprintf(\"# Proposal #%v not available\\n\\nThis proposal was not created by the current govDAO implementation, so it has no voting status here.\", pid)\n\t}\n\n\tout := ufmt.Sprintf(\"## Prop #%v - %v\\n\", pid, md.EscapeText(clampField(p.Title(), maxRenderedTitle)))\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\n`, p.ExecutorString())\n\t}\n\n\t// Disclosed independently of the description: this names the realm whose\n\t// code runs if the proposal passes. Sharing the gate above hid it for every\n\t// proposal built with an empty executor description — 16 call sites across\n\t// 7 production realms, nearly all r/sys/* governance actions.\n\t//\n\t// Escaped, not trusted: CreationRealm() is dispatched through the public\n\t// dao.Executor interface, so only SimpleExecutor's value is VM-captured;\n\t// a third-party executor returns any string it likes.\n\t// Guarded on the sanitized value, not the raw one: InlineCode returns \"\"\n\t// for input that strips to nothing (bidi/zero-width only), and TrimSpace\n\t// catches plain whitespace, so neither prints a label with nothing after\n\t// it. This is tidiness, not a boundary: a zero-width/space mixture still\n\t// renders an empty-looking span, and a hostile executor can always return\n\t// a plausible-looking lie. The security property is the escaping below.\n\tif cr := sanitize.InlineCode(strings.TrimSpace(clampField(p.ExecutorCreationRealm(), maxRenderedRealm))); cr != \"\" {\n\t\tout += ufmt.Sprintf(\"Executor created in: %s\\n\", cr)\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\t// err echoes the caller's raw pid segment (strconv quotes it but leaves\n\t\t// markdown/HTML metachars); escape+clamp it like every other untrusted\n\t\t// slot on this page, since Render is reachable unauthenticated.\n\t\treturn ufmt.Sprintf(\"# Error: Invalid proposal ID format.\\n\\n\\n%s\\n\\n\", md.EscapeText(clampField(err.Error(), maxRenderedError)))\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(clampField(p.Title(), maxRenderedTitle)), 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\t// err echoes the caller's raw pid segment (strconv quotes it but leaves\n\t\t// markdown/HTML metachars); escape+clamp it like every other untrusted\n\t\t// slot on this page, since Render is reachable unauthenticated.\n\t\treturn ufmt.Sprintf(\"# Error: Invalid proposal ID format.\\n\\n\\n%s\\n\\n\", md.EscapeText(clampField(err.Error(), maxRenderedError)))\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\n// tryResolveAddr renders the author/voter as a username link. RenderLink\n// interpolates the username raw into \"[@name](/u/name)\" (r/sys/users), so this\n// is markdown-safe ONLY because r/sys/users validateName restricts names to\n// ^[a-z][a-z0-9]*([_-][a-z0-9]+)*$ (max 64) — no markdown/HTML metachar. If that\n// charset ever loosens, this line and writeVotes in types.gno need escaping.\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":"render_error_paths_filetest.gno","body":"// PKGPATH: gno.land/r/test/rendererr\npackage rendererr\n\n// Render is reachable unauthenticated via vm/qrender with an attacker-chosen\n// path. Two pre-existing defects on that path are covered here:\n//   - the \"invalid proposal id\" error echoed the raw pid segment (strconv\n//     quotes it but leaves markdown/HTML metachars), so a crafted pid injected\n//     inline markup into a govDAO-branded page;\n//   - a control byte in the query string made the pager's MustGetPageByPath\n//     panic, aborting the landing-page render.\n// Both are now escaped / degraded-gracefully.\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/impl/v0\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\n)\n\nconst user address = \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"\n\nfunc init(cur realm) {\n\tmemberstore.Get(0, cur).DeleteAll()\n\tmemberstore.Get(0, cur).SetTier(memberstore.T1)\n\tmemberstore.Get(0, cur).SetMember(memberstore.T1, user, memberstore.NewMember(3))\n\tdao.UpdateImpl(cross(cur), dao.NewUpdateRequest(impl.NewGovDAO(), nil))\n}\n\nfunc main(cur realm) {\n\ttesting.SetOriginCaller(user)\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// 1. A non-numeric pid whose segment carries an inline-markdown payload.\n\t// No '/', so it routes to {pid}; ParseInt fails and the error is escaped.\n\tpout := dao.Render(cross(cur), \"99[pwn](evil.example)\")\n\tprintln(\"payload is escaped, not a live link:\",\n\t\tstrings.Contains(pout, `\\[pwn\\]`) \u0026\u0026 !strings.Contains(pout, \"[pwn](evil.example)\"))\n\tprintln(\"still shows the invalid-id message:\",\n\t\tstrings.Contains(pout, \"Invalid proposal ID format\"))\n\n\t// Same on the /votes route.\n\tvout := dao.Render(cross(cur), \"88[x](evil.example)/votes\")\n\tprintln(\"votes route escapes it too:\",\n\t\t!strings.Contains(vout, \"[x](evil.example)\"))\n\n\t// 2. A control byte in the query — url.Parse rejects it. The landing page\n\t// must degrade to page 1, not panic.\n\tlout := dao.Render(cross(cur), \"?\\x01\")\n\tprintln(\"control-byte query renders instead of panicking:\",\n\t\tstrings.Contains(lout, \"# GovDAO\"))\n}\n\n// Output:\n// payload is escaped, not a live link: true\n// still shows the invalid-id message: true\n// votes route escapes it too: true\n// control-byte query renders instead of panicking: true\n"},{"name":"stringify_proposal_00_filetest.gno","body":"// PKGPATH: gno.land/r/test\npackage test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/impl/v0\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\n)\n\nvar (\n\ttestUser = testutils.TestAddress(\"test\")\n)\n\n// StringifyProposal carries its own copy of the disclosure expression, separate\n// from the one in render.gno. It has no production caller today, but it is\n// exported, and an untested copy of a security-relevant expression is free to\n// drift from the tested one. This executor pins it: CreationRealm is reached\n// through the public interface, so it can return anything, and the run of two\n// backticks closes a narrower fence.\ntype hostileExec struct{}\n\nfunc (e *hostileExec) Execute(cur realm) error { return nil }\n\nfunc (e *hostileExec) String() string { return \"\" }\n\n// Padded on both ends as well as fenced: without the TrimSpace, InlineCode\n// pads the fence to protect the leading and trailing spaces, and the expected\n// span below no longer matches. That makes one payload cover both the escaping\n// and the trim.\nfunc (e *hostileExec) CreationRealm() string { return \"  gno.land/r/evil`` **INJECTED**  \" }\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\tdefault:\n\t\tpanic(\"unsupported tier: \" + tier)\n\t}\n}\n\nfunc init(cur realm) {\n\t// Load members for testing\n\tmstore := memberstore.Get(0, cur)\n\tmstore.DeleteAll()\n\tmstore.SetTier(memberstore.T1)\n\tmstore.SetMember(memberstore.T1, testUser, memberByTier(memberstore.T1))\n\n\t// Set up the DAO implementation using proper constructor\n\tgovDAO := impl.NewGovDAO()\n\tdao.UpdateImpl(cross(cur), dao.NewUpdateRequest(govDAO, []string{\"gno.land/r/test\", \"gno.land/r/gov/dao/impl/v0\"}))\n}\n\nfunc main(cur realm) {\n\t// Create an executor in a specific realm to test creation realm tracking\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/template/contract\"))\n\texecutor := dao.NewSimpleExecutor(0, cur, func(realm) error { return nil }, \"Test executor description\")\n\n\t// Create a proposal request\n\tproposalRequest := dao.NewProposalRequest(\n\t\t\"Test Proposal Title\",\n\t\t\"This is a test proposal description to verify StringifyProposal works correctly.\",\n\t\texecutor,\n\t)\n\n\t// Switch to user realm to create the proposal\n\ttesting.SetOriginCaller(testUser)\n\ttesting.SetRealm(testing.NewUserRealm(testUser))\n\tpid := dao.MustCreateProposal(cross(cur), proposalRequest)\n\n\t// Get the proposal and test the core functionality\n\tprop := dao.MustGetProposal(pid)\n\n\t// Test that executor string is captured correctly\n\tprintln(\"Executor string:\", prop.ExecutorString())\n\n\t// Test that executor creation realm is captured correctly\n\tprintln(\"Executor creation realm:\", prop.ExecutorCreationRealm())\n\n\t// Stringify\n\tprintln(\"----\")\n\tprintln(impl.StringifyProposal(prop))\n\n\t// The same disclosure, through a hostile executor.\n\thpid := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(\n\t\t\"Hostile\", \"creation realm attacks the code span\", \u0026hostileExec{}))\n\thout := impl.StringifyProposal(dao.MustGetProposal(hpid))\n\n\tprintln(\"stringify widens the fence too:\",\n\t\tstrings.Contains(hout, \"```gno.land/r/evil`` **INJECTED**```\"))\n\tprintln(\"and no bold escapes it:\", !strings.Contains(hout, \"\\n**INJECTED**\"))\n}\n\n// Output:\n// Executor string: Test executor description\n// Executor creation realm: gno.land/r/template/contract\n// ----\n//\n// ### Title: Test Proposal Title\n//\n// ### Proposed by: g1w3jhxazlta047h6lta047h6lta047h6lwmjv0n\n//\n// This is a test proposal description to verify StringifyProposal works correctly.\n//\n// This proposal contains the following metadata:\n//\n// Test executor description\n//\n// Executor created in: `gno.land/r/template/contract`\n//\n// stringify widens the fence too: true\n// and no bold escapes it: true\n"},{"name":"title_clamp_filetest.gno","body":"// PKGPATH: gno.land/r/test/titleclamp\npackage titleclamp\n\n// A proposal title is attacker-chosen (by a member) and is escaped on two\n// pages: the proposal page and the list page. Escaping costs about 6,990 gas a\n// byte, and the list page escapes one title per proposal shown, so an oversized\n// title priced the whole list out of the query cap: five proposals with 90 KB\n// titles cost 3,172,507,361 gas against a 3,000,000,000 cap. Clamping the title\n// before it is escaped brings the same five to 66,871,916.\n//\n// Runs in its own realm because it creates proposals, and the unit tests in the\n// impl package assert hard-coded proposal ids against shared state.\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/impl/v0\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\n)\n\nconst user address = \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"\n\nfunc init(cur realm) {\n\tmemberstore.Get(0, cur).DeleteAll()\n\tmemberstore.Get(0, cur).SetTier(memberstore.T1)\n\tmemberstore.Get(0, cur).SetMember(memberstore.T1, user, memberstore.NewMember(3))\n\tdao.UpdateImpl(cross(cur), dao.NewUpdateRequest(impl.NewGovDAO(), nil))\n}\n\nfunc main(cur realm) {\n\ttesting.SetOriginCaller(user)\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tshort := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(\n\t\t\"An ordinary title\", \"d\", nil))\n\tlong := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(\n\t\tstrings.Repeat(\"t\", 50000), \"d\", nil))\n\n\t// An ordinary title is untouched. Real titles in examples/ are about 40\n\t// bytes, so the bound never reaches them.\n\tsout := dao.Render(cross(cur), short.String())\n\tprintln(\"an ordinary title is left alone:\",\n\t\tstrings.Contains(sout, \"An ordinary title\") \u0026\u0026 !strings.Contains(sout, \"… truncated\"))\n\n\t// The proposal page cuts the title before escaping it. Asserting the marker\n\t// rather than a length: escaping first and cutting second would also\n\t// produce a short page, so length alone cannot tell the two apart.\n\tpout := dao.Render(cross(cur), long.String())\n\tprintln(\"the proposal page clamps a huge title:\",\n\t\tstrings.Contains(pout, \"… truncated\") \u0026\u0026 len(pout) \u003c 4000)\n\n\t// The list page escapes one title per proposal it shows, so it is the page\n\t// the bound actually protects.\n\tlout := dao.Render(cross(cur), \"\")\n\tprintln(\"the list page clamps it too:\",\n\t\tstrings.Contains(lout, \"… truncated\") \u0026\u0026 len(lout) \u003c 4000)\n}\n\n// Output:\n// an ordinary title is left alone: true\n// the proposal page clamps a huge title: true\n// the list page clamps it too: true\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/markdown/sanitize/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\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\t// DeniedReason is \"execution failed: \" + err.Error() from the\n\t\t\t// proposal's executor callback, which any third-party realm can\n\t\t\t// supply. Raw, it wrote attacker-controlled markdown directly beneath\n\t\t\t// the \"PROPOSAL HAS BEEN DENIED\" line. InlineText folds newlines, so\n\t\t\t// the reason cannot leave the REASON: line, and escapes emphasis and\n\t\t\t// raw HTML.\n\t\t\tsb.WriteString(sanitize.InlineText(clampField(ps.DeniedReason, maxRenderedReason)))\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`, p.ExecutorString())\n\t}\n\n\t// Disclosed independently of the metadata string, and escaped, for the\n\t// same reasons as render.renderProposalPage.\n\tif cr := sanitize.InlineCode(strings.TrimSpace(clampField(p.ExecutorCreationRealm(), maxRenderedRealm))); cr != \"\" {\n\t\tout += ufmt.Sprintf(\"\\nExecutor created in: %s\\n\", cr)\n\t}\n\n\treturn out\n}\n"},{"name":"z_addr_validation_test.gno","body":"package impl\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/urequire/v0\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\n)\n\n// Member addresses arrive as raw MsgCall strings (the VM does not validate an\n// `address` arg), so a non-bech32 value — blank, oversized, or carrying\n// markdown/pipe/HTML metachars — would otherwise be stored and then injected\n// into the members render table. Both admission points reject it, matching the\n// InitWithUsers precedent.\nfunc TestNewAddMemberRequestRejectsInvalidAddress(cur realm, t *testing.T) {\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\n\tfor _, bad := range []string{\"evil|**pwn**\", \"\", \"not-an-address\"} {\n\t\turequire.PanicsWithMessage(t, cur,\n\t\t\t\"invalid member address: \"+bad,\n\t\t\tfunc() {\n\t\t\t\tNewAddMemberRequest(cur, address(bad), memberstore.T2, \"portfolio\")\n\t\t\t})\n\t}\n}\n"},{"name":"z_denied_reason_store_test.gno","body":"package impl\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\n)\n\n// Named z_* so it sorts after govdao_test.gno, whose TestUpgradeDaoImplementation\n// asserts a hard-coded proposal id. This builds its own GovDAO instance and\n// registers a status directly, so it never touches the shared one.\n\ntype hugeErrExecutor struct{ msg string }\n\nfunc (e *hugeErrExecutor) Execute(cur realm) error { return errors.New(e.msg) }\n\nfunc (e *hugeErrExecutor) String() string { return \"\" }\n\nfunc (e *hugeErrExecutor) CreationRealm() string { return \"gno.land/r/test/hugeerr\" }\n\n// The denial reason is stored, not just shown. It is \"execution failed: \" plus\n// whatever error the proposal's executor returned, and the executor is\n// third-party code. Whoever executes the proposal pays the storage deposit for\n// it, and that need not be the person who wrote the executor.\n//\n// This has to read the stored value rather than the rendered page. The render\n// clamps to the same bound, so a page rendered from an unclamped store looks\n// exactly like one rendered from a clamped store — testing through Render would\n// pass whether or not the write is bounded.\nfunc TestDeniedReasonIsClampedBeforeStoring(cur realm, t *testing.T) {\n\tg := NewGovDAO()\n\tpid := dao.ProposalID(987654)\n\tg.pss.Set(pid.String(), newProposalStatus([]string{memberstore.T1}))\n\n\tg.ExecuteProposal(0, cur, pid, \u0026hugeErrExecutor{msg: strings.Repeat(\"E\", 50000)})\n\n\tstored := g.pss.GetStatus(pid).DeniedReason\n\tuassert.True(t, len(stored) \u003c maxRenderedReason+64,\n\t\t\"the realm must not store a reason larger than it can display\")\n\tuassert.True(t, strings.Contains(stored, \"… truncated\"),\n\t\t\"a stored reason that was cut must say so\")\n\tuassert.True(t, strings.HasPrefix(stored, \"execution failed: E\"),\n\t\t\"the prefix and the start of the error must survive\")\n}\n"},{"name":"z_treasury_payment_request_test.gno","body":"package impl\n\nimport (\n\t\"testing\"\n\n\ttrs_pkg \"gno.land/p/nt/treasury/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\n// Named z_* for the same reason as z_upgrade_request_test.gno: govdao_test.gno\n// asserts hard-coded proposal ids, and this test creates no proposal.\n\n// foreignPayment satisfies trs_pkg.Payment without being one of treasury's own\n// impls. BankerID names a banker the treasury really has registered, and\n// String says whatever this realm likes.\ntype foreignPayment struct{}\n\nfunc (foreignPayment) BankerID() string { return \"Coins\" }\n\nfunc (foreignPayment) String() string { return \"42ugnot to g1nobody\" }\n\n// The immutability NewCoinsPayment provides is a property of the concrete\n// type, not of the Payment interface: a foreign impl renders a payment line\n// members vote on and then fails CoinsBanker.Send's type assertion at\n// execution, spending a whole vote cycle on a proposal that can never\n// execute. Rejected at construction so the proposer sees it, not the voters.\nfunc TestTreasuryPaymentRequestRejectsForeignPayment(cur realm, t *testing.T) {\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\n\t// PanicsWithMessage, not AbortsWithMessage: same-realm call, so the guard\n\t// raises a plain panic rather than a cross-realm abort.\n\turequire.PanicsWithMessage(t, cur,\n\t\t\"payment must be built by treasury.NewCoinsPayment or treasury.NewGRC20Payment\",\n\t\tfunc() {\n\t\t\tNewTreasuryPaymentRequest(cur, foreignPayment{}, \"a reason\")\n\t\t})\n}\n\n// The check must not reject the real thing. GRC20 is used here because the\n// Coins banker is the one govdao_test.gno's shared state leaves funded; both\n// are canonical, and only the canonicality gate is under test.\nfunc TestTreasuryPaymentRequestAcceptsCanonicalPayment(cur realm, t *testing.T) {\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\n\turequire.NotPanics(t, cur, func() {\n\t\tNewTreasuryPaymentRequest(cur, trs_pkg.NewGRC20Payment(\"TOK\", 1, m1), \"a reason\")\n\t})\n}\n"},{"name":"z_unknown_proposal_test.gno","body":"package impl\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/urequire/v0\"\n\t\"gno.land/r/gov/dao\"\n)\n\n// Named z_* so it sorts after govdao_test.gno, whose TestUpgradeDaoImplementation\n// swaps the DAO implementation and asserts a hard-coded proposal id. This test\n// creates no proposal, so it is safe to run against the shared state.\n\n// A proposal id this implementation has no status for — an unknown id, or a\n// proposal that outlived the GovDAO instance that created it (statuses live on\n// the instance). The nil deref this used to hit surfaced as \"runtime error:\n// nil pointer dereference\", which reads like a VM fault rather than a bad\n// request.\n//\n// Note what this does NOT assert: that the proposal becomes rejectable.\n// dao.executeProposal panics on ANY error from PreExecuteProposal, ahead of\n// its execErrorRejects branch, so the guard buys a named error and nothing\n// more.\nfunc TestExecuteUnknownProposalNamesTheError(cur realm, t *testing.T) {\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\n\turequire.AbortsWithMessage(t, cur, \"proposal not found\", func() {\n\t\tdao.ExecuteProposal(cross(cur), dao.ProposalID(999999))\n\t})\n}\n"},{"name":"z_upgrade_request_test.gno","body":"package impl\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/urequire/v0\"\n\t\"gno.land/r/gov/dao\"\n)\n\n// Named z_* so it sorts after govdao_test.gno, whose TestUpgradeDaoImplementation\n// swaps the DAO implementation and asserts a hard-coded proposal id. This test\n// creates no proposal, so it is safe to run against the shared state.\n\n// An empty realmPkg would be stored verbatim as an allowlist entry, and\n// InAllowedDAOs compares by exact string against a caller's PkgPath() — which\n// is \"\" for a user realm. It would also render as a blank name in the grant\n// sentence, so the proposal would not say who it was empowering. Rejected at\n// proposal-construction time so the mistake surfaces to the author rather than\n// at execution. UpdateImpl rejects it too; see r/gov/dao/allowlist_test.gno.\nfunc TestUpgradeRequestRejectsBlankRealmPkg(cur realm, t *testing.T) {\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\n\tfor _, padded := range []string{\" gno.land/r/x\", \"gno.land/r/x \"} {\n\t\turequire.PanicsWithMessage(t, cur,\n\t\t\t\"realmPkg must not have leading or trailing spaces\",\n\t\t\tfunc() {\n\t\t\t\tNewUpgradeDaoImplRequest(cur, \u0026mockDAO{}, padded, \"reason\")\n\t\t\t})\n\t}\n\n\tfor _, blank := range []string{\"\", \"   \"} {\n\t\t// PanicsWithMessage, not AbortsWithMessage: this is a same-realm call,\n\t\t// so the guard raises a plain panic rather than a cross-realm abort.\n\t\turequire.PanicsWithMessage(t, cur,\n\t\t\t\"realmPkg must be the realm path being granted govDAO authority\",\n\t\t\tfunc() {\n\t\t\t\tNewUpgradeDaoImplRequest(cur, \u0026mockDAO{}, blank, \"reason\")\n\t\t\t})\n\t}\n}\n\ntype mockDAO struct{}\n\nfunc (d *mockDAO) PreCreateProposal(_ int, rlm realm, r dao.ProposalRequest) (address, error) {\n\treturn m1, nil\n}\n\nfunc (d *mockDAO) PostCreateProposal(_ int, rlm realm, r dao.ProposalRequest, pid dao.ProposalID) {}\n\nfunc (d *mockDAO) VoteOnProposal(_ int, rlm realm, r dao.VoteRequest) error { return nil }\n\nfunc (d *mockDAO) PreExecuteProposal(_ int, rlm realm, pid dao.ProposalID) (bool, error) {\n\treturn true, nil\n}\n\nfunc (d *mockDAO) ExecuteProposal(_ int, rlm realm, pid dao.ProposalID, e dao.Executor) error {\n\treturn nil\n}\n\nfunc (d *mockDAO) Render(cur realm, pkgpath string, path string) string { return \"\" }\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"ENThg+UG9Wu4eoNPIAcw4Tycwe1PBiDFcszk/b4QtENJ45+r58pY3jIb6qYkf7zL0CLZ/jKdwaIU4nm8XsEWgA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1g73v2anukg4ej7axwqpthsatzrxjsh0wk797da","package":{"name":"init","path":"gno.land/r/gov/dao/init/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/gov/dao/init/v0\"\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/impl/v0\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\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/impl/v0\"}))\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/impl/v0\"}))\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"1IXR4Cjm83B4QpLilMroNWPcMIZEfYwROUCTeKmUiwVQPUbHXEZhk5SNlS3CUhn69YjaEa5TPHZG4IB9RcK2GQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l","package":{"name":"params","path":"gno.land/r/sys/params","files":[{"name":"delegate.gno","body":"package params\n\n// Delegation: letting a realm other than GovDAO manage one specific parameter.\n//\n// This realm holds a capability nothing else can: the `sys/params` stdlib\n// refuses every caller except `gno.land/r/sys/params`, checked in the VM by\n// package path, and the same gate covers the getters. So every parameter write\n// on the chain physically originates here, and \"delegating a parameter\" can only\n// mean adding a path in this file that authorizes someone other than a GovDAO\n// vote.\n//\n// Two shapes are deliberately NOT used.\n//\n// Not a registry of key -\u003e realm. This realm can never be redeployed\n// (AddPackage refuses an occupied path), and the stdlib gate names this exact\n// path, so making a NEW parameter delegatable already requires editing this\n// file, which means a chain relaunch. A registry's runtime generality would\n// therefore never be exercised: the only freedom that matters is \"which realm,\n// or none\" for keys already blessed in source. A named slot says exactly that\n// and nothing more. It also removes a whole class of mistake — there is no\n// key string to mis-compose, no container to accidentally expose, and no way for\n// a proposal to name a key the author never considered. A shape-based allowlist\n// like \"\u003cmodule\u003e:p:\u003cname\u003e\" would have admitted bank:p:restricted_denoms and\n// auth:p:unrestricted_addrs, which is not a delegation anyone asked for.\n//\n// Not a capability object. Returning something the delegate holds would make\n// authority survive revocation, because the check would have happened at\n// construction. The authority is re-checked on every crossing call instead, so\n// clearing the slot takes effect immediately.\n\nimport (\n\t\"chain\"\n\n\t\"gno.land/r/gov/dao\"\n)\n\n// Event names. PascalCase with a named const is the house style across r/sys\n// and r/gov; the bare lowercase \"set\" elsewhere in this realm predates it.\nconst (\n\tDelegateSetEvent     = \"ParamDelegateSet\"\n\tDelegateClearedEvent = \"ParamDelegateCleared\"\n\tDelegateWriteEvent   = \"ParamDelegateWrite\"\n)\n\n// assertDelegate authorizes a caller as the holder of a delegated capability.\n//\n// want is the authorized package path, or \"\" when nothing is delegated.\n//\n// The empty check comes FIRST and is load-bearing, not defensive. A direct call\n// from a user account has an empty previous package path — that is exactly what\n// IsUserCall tests — so comparing against an unset slot would compare \"\" to \"\"\n// and admit every user on the chain. r/gov/dao's own allowlist has the\n// mirror-image bug (it returns true for everyone when the list is empty), kept\n// there deliberately for genesis bootstrap. Nothing here needs that, so empty\n// denies.\n//\n// Matching is exact, never a prefix. A sub-realm identity minted by\n// cur.Sub(subpath) presents the synthesized path \"host#subpath\", so a delegate\n// may legitimately be a single DAO hosted by a multi-tenant realm — for\n// instance \"gno.land/r/nt/commondao/v0#dao/42\". An anchored-prefix match on the\n// host would hand the capability to every DAO that realm hosts, and a bare\n// prefix match would additionally match sibling packages. \"#\" cannot occur in a\n// real package path, so exact matching on the full string is unambiguous.\n//\n// Takes rlm in the non-crossing dispatch position, matching assertValsetCaller\n// so the two gates stay one shape.\n// subject names what is being written, so the refusal stays specific to the\n// capability rather than generic to the mechanism. params_valset_auth.txtar\n// pins valset's exact wording as a regression test for PR #5485.\nfunc assertDelegate(_ int, rlm realm, want, subject string) {\n\tif want == \"\" {\n\t\tpanic(\"unauthorized: no delegate is configured for \" + subject)\n\t}\n\t// The helper trusts its rlm input, so a future caller threading a stashed\n\t// or sibling-frame realm value would otherwise bypass the path check below.\n\tif !rlm.IsCurrent() {\n\t\tpanic(\"unauthorized: rlm is not the caller's live cur\")\n\t}\n\tif rlm.Previous().PkgPath() != want {\n\t\tpanic(\"unauthorized: only \" + want + \" may write \" + subject)\n\t}\n}\n\n// assertDelegatePath rejects package paths that must never be stored in a\n// delegation slot, at the point a proposal is built rather than when it\n// executes.\n//\n// Rejecting the empty string is the same fail-open guard as in assertDelegate,\n// enforced on the way in as well so an accidental clear-by-empty-set cannot be\n// mistaken for a grant. Requiring the gno.land/r/ prefix excludes user accounts\n// (empty path), pure packages, and ephemeral `maketx run` realms — the last of\n// which matters because a run realm's path is non-empty, so a check based on\n// \"is this code\" would let one through.\nfunc assertDelegatePath(pkgpath string) {\n\tif pkgpath == \"\" {\n\t\tpanic(\"invalid delegate: empty package path\")\n\t}\n\tif !isRealmPath(pkgpath) {\n\t\tpanic(\"invalid delegate: must be a gno.land/r/ realm path, got \" + pkgpath)\n\t}\n\t// Reject anything no caller could ever present, so a delegation cannot be\n\t// silently dead: GovDAO would believe it granted the capability while the\n\t// delegate is refused on every call. The sibling UpdateImpl rejects\n\t// whitespace-padded entries for the same reason.\n\tfor i := 0; i \u003c len(pkgpath); i++ {\n\t\tc := pkgpath[i]\n\t\tswitch {\n\t\tcase c \u003e= 'a' \u0026\u0026 c \u003c= 'z', c \u003e= '0' \u0026\u0026 c \u003c= '9':\n\t\tcase c == '/' || c == '.' || c == '-' || c == '_' || c == '#':\n\t\tdefault:\n\t\t\tpanic(\"invalid delegate: \" + pkgpath +\n\t\t\t\t\" contains a character no package path or sub-identity can hold\")\n\t\t}\n\t}\n\tif n := countByte(pkgpath, '#'); n \u003e 1 {\n\t\tpanic(\"invalid delegate: more than one '#' in \" + pkgpath)\n\t}\n}\n\nfunc countByte(s string, b byte) int {\n\tn := 0\n\tfor i := 0; i \u003c len(s); i++ {\n\t\tif s[i] == b {\n\t\t\tn++\n\t\t}\n\t}\n\treturn n\n}\n\nconst realmPathPrefix = \"gno.land/r/\"\n\nfunc isRealmPath(pkgpath string) bool {\n\treturn len(pkgpath) \u003e len(realmPathPrefix) \u0026\u0026\n\t\tpkgpath[:len(realmPathPrefix)] == realmPathPrefix\n}\n\n// newDelegateProposal builds the GovDAO proposal that applies a delegation\n// change. apply mutates the slot and is run only when the vote passes.\n//\n// event is emitted from inside the executor, so an observer sees the delegation\n// change exactly when it takes effect rather than when it was proposed. from is\n// the previous holder (\"\" when none), included so a re-delegation is auditable\n// from the single event without reading prior state.\n// apply returns the holder it replaced, read at EXECUTION time. Capturing that\n// at creation would misreport a re-delegation: two set-proposals created while\n// the slot is empty, then executed in sequence, would both emit from=\"\" even\n// though the second replaced the first.\nfunc newDelegateProposal(cur realm, event, key, to, title, desc string, apply func() string) dao.ProposalRequest {\n\tcallback := func(cur realm) error {\n\t\tfrom := apply()\n\t\tchain.Emit(event, \"key\", key, \"from\", from, \"to\", to)\n\t\treturn nil\n\t}\n\treturn dao.NewProposalRequest(title, desc,\n\t\tdao.NewSimpleExecutor(0, cur, callback, \"\"))\n}\n"},{"name":"delegate_test.gno","body":"package params\n\nimport (\n\t\"testing\"\n\n\tprms \"sys/params\"\n\n\t\"gno.land/p/moul/addrset/v0\"\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nconst (\n\ttestDelegate = \"gno.land/r/test/delegate\"\n\ttestOther    = \"gno.land/r/test/other\"\n\t// A sub-realm identity, the form a single DAO hosted by a multi-tenant\n\t// realm presents. \"#\" cannot occur in a real package path.\n\ttestSubDelegate = \"gno.land/r/nt/commondao/v0#dao/42\"\n)\n\n// resetDelegation returns the slot to undelegated. The realm's tests share\n// package state, so anything touching the slot must put it back.\nfunc resetDelegation() {\n\trunSubmittersMgr = \"\"\n\trunSubmittersGrants = addrset.Set{}\n}\n\n// armRunSubmitters puts addresses on the list the way GovDAO does: by writing\n// the parameter, not through the delegate.\n//\n// Tests need it because the delegate may no longer arm an empty list. An empty\n// run_submitters means the gate is off, so the first add would switch a\n// chain-wide restriction on rather than curate one, and that is a vote.\nfunc armRunSubmitters(addrs ...string) {\n\tprms.UpdateSysParamStrings(vmModulePrefix, vmParamsSubmodule, runSubmittersKey, addrs, true)\n}\n\n// unarmRunSubmitters drops addresses the way a GovDAO vote does, bypassing the\n// delegate's grant-scoping. Used to build a list whose every remaining entry was\n// granted by the delegate, which is the only state where the non-empty floor is\n// the binding constraint.\nfunc unarmRunSubmitters(addrs ...string) {\n\tprms.UpdateSysParamStrings(vmModulePrefix, vmParamsSubmodule, runSubmittersKey, addrs, false)\n}\n\n// TestRunSubmittersUndelegatedDeniesEveryone is the single most important test\n// here: with no delegate configured, the slot is \"\" and a direct user call also\n// presents \"\". A gate that compared the two without checking for empty first\n// would admit every account on the chain.\nfunc TestRunSubmittersUndelegatedDeniesEveryone(cur realm, t *testing.T) {\n\tresetDelegation()\n\tdefer resetDelegation()\n\n\tuassert.Equal(t, \"\", RunSubmittersManager())\n\n\t// A user account: PkgPath() is empty, matching the empty slot.\n\ttesting.SetRealm(testing.NewUserRealm(testutils.TestAddress(\"nobody\")))\n\tuassert.AbortsContains(t, cur, \"no delegate is configured\", func() {\n\t\tAddRunSubmitters(cross(cur), []string{testutils.TestAddress(\"victim\").String()})\n\t})\n\n\t// And a code realm, for completeness.\n\ttesting.SetRealm(testing.NewCodeRealm(testOther))\n\tuassert.AbortsContains(t, cur, \"no delegate is configured\", func() {\n\t\tAddRunSubmitters(cross(cur), []string{testutils.TestAddress(\"victim\").String()})\n\t})\n}\n\n// TestRunSubmittersOnlyTheDelegateMayWrite pins that authorization is by exact\n// package path.\nfunc TestRunSubmittersOnlyTheDelegateMayWrite(cur realm, t *testing.T) {\n\tresetDelegation()\n\tdefer resetDelegation()\n\n\trunSubmittersMgr = testDelegate\n\tarmRunSubmitters(testutils.TestAddress(\"seeded-by-vote\").String())\n\taddr := testutils.TestAddress(\"granted\").String()\n\n\t// A different realm is refused.\n\ttesting.SetRealm(testing.NewCodeRealm(testOther))\n\tuassert.AbortsContains(t, cur, \"unauthorized\", func() {\n\t\tAddRunSubmitters(cross(cur), []string{addr})\n\t})\n\n\t// A user account is refused: an empty path must not match a set slot.\n\ttesting.SetRealm(testing.NewUserRealm(testutils.TestAddress(\"nobody\")))\n\tuassert.AbortsContains(t, cur, \"unauthorized\", func() {\n\t\tAddRunSubmitters(cross(cur), []string{addr})\n\t})\n\n\t// The delegate itself succeeds.\n\ttesting.SetRealm(testing.NewCodeRealm(testDelegate))\n\tAddRunSubmitters(cross(cur), []string{addr})\n\tuassert.True(t, contains(GetRunSubmitters(), addr),\n\t\t\"the delegate's addition must reach the parameter\")\n}\n\n// TestRunSubmittersSubRealmIdentityIsExact pins that delegate matching is exact\n// string equality, which is what makes a per-DAO delegation safe.\n//\n// A sub-realm identity minted by cur.Sub(subpath) presents \"host#subpath\", so a\n// single DAO hosted by a multi-tenant realm can hold the capability. Matching by\n// prefix instead would hand it to every DAO that host serves -- and CommonDAO\n// membership, while invite-gated, is unlimited once invited.\n//\n// Asserted through the pure predicate rather than by crossing: constructing a\n// live sub-realm cur is not something the test harness can do (NewCodeRealm\n// rejects \"#\", and MakeRealm does not satisfy IsCurrent), and the comparison\n// under test is the same one assertDelegate performs.\nfunc TestRunSubmittersSubRealmIdentityIsExact(t *testing.T) {\n\tresetDelegation()\n\tdefer resetDelegation()\n\n\trunSubmittersMgr = testSubDelegate\n\n\tuassert.True(t, IsRunSubmittersDelegate(testSubDelegate),\n\t\t\"the named DAO holds the capability\")\n\tuassert.False(t, IsRunSubmittersDelegate(\"gno.land/r/nt/commondao/v0\"),\n\t\t\"the host realm must not inherit its sub-identity's authority\")\n\tuassert.False(t, IsRunSubmittersDelegate(\"gno.land/r/nt/commondao/v0#dao/43\"),\n\t\t\"a sibling DAO of the same host must not match\")\n\tuassert.False(t, IsRunSubmittersDelegate(\"gno.land/r/nt/commondao/v0#dao/4\"),\n\t\t\"a prefix of the subpath must not match\")\n\tuassert.False(t, IsRunSubmittersDelegate(\"\"),\n\t\t\"an empty path must never match a configured delegate\")\n}\n\n// TestRunSubmittersRemoveIsScopedToOwnGrants pins that the delegate cannot\n// remove an address it did not add, so a list GovDAO curated survives a rogue\n// delegate. The companion bound -- that it can never take the list to zero by\n// any route -- is TestRunSubmittersCannotEmptyTheList.\nfunc TestRunSubmittersRemoveIsScopedToOwnGrants(cur realm, t *testing.T) {\n\tresetDelegation()\n\tdefer resetDelegation()\n\n\tpreexisting := testutils.TestAddress(\"breakglass\").String()\n\ttesting.SetSysParamStrings(vmModulePrefix, vmParamsSubmodule, runSubmittersKey,\n\t\t[]string{preexisting})\n\n\trunSubmittersMgr = testDelegate\n\tarmRunSubmitters(testutils.TestAddress(\"seeded-by-vote\").String())\n\town := testutils.TestAddress(\"ownadd\").String()\n\n\ttesting.SetRealm(testing.NewCodeRealm(testDelegate))\n\tAddRunSubmitters(cross(cur), []string{own})\n\n\t// It may remove what it granted.\n\tuassert.True(t, RunSubmittersGrantedBy(address(own)))\n\tRemoveRunSubmitters(cross(cur), []string{own})\n\tuassert.False(t, contains(GetRunSubmitters(), own))\n\n\t// It may NOT remove the address that predated the delegation.\n\tuassert.False(t, RunSubmittersGrantedBy(address(preexisting)))\n\tuassert.AbortsContains(t, cur, \"only GovDAO may remove it\", func() {\n\t\tRemoveRunSubmitters(cross(cur), []string{preexisting})\n\t})\n\tuassert.True(t, contains(GetRunSubmitters(), preexisting),\n\t\t\"the pre-existing entry must survive a refused removal\")\n}\n\n// TestRunSubmittersCannotEmptyTheList pins the non-empty floor.\n//\n// An empty run_submitters means the gate is OFF: anyone on the chain may\n// MsgRun. So emptying the list is not a smaller version of removing one\n// address, it is the unilateral revocation of the whole restriction GovDAO\n// voted for -- the one thing this capability must not be able to do.\n//\n// Grant-scoping alone does not prevent it. It holds only while an entry the\n// delegate did not grant survives, and GovDAO replacing the list wholesale can\n// remove its own entries without touching the grant record, which is exactly\n// the state set up below.\nfunc TestRunSubmittersCannotEmptyTheList(cur realm, t *testing.T) {\n\tresetDelegation()\n\tdefer resetDelegation()\n\n\trunSubmittersMgr = testDelegate\n\tseed := testutils.TestAddress(\"seeded-by-vote\").String()\n\tarmRunSubmitters(seed)\n\ta := testutils.TestAddress(\"granted-a\").String()\n\tb := testutils.TestAddress(\"granted-b\").String()\n\n\ttesting.SetRealm(testing.NewCodeRealm(testDelegate))\n\tAddRunSubmitters(cross(cur), []string{a, b})\n\n\t// GovDAO then drops its own seed by vote, which grant-scoping does not\n\t// constrain. This is the only way to reach a list whose every entry was\n\t// granted by the delegate, now that the delegate cannot arm an empty one --\n\t// and it is the state where the floor is the last thing standing.\n\tunarmRunSubmitters(seed)\n\tuassert.Equal(t, 2, len(GetRunSubmitters()))\n\n\t// Every listed address is now one the delegate granted, so grant-scoping\n\t// permits removing all of them. The floor is the only thing left.\n\tuassert.True(t, RunSubmittersGrantedBy(address(a)))\n\tuassert.True(t, RunSubmittersGrantedBy(address(b)))\n\n\t// Down to one is fine: shrinking the list is the delegate's job.\n\tRemoveRunSubmitters(cross(cur), []string{a})\n\tuassert.Equal(t, 1, len(GetRunSubmitters()))\n\n\t// The last one is not.\n\tuassert.AbortsContains(t, cur, \"refusing to empty\", func() {\n\t\tRemoveRunSubmitters(cross(cur), []string{b})\n\t})\n\tuassert.True(t, contains(GetRunSubmitters(), b),\n\t\t\"the last entry must survive a refused removal\")\n\n\t// Nor in one call, and nor by naming addresses that are not listed: the\n\t// count is taken against the parameter, not the argument.\n\tuassert.AbortsContains(t, cur, \"refusing to empty\", func() {\n\t\tRemoveRunSubmitters(cross(cur), []string{b, b})\n\t})\n\tuassert.True(t, contains(GetRunSubmitters(), b))\n\n\t// And the delegate is not locked out of its ordinary work: adding still\n\t// works, and once there are two again it may remove one.\n\tAddRunSubmitters(cross(cur), []string{a})\n\tRemoveRunSubmitters(cross(cur), []string{b})\n\tuassert.True(t, contains(GetRunSubmitters(), a))\n\tuassert.False(t, contains(GetRunSubmitters(), b))\n}\n\n// TestRunSubmittersRevocationDropsGrantRecord pins that a new holder does not\n// inherit removal authority over its predecessor's grants.\nfunc TestRunSubmittersRevocationDropsGrantRecord(cur realm, t *testing.T) {\n\tresetDelegation()\n\tdefer resetDelegation()\n\n\trunSubmittersMgr = testDelegate\n\tarmRunSubmitters(testutils.TestAddress(\"seeded-by-vote\").String())\n\taddr := testutils.TestAddress(\"byfirst\").String()\n\ttesting.SetRealm(testing.NewCodeRealm(testDelegate))\n\tAddRunSubmitters(cross(cur), []string{addr})\n\tuassert.True(t, RunSubmittersGrantedBy(address(addr)))\n\n\t// Hand the capability over, as the proposal executor would.\n\trunSubmittersMgr = testOther\n\trunSubmittersGrants = addrset.Set{}\n\n\ttesting.SetRealm(testing.NewCodeRealm(testOther))\n\tuassert.False(t, RunSubmittersGrantedBy(address(addr)))\n\tuassert.AbortsContains(t, cur, \"only GovDAO may remove it\", func() {\n\t\tRemoveRunSubmitters(cross(cur), []string{addr})\n\t})\n}\n\n// TestDelegatePathValidation pins what may be stored in a slot. An ephemeral\n// `maketx run` realm has a NON-empty path, so a check for \"is this code\" would\n// let one through; only the gno.land/r/ requirement excludes it.\nfunc TestDelegatePathValidation(t *testing.T) {\n\tfor _, bad := range []string{\n\t\t\"\",\n\t\t\"gno.land/p/nt/avl/v0\",\n\t\t\"gno.land/e/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5/run\",\n\t\t\"gno.land/r/\",\n\t\t// Paths no caller could ever present. Storing one would leave GovDAO\n\t\t// believing it delegated while the delegate is refused on every call.\n\t\t\"gno.land/r/test/delegate \",                // trailing space\n\t\t\" gno.land/r/test/delegate\",                // leading space\n\t\t\"gno.land/r/nt/commondao/v0#DAO/42\",        // uppercase subpath\n\t\t\"gno.land/r/nt/commondao/v0#dao/42#dao/43\", // two separators\n\t} {\n\t\t// PanicsContains, not AbortsContains: this is a same-realm call, so\n\t\t// the panic never crosses a realm boundary and is not an abort.\n\t\tuassert.PanicsContains(t, cur, \"invalid delegate\", func() {\n\t\t\tassertDelegatePath(bad)\n\t\t})\n\t}\n\t// Sound paths, including a sub-realm identity.\n\tassertDelegatePath(testDelegate)\n\tassertDelegatePath(testSubDelegate)\n}\n\n// TestValsetGateRejectsForeignRealm closes a pre-existing coverage gap found\n// while factoring the shared gate: assertValsetCaller had no test in this realm\n// at all, so nothing asserted that a realm other than r/sys/validators/v0 is\n// refused.\nfunc TestValsetGateRejectsForeignRealm(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(testOther))\n\tuassert.AbortsContains(t, cur, \"unauthorized\", func() {\n\t\tSetValsetProposal(cross(cur), []string{\"somepubkey:1\"})\n\t})\n\n\t// A user account must be refused identically -- the empty-path case.\n\ttesting.SetRealm(testing.NewUserRealm(testutils.TestAddress(\"nobody\")))\n\tuassert.AbortsContains(t, cur, \"unauthorized\", func() {\n\t\tSetValsetProposal(cross(cur), []string{\"somepubkey:1\"})\n\t})\n}\n\n// TestAssertNotValsetKeyRejectsGenericFactory closes the other gap: the guard\n// whose own comment says it stops \"any GovDAO supermajority\" from writing\n// validator-set state through the generic param factory had no test.\nfunc TestAssertNotValsetKeyRejectsGenericFactory(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(testutils.TestAddress(\"proposer\")))\n\tuassert.AbortsContains(t, cur, \"reserved for\", func() {\n\t\tNewSysParamStringsPropRequest(cross(cur), \"node\", \"valset\", \"proposed\",\n\t\t\t[]string{\"somepubkey:1\"})\n\t})\n}\n\nfunc contains(haystack []string, needle string) bool {\n\tfor _, h := range haystack {\n\t\tif h == needle {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// TestRunSubmittersGrantLaunderingIsRefused reproduces a real hole in the first\n// version of this code, found by audit.\n//\n// UpdateSysParamStrings dedupes on add, so re-adding an address already on the\n// list is a no-op on the parameter. The first implementation recorded a grant\n// for every argument regardless, which let the delegate launder authority over\n// entries it never granted:\n//\n//\tread the list -\u003e re-add all of it -\u003e remove all of it\n//\n// The parameter never changed on the middle step, but every address became\n// \"granted by me\", so the removal was permitted and the allowlist ended empty --\n// including the entry that predated the delegation. Since GovDAO proposal\n// creation is MsgRun-only, an empty run_submitters means no proposal can be\n// created to revoke the delegate or restore the list. That is a chain brick\n// recoverable only by relaunch.\nfunc TestRunSubmittersGrantLaunderingIsRefused(cur realm, t *testing.T) {\n\tresetDelegation()\n\tdefer resetDelegation()\n\n\tbreakglass := testutils.TestAddress(\"breakglass2\").String()\n\ttesting.SetSysParamStrings(vmModulePrefix, vmParamsSubmodule, runSubmittersKey,\n\t\t[]string{breakglass})\n\n\trunSubmittersMgr = testDelegate\n\tarmRunSubmitters(testutils.TestAddress(\"seeded-by-vote\").String())\n\ttesting.SetRealm(testing.NewCodeRealm(testDelegate))\n\n\t// Step 1: re-add what is already there. A no-op on the parameter, and it\n\t// must NOT create a grant.\n\tAddRunSubmitters(cross(cur), GetRunSubmitters())\n\tuassert.False(t, RunSubmittersGrantedBy(address(breakglass)),\n\t\t\"re-adding an existing address must not record a grant for it\")\n\n\t// Step 2: the removal must therefore still be refused.\n\tuassert.AbortsContains(t, cur, \"only GovDAO may remove it\", func() {\n\t\tRemoveRunSubmitters(cross(cur), []string{breakglass})\n\t})\n\tuassert.True(t, contains(GetRunSubmitters(), breakglass),\n\t\t\"the pre-existing entry must survive\")\n\n\t// And the whole-list version of the same attack, which is how it would\n\t// actually be run.\n\tAddRunSubmitters(cross(cur), GetRunSubmitters())\n\tuassert.AbortsContains(t, cur, \"only GovDAO may remove it\", func() {\n\t\tRemoveRunSubmitters(cross(cur), GetRunSubmitters())\n\t})\n\tuassert.True(t, len(GetRunSubmitters()) \u003e 0,\n\t\t\"the allowlist must never end up empty at a delegate's hand\")\n\n\t// A genuinely new address still works normally.\n\tfresh := testutils.TestAddress(\"freshgrant\").String()\n\tAddRunSubmitters(cross(cur), []string{fresh})\n\tuassert.True(t, RunSubmittersGrantedBy(address(fresh)))\n\tRemoveRunSubmitters(cross(cur), []string{fresh})\n\tuassert.False(t, contains(GetRunSubmitters(), fresh))\n}\n\n// TestRenderShowsDelegationState pins that the page actually reports the state\n// it exists to report.\n//\n// The value of a Render here is that someone auditing the chain can see whether\n// a parameter is delegated without knowing to ask. A page that renders the same\n// text whether or not a delegation exists would defeat that, so both states are\n// checked.\nfunc TestRenderShowsDelegationState(t *testing.T) {\n\tresetDelegation()\n\tdefer resetDelegation()\n\n\taddr := testutils.TestAddress(\"rendered\").String()\n\ttesting.SetSysParamStrings(vmModulePrefix, vmParamsSubmodule, runSubmittersKey,\n\t\t[]string{addr})\n\n\t// Undelegated: it must say so rather than leave the reader guessing.\n\tout := Render(\"\")\n\tuassert.True(t, contains2(out, \"nobody\"),\n\t\t\"an undelegated parameter must be reported as such\")\n\tuassert.False(t, contains2(out, testDelegate),\n\t\t\"no delegate should be named when none is set\")\n\n\t// Delegated: the holder is named.\n\trunSubmittersMgr = testDelegate\n\tarmRunSubmitters(testutils.TestAddress(\"seeded-by-vote\").String())\n\tout = Render(\"\")\n\tuassert.True(t, contains2(out, testDelegate),\n\t\t\"the delegate holding the capability must be named\")\n\tuassert.True(t, contains2(out, addr),\n\t\t\"the addresses the parameter currently allows must be listed\")\n\n\t// The valset writer is fixed in source, not delegated, and the page should\n\t// not blur the two.\n\tuassert.True(t, contains2(out, valsetAuthorizedRealm),\n\t\t\"the realm that writes valset params must be shown too\")\n}\n\n// contains2 reports whether s contains sub.\nfunc contains2(s, sub string) bool {\n\tif len(sub) == 0 {\n\t\treturn true\n\t}\n\tfor i := 0; i+len(sub) \u003c= len(s); i++ {\n\t\tif s[i:i+len(sub)] == sub {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// TestRunSubmittersDelegateCannotArmTheGate covers the direction the non-empty\n// floor does not: turning the allowlist ON.\n//\n// An empty run_submitters means the gate is off and anyone may MsgRun. So a\n// delegate adding the first address is not curating a list, it is switching a\n// chain-wide restriction on and choosing who it admits -- leaving that address\n// the only one on the chain that may run code.\n//\n// That is unrepairable in band. Creating a GovDAO proposal needs MsgRun, so\n// once the gate is armed against the members they cannot propose the vote that\n// would reset the list or revoke the delegation, and the floor in\n// RemoveRunSubmitters stops the delegate from undoing it either.\nfunc TestRunSubmittersDelegateCannotArmTheGate(cur realm, t *testing.T) {\n\tresetDelegation()\n\tdefer resetDelegation()\n\n\trunSubmittersMgr = testDelegate\n\tattacker := testutils.TestAddress(\"attacker\").String()\n\n\t// The gate starts off.\n\tuassert.Equal(t, 0, len(GetRunSubmitters()))\n\n\ttesting.SetRealm(testing.NewCodeRealm(testDelegate))\n\tuassert.AbortsContains(t, cur, \"refusing to arm\", func() {\n\t\tAddRunSubmitters(cross(cur), []string{attacker})\n\t})\n\tuassert.Equal(t, 0, len(GetRunSubmitters()),\n\t\t\"a refused arm must leave the gate off\")\n\n\t// Once GovDAO has armed it, the delegate may curate as before.\n\tseeded := testutils.TestAddress(\"seeded-by-vote\").String()\n\tarmRunSubmitters(seeded)\n\ttesting.SetRealm(testing.NewCodeRealm(testDelegate))\n\tAddRunSubmitters(cross(cur), []string{attacker})\n\tuassert.Equal(t, 2, len(GetRunSubmitters()),\n\t\t\"curating an armed list is still the delegate's job\")\n}\n\n// TestRunSubmittersKeyIsReservedFromGenericFactories pins that the whole-list\n// path is the dedicated constructor and nothing else.\n//\n// The generic factories take module, submodule and name as arguments, so\n// without this any of the nine could write run_submitters and walk past the\n// proposer rule in ProposeSetRunSubmitters. Same shape as the valset\n// reservation next to it.\nfunc TestRunSubmittersKeyIsReservedFromGenericFactories(cur realm, t *testing.T) {\n\taddr := testutils.TestAddress(\"someone\").String()\n\n\tuassert.AbortsContains(t, cur, \"reserved for ProposeSetRunSubmitters\", func() {\n\t\tNewSysParamStringsPropRequest(cross(cur), \"vm\", \"p\", runSubmittersKey, []string{addr})\n\t})\n\tuassert.AbortsContains(t, cur, \"reserved for ProposeSetRunSubmitters\", func() {\n\t\tNewSysParamStringsPropRequestWithTitle(cross(cur), \"vm\", \"p\", runSubmittersKey, \"t\", []string{addr})\n\t})\n\tuassert.AbortsContains(t, cur, \"reserved for ProposeSetRunSubmitters\", func() {\n\t\tNewSysParamStringsPropRequestAddWithTitle(cross(cur), \"vm\", \"p\", runSubmittersKey, \"t\", []string{addr})\n\t})\n\tuassert.AbortsContains(t, cur, \"reserved for ProposeSetRunSubmitters\", func() {\n\t\tNewSysParamStringsPropRequestRemoveWithTitle(cross(cur), \"vm\", \"p\", runSubmittersKey, \"t\", []string{addr})\n\t})\n\tuassert.AbortsContains(t, cur, \"reserved for ProposeSetRunSubmitters\", func() {\n\t\tNewSysParamStringPropRequest(cross(cur), \"vm\", \"p\", runSubmittersKey, addr)\n\t})\n\n\t// The four typed factories too. They cannot carry a run_submitters value,\n\t// but they can name the key, and the reservation is about the key -- all\n\t// nine share one check on the funnel they return through, so all nine are\n\t// listed here rather than the five that happen to take strings.\n\tuassert.AbortsContains(t, cur, \"reserved for ProposeSetRunSubmitters\", func() {\n\t\tNewSysParamInt64PropRequest(cross(cur), \"vm\", \"p\", runSubmittersKey, 1)\n\t})\n\tuassert.AbortsContains(t, cur, \"reserved for ProposeSetRunSubmitters\", func() {\n\t\tNewSysParamUint64PropRequest(cross(cur), \"vm\", \"p\", runSubmittersKey, 1)\n\t})\n\tuassert.AbortsContains(t, cur, \"reserved for ProposeSetRunSubmitters\", func() {\n\t\tNewSysParamBoolPropRequest(cross(cur), \"vm\", \"p\", runSubmittersKey, true)\n\t})\n\tuassert.AbortsContains(t, cur, \"reserved for ProposeSetRunSubmitters\", func() {\n\t\tNewSysParamBytesPropRequest(cross(cur), \"vm\", \"p\", runSubmittersKey, []byte{1})\n\t})\n\n\t// A different key in the same module is unaffected.\n\tuassert.NotAborts(t, cur, func() {\n\t\tNewSysParamStringsPropRequest(cross(cur), \"vm\", \"p\", \"code_submitters\", []string{addr})\n\t})\n}\n\n// TestProposeSetRunSubmittersRequiresProposerOnTheList covers the rule that a\n// non-empty allowlist must include whoever proposed it.\n//\n// A list naming nobody who can create a proposal cannot be undone, because\n// creating one needs MsgRun. Requiring the proposer's own address proves the\n// list is usable: GovDAO refuses a proposal from a non-member, and the proposer\n// just signed the transaction, so that address demonstrably holds a key.\nfunc TestProposeSetRunSubmittersRequiresProposerOnTheList(cur realm, t *testing.T) {\n\tother := testutils.TestAddress(\"someone-else\").String()\n\n\t// A list without the proposer is refused.\n\tuassert.AbortsContains(t, cur, \"omits the proposer\", func() {\n\t\tProposeSetRunSubmitters(cross(cur), []string{other})\n\t})\n\n\t// Emptying the list is always allowed: that switches the gate off, which\n\t// cannot lock anyone out.\n\tuassert.NotAborts(t, cur, func() {\n\t\tProposeSetRunSubmitters(cross(cur), []string{})\n\t})\n\n\t// A list that does include the proposer goes through.\n\tproposer := testutils.TestAddress(\"proposer\")\n\ttesting.SetRealm(testing.NewUserRealm(proposer))\n\tuassert.NotAborts(t, cur, func() {\n\t\tProposeSetRunSubmitters(cross(cur), []string{other, proposer.String()})\n\t})\n}\n\n// TestSetRunSubmittersReplacesTheList covers what the whole-list setter does to\n// a list that is not empty.\n//\n// This realm reserves run_submitters from the generic factories, so its own\n// setter is the only route by vote. If that setter appended instead of\n// replacing, the parameter would be append-only chain-wide: a compromised\n// address could never be de-listed and the gate could never be turned back off,\n// while the proposal shown to voters would say otherwise.\n//\n// Every other test here starts from an empty list, where appending and\n// replacing look identical, which is why this one starts armed.\nfunc TestSetRunSubmittersReplacesTheList(cur realm, t *testing.T) {\n\tresetDelegation()\n\tdefer resetDelegation()\n\n\ta := testutils.TestAddress(\"keep-a\").String()\n\tb := testutils.TestAddress(\"drop-b\").String()\n\tarmRunSubmitters(a, b)\n\tuassert.Equal(t, 2, len(GetRunSubmitters()))\n\n\t// Dropping b must actually drop it.\n\tsetRunSubmitters([]string{a})\n\tgot := GetRunSubmitters()\n\tuassert.Equal(t, 1, len(got), \"the list must be replaced, not appended to\")\n\tuassert.True(t, contains(got, a))\n\tuassert.False(t, contains(got, b), \"a de-listed address must be gone\")\n\n\t// And emptying must switch the gate off, which is what the proposal says.\n\tsetRunSubmitters([]string{})\n\tuassert.Equal(t, 0, len(GetRunSubmitters()),\n\t\t\"an empty list must be reachable, or the gate can never be turned off\")\n}\n\n// TestAssertDelegateRejectsANonLiveRealm covers the IsCurrent check in\n// assertDelegate.\n//\n// This is the shape where the check can actually fire. assertDelegate takes its\n// realm as an ordinary parameter, so a caller can hand it a stashed or\n// sibling-frame value; without IsCurrent it would then compare that value's\n// Previous() against the delegate path and admit whoever assembled it.\n//\n// Crossing functions cannot be tested this way, and do not need to be: the\n// compiler refuses anything but `cur` or `cross(rlm)` as their first argument,\n// so their cur is live by construction.\nfunc TestAssertDelegateRejectsANonLiveRealm(cur realm, t *testing.T) {\n\tresetDelegation()\n\tdefer resetDelegation()\n\n\trunSubmittersMgr = testDelegate\n\n\t// A synthetic realm whose Previous() names the delegate. The path check\n\t// alone would admit it; IsCurrent is the only thing that does not.\n\tfake := testing.MakeRealm(\n\t\ttestutils.TestAddress(\"impostor\"), \"gno.land/r/impostor\",\n\t\ttesting.MakeRealm(testutils.TestAddress(\"d\"), testDelegate, testing.OriginRealm()),\n\t)\n\tuassert.Equal(t, testDelegate, fake.Previous().PkgPath(),\n\t\t\"premise: the forged realm must pass the path check, or this proves nothing\")\n\n\t// PanicsContains, not Aborts: assertDelegate is called directly here, in\n\t// this realm, so the refusal is a panic. It surfaces as an abort only when\n\t// it happens across a realm boundary.\n\tuassert.PanicsContains(t, cur, \"not the caller's live cur\", func() {\n\t\tassertDelegate(0, fake, runSubmittersMgr, \"the \"+runSubmittersKey+\" allowlist\")\n\t})\n}\n"},{"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":"fee_collector_test.gno","body":"package params\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/urequire/v0\"\n\t\"gno.land/r/gov/dao\"\n)\n\n// cur is a zero-value realm used as a placeholder when forwarding to\n// uassert/urequire dispatch helpers that gained an `rlm realm` param.\n// These tests pass `func()` callbacks (no crossing inside the callback),\n// so rlm is ignored — a nil realm here is safe.\nvar cur realm\n\nfunc TestSetFeeCollector(cur realm, t *testing.T) {\n\tuserRealm := testing.NewUserRealm(g1user)\n\ttesting.SetRealm(userRealm)\n\n\tpr := NewSetFeeCollectorRequest(cur, userRealm.Address())\n\tid := dao.MustCreateProposal(cross(cur), pr)\n\t_, err := dao.GetProposal(id)\n\turequire.NoError(t, err)\n\n\turequire.NotPanics(\n\t\tt, cur,\n\t\tfunc() {\n\t\t\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(id)))\n\t\t},\n\t)\n\n\turequire.NotPanics(\n\t\tt, cur,\n\t\tfunc() {\n\t\t\tdao.ExecuteProposal(cross(cur), id)\n\t\t},\n\t)\n\n\t// XXX: test that the value got properly updated, when we can get params from gno code\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=\"v1.3.0\" prevents a v1.2.0 binary from resuming.\n//\n// minVersion must be a release tag the node can parse: vMAJOR.MINOR.PATCH, or\n// betanet's retired chain/gnolandMAJOR.MINOR. It is NOT the chain's launch tag —\n// naming \"chain/mainnet\" here parses as nothing, so the comparison degrades to\n// byte equality and refuses the upgraded binary alongside the stale ones, leaving\n// the chain unable to restart. This function does not check the shape; the release\n// tooling does. See RELEASING.md and gno.land/cmd/gnoland/UPGRADES.md, and prefer\n// misc/govdao-scripts/set-halt.sh over writing the call by hand.\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/v0. Without this\n// guard, a generic NewSysParam*PropRequest(\"node\",\"valset\",...) would\n// let any GovDAO supermajority bypass the v0 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/v0; use it instead of the generic factory\")\n\t}\n}\n\n// assertNotRunSubmittersKey rejects generic proposals that target\n// vm:p:run_submitters. Same shape as assertNotValsetKey above, and the same\n// reason: the key has a realm-side gate the generic factory would walk past.\n//\n// An empty run_submitters means the MsgRun gate is OFF. So a non-empty list is\n// the chain deciding who may run code, and a list that names nobody able to\n// create a GovDAO proposal cannot be changed back -- creating a proposal needs\n// MsgRun, because a ProposalRequest carries an Executor that MsgCall cannot\n// build from string arguments. The dedicated ProposeSetRunSubmitters in\n// run_submitters.gno refuses that list at proposal-creation time, where a human\n// can still act on the refusal.\n//\n// Reserving the key rather than adding the check to all nine factories keeps\n// the number of ways to write this parameter at one.\nfunc assertNotRunSubmittersKey(module, submodule, name string) {\n\tif module == \"vm\" \u0026\u0026 submodule == \"p\" \u0026\u0026 name == runSubmittersKey {\n\t\tpanic(runSubmittersKey + \" is reserved for ProposeSetRunSubmitters in this realm; \" +\n\t\t\t\"use it instead of the generic factory\")\n\t}\n}\n\nfunc NewSysParamStringPropRequest(cur realm, module, submodule, name, value string) dao.ProposalRequest {\n\treturn newPropRequest(cur, 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\treturn newPropRequest(cur, 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\treturn newPropRequest(cur, 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\treturn newPropRequest(cur, 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\treturn newPropRequest(cur, 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\treturn newPropRequest(cur, 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\treturn newPropRequest(cur, 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\treturn newPropRequest(cur, 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\treturn newPropRequest(cur, module, submodule, name,\n\t\tfunc() { prms.UpdateSysParamStrings(module, submodule, name, value, false) },\n\t\ttitle,\n\t)\n}\nfunc newPropRequest(cur realm, module, submodule, name string, fn func(), title string) dao.ProposalRequest {\n\t// The reserved-key checks live here rather than in each factory. Every\n\t// generic factory returns through this one function, so a factory added\n\t// later cannot forget them. The dedicated setters for those keys build their\n\t// own request and never come here, which is what makes them the exception.\n\tassertNotValsetKey(module, submodule)\n\tassertNotRunSubmittersKey(module, submodule, name)\n\n\tkey := syskey(module, submodule, name)\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":"params_test.gno","body":"package params\n\nimport (\n\t\"testing\"\n)\n\n// These tests cover proposal CONSTRUCTION. Reading a value back is possible --\n// sys/params exposes typed getters (GetSysParamStrings and friends), which\n// valset.gno and delegate_test.gno both use, and tests can seed with\n// testing.SetSysParam* -- so add read-back assertions rather than assuming they\n// cannot be written. End-to-end execution is additionally covered by the\n// propX_filetest.gno files under r/gov/dao/.\n\nfunc TestNewStringPropRequest(cur realm, t *testing.T) {\n\tpr := NewSysParamStringPropRequest(cur, \"foo\", \"bar\", \"baz\", \"qux\")\n\tif pr.Title() == \"\" {\n\t\tt.Errorf(\"executor shouldn't be nil\")\n\t}\n}\n\nfunc TestNewSetHaltRequest(cur realm, t *testing.T) {\n\tpr := NewSetHaltRequest(cur, 100_000, \"chain/gnoland1.1\")\n\tif pr.Title() == \"\" {\n\t\tt.Errorf(\"proposal title shouldn't be empty\")\n\t}\n}\n\nfunc TestNewSetHaltRequestNoVersion(cur realm, t *testing.T) {\n\tpr := NewSetHaltRequest(cur, 100_000, \"\")\n\tif pr.Title() == \"\" {\n\t\tt.Errorf(\"proposal title shouldn't be empty\")\n\t}\n}\n\nfunc TestNewSetHaltRequestCancel(cur realm, t *testing.T) {\n\tpr := NewSetHaltRequest(cur, 0, \"\")\n\tif pr.Title() == \"\" {\n\t\tt.Errorf(\"proposal title shouldn't be empty\")\n\t}\n}\n"},{"name":"render.gno","body":"package params\n\nimport (\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/moul/mdtable/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Render shows who, other than GovDAO, may currently write a chain parameter.\n//\n// This realm had no Render, so the only way to see a delegation was to know it\n// existed and query for it by name. That is the wrong shape for state that\n// grants a capability: someone auditing the chain should be able to see, in one\n// place, whether any parameter is delegated and to whom.\n//\n// Values are rendered without escaping, which is safe here because none of them\n// is free-form. A delegate path has passed assertDelegatePath, which permits\n// only lowercase letters, digits and a few separators; the addresses come from\n// the parameter itself, which the chain validates as bech32; and the valset\n// realm is a compile-time constant.\nfunc Render(path string) string {\n\tout := md.H1(\"Chain parameter delegation\")\n\tout += md.Paragraph(\n\t\t\"Chain parameters are written by GovDAO proposal. A parameter may also \" +\n\t\t\t\"be delegated to one other realm, which can then manage it without a \" +\n\t\t\t\"vote. GovDAO keeps full control of every parameter and can withdraw \" +\n\t\t\t\"a delegation at any time.\",\n\t)\n\n\tout += md.HorizontalRule()\n\tout += md.H2(\"run_submitters\")\n\tout += md.Paragraph(\"Addresses allowed to send MsgRun, which runs code the sender supplies.\")\n\n\tmgr := RunSubmittersManager()\n\ttable := mdtable.Table{Headers: []string{\"\", \"\"}}\n\tif mgr == \"\" {\n\t\ttable.Append([]string{md.Bold(\"Delegated to\"), \"nobody, only GovDAO may change it\"})\n\t} else {\n\t\ttable.Append([]string{md.Bold(\"Delegated to\"), md.InlineCode(mgr)})\n\t}\n\n\tlisted := GetRunSubmitters()\n\ttable.Append([]string{md.Bold(\"Addresses allowed\"), ufmt.Sprintf(\"%d\", len(listed))})\n\tout += table.String()\n\n\tif mgr != \"\" {\n\t\t// Which entries the delegate may remove again. Anything it did not add\n\t\t// is GovDAO's to remove, including whatever the chain started with.\n\t\tgranted := 0\n\t\tfor _, a := range listed {\n\t\t\tif RunSubmittersGrantedBy(address(a)) {\n\t\t\t\tgranted++\n\t\t\t}\n\t\t}\n\t\tout += md.Paragraph(ufmt.Sprintf(\n\t\t\t\"%d of those were added by the delegate, and are the only ones it may remove.\",\n\t\t\tgranted))\n\t}\n\n\tif len(listed) \u003e 0 {\n\t\titems := make([]string, 0, len(listed))\n\t\tfor _, a := range listed {\n\t\t\titems = append(items, md.InlineCode(a))\n\t\t}\n\t\tout += md.BulletList(items)\n\t}\n\n\tout += md.HorizontalRule()\n\tout += md.H2(\"node:valset\")\n\tout += md.Paragraph(\n\t\t\"Validator set parameters are written by one realm, fixed in this \" +\n\t\t\t\"realm's source. Unlike a delegation this cannot be repointed by a \" +\n\t\t\t\"vote, and GovDAO cannot write these keys itself.\",\n\t)\n\tvtable := mdtable.Table{Headers: []string{\"\", \"\"}}\n\tvtable.Append([]string{md.Bold(\"Writable only by\"), md.InlineCode(valsetAuthorizedRealm)})\n\tout += vtable.String()\n\n\treturn out\n}\n"},{"name":"run_submitters.gno","body":"package params\n\n// Delegated management of vm:p:run_submitters, the allowlist of addresses\n// permitted to send MsgRun.\n//\n// See delegate.gno for why this is a named slot rather than a registry.\n//\n// What the delegate can and cannot do, and why the asymmetry is this way round:\n//\n//   - It may ADD addresses. This is the routine work the delegation exists for.\n//   - It may REMOVE only addresses it added itself, and never the last one.\n//     De-listing its own mistake is a core part of managing an allowlist, so\n//     add-only would be a strange capability to hand out. Two bounds keep that\n//     safe. Grant-scoping stops it removing an address that predates the\n//     delegation. The non-empty floor in RemoveRunSubmitters stops it reaching\n//     zero by any route — an empty list means the gate is OFF and anyone may\n//     MsgRun, so emptying it would let the delegate revoke the entire\n//     restriction GovDAO voted for, which is the one thing this capability must\n//     not be able to do.\n//   - GovDAO retains everything, through ProposeSetRunSubmitters below -- the\n//     generic factories no longer accept this key. That is the bounded reset:\n//     one proposal returns the key to a known-good list regardless of what the\n//     delegate did, and the proposal shows voters the exact resulting list.\n//\n// A cost to state plainly rather than bury: this key is read by the ante handler\n// on EVERY transaction, before the per-tx gas meter exists, so its length is an\n// unmetered per-transaction constant for the whole chain. The delegate therefore\n// holds a knob on that constant, bounded only by maxAddressListLen in\n// gno.land/pkg/sdk/vm/params.go. That bound is enforced on this path for free:\n// UpdateSysParamStrings re-sets the whole list, which re-enters WillSetParam and\n// Params.Validate, so both the cap and bech32 validation apply to a delegate's\n// additions. A key that is not read on the ante path would be a cheaper first\n// delegation; this one is the one that was asked for.\n\nimport (\n\t\"chain\"\n\tprms \"sys/params\"\n\n\t\"gno.land/p/moul/addrset/v0\"\n\n\t\"gno.land/r/gov/dao\"\n)\n\nconst (\n\tvmModulePrefix    = \"vm\"\n\tvmParamsSubmodule = \"p\"\n\n\trunSubmittersKey = \"run_submitters\"\n)\n\n// runSubmittersMgr is the package path authorized to manage run_submitters.\n// Empty means the capability is not delegated, and empty must deny — see\n// assertDelegate.\nvar runSubmittersMgr string\n\n// runSubmittersGrants records which addresses the current delegate added, so\n// removal can be scoped to its own grants.\n//\n// The parameter is the source of truth and this is a side table, so the two can\n// disagree — genesis, `gnogenesis params set`, or any future direct keeper write\n// produces entries with no grant recorded. That direction is safe: an unrecorded\n// address is simply not removable by the delegate, which is the conservative\n// answer. Cleared whenever the delegation changes hands, so a new delegate never\n// inherits authority over its predecessor's grants.\nvar runSubmittersGrants = addrset.Set{}\n\n// RunSubmittersManager returns the package path currently authorized to manage\n// run_submitters, or \"\" when the capability is not delegated.\nfunc RunSubmittersManager() string {\n\treturn runSubmittersMgr\n}\n\n// RunSubmittersGrantedBy reports whether the current delegate added addr, i.e.\n// whether it may remove it.\n//\n// Exposed so a delegate can check before acting. A delegate that discovers a\n// refusal by panicking mid-proposal-execution is in a bad place: the panic\n// aborts the transaction, and for a DAO whose proposal has already passed, every\n// retry aborts the same way.\nfunc RunSubmittersGrantedBy(addr address) bool {\n\treturn runSubmittersGrants.Has(addr)\n}\n\n// IsRunSubmittersDelegate reports whether pkgpath currently holds the\n// capability. Pure predicate, for a caller that wants to fail cleanly rather\n// than be panicked at.\nfunc IsRunSubmittersDelegate(pkgpath string) bool {\n\treturn pkgpath != \"\" \u0026\u0026 pkgpath == runSubmittersMgr\n}\n\n// ProposeSetRunSubmittersManager creates a GovDAO proposal handing management of\n// run_submitters to pkgpath.\n//\n// pkgpath may be a sub-realm identity such as\n// \"gno.land/r/nt/commondao/v0#dao/42\", which is how a single DAO hosted by a\n// multi-tenant realm is named. Matching is exact, so naming the bare host would\n// authorize the host itself and none of its DAOs.\nfunc ProposeSetRunSubmittersManager(cur realm, pkgpath string) dao.ProposalRequest {\n\tassertDelegatePath(pkgpath)\n\tif pkgpath == runSubmittersMgr {\n\t\tpanic(\"no-op proposal rejected: \" + pkgpath + \" already manages \" + runSubmittersKey)\n\t}\n\n\t// desc uses the manager as of proposal creation, which is the honest thing\n\t// to show a voter. The EVENT reads it again inside the executor, because two\n\t// proposals created while the slot is empty and executed in sequence would\n\t// otherwise both report from=\"\" while the second actually replaced the first.\n\tfrom := runSubmittersMgr\n\tdesc := \"Authorize \" + pkgpath + \" to add addresses to the \" + runSubmittersKey +\n\t\t\" allowlist, which gates who may send MsgRun. It may remove only \" +\n\t\t\"addresses it added itself. GovDAO retains full control, including \" +\n\t\t\"replacing the whole list.\"\n\tif from != \"\" {\n\t\tdesc += \" This replaces the current manager, \" + from +\n\t\t\t\", and discards the record of which addresses it granted.\"\n\t}\n\n\treturn newDelegateProposal(cur, DelegateSetEvent, runSubmittersKey, pkgpath,\n\t\t\"Delegate \"+runSubmittersKey+\" management\", desc,\n\t\tfunc() string {\n\t\t\tprev := runSubmittersMgr\n\t\t\trunSubmittersMgr = pkgpath\n\t\t\t// A new holder must not inherit removal authority over addresses\n\t\t\t// the previous one granted.\n\t\t\trunSubmittersGrants = addrset.Set{}\n\t\t\treturn prev\n\t\t})\n}\n\n// ProposeClearRunSubmittersManager creates a GovDAO proposal revoking the\n// delegation.\n//\n// Revocation is immediate on execution because the slot is consulted on every\n// call. It deliberately does NOT remove addresses the delegate added: sweeping\n// them would make the executed effect invisible at vote time, and would silently\n// remove nothing whenever the grant record had drifted. Use the existing\n// whole-list setter to reset the list to a reviewed value.\nfunc ProposeClearRunSubmittersManager(cur realm) dao.ProposalRequest {\n\tif runSubmittersMgr == \"\" {\n\t\tpanic(\"no-op proposal rejected: \" + runSubmittersKey + \" is not delegated\")\n\t}\n\n\tfrom := runSubmittersMgr\n\treturn newDelegateProposal(cur, DelegateClearedEvent, runSubmittersKey, \"\",\n\t\t\"Revoke \"+runSubmittersKey+\" management\",\n\t\t\"Revoke \"+from+\"'s authority to manage the \"+runSubmittersKey+\n\t\t\t\" allowlist. Addresses it already added REMAIN on the list; reset the \"+\n\t\t\t\"list explicitly if that is not wanted.\",\n\t\tfunc() string {\n\t\t\tprev := runSubmittersMgr\n\t\t\trunSubmittersMgr = \"\"\n\t\t\trunSubmittersGrants = addrset.Set{}\n\t\t\treturn prev\n\t\t})\n}\n\n// AddRunSubmitters adds addresses to the run_submitters allowlist.\n//\n// Callable only by the delegated manager. Addresses already present are a no-op\n// (UpdateSysParamStrings dedupes), and the chain still validates every entry and\n// enforces the list-length cap, because the update re-sets the whole list.\nfunc AddRunSubmitters(cur realm, addrs []string) {\n\tassertDelegate(0, cur, runSubmittersMgr, \"the \"+runSubmittersKey+\" allowlist\")\n\tif len(addrs) == 0 {\n\t\treturn\n\t}\n\n\t// The delegate may curate a list that is already in force. It may not put\n\t// one into force.\n\t//\n\t// An empty run_submitters means the allowlist is OFF and anyone may MsgRun.\n\t// So the first add is not curation: it switches a chain-wide restriction on\n\t// and picks who it admits. A delegate adding one address to an empty list\n\t// leaves that address the only one on the chain that may run code.\n\t//\n\t// Refused rather than discouraged because it is unrepairable. Creating a\n\t// GovDAO proposal needs MsgRun -- a ProposalRequest carries an Executor,\n\t// which MsgCall cannot build from string arguments -- so once the gate is\n\t// armed against the members they cannot propose the vote that would undo\n\t// it, and the floor in RemoveRunSubmitters stops the delegate undoing it\n\t// either.\n\tif len(GetRunSubmitters()) == 0 {\n\t\tpanic(\"refusing to arm the \" + runSubmittersKey +\n\t\t\t\" allowlist: it is empty, so the gate is off and anyone may MsgRun. \" +\n\t\t\t\"Turning it on is a GovDAO vote, not a delegated edit\")\n\t}\n\n\t// Record a grant only for an address this call actually ADDED.\n\t//\n\t// UpdateSysParamStrings dedupes on add, so passing an address already on the\n\t// list leaves the parameter unchanged. Recording a grant for it anyway would\n\t// let the delegate launder authority over entries it never granted: read the\n\t// list, re-add all of it (a no-op on the parameter, but every address now\n\t// recorded as its own), then remove all of it -- including whatever predated\n\t// the delegation. The floor in RemoveRunSubmitters would refuse the last of\n\t// those removals, but only the last: everything up to it would still go\n\t// through, leaving the delegate holding the only listed address and so the\n\t// sole authority over who may MsgRun.\n\t//\n\t// So the grant record has to follow the parameter, not the argument.\n\tpresent := make(map[string]bool)\n\tfor _, a := range GetRunSubmitters() {\n\t\tpresent[a] = true\n\t}\n\n\tprms.UpdateSysParamStrings(vmModulePrefix, vmParamsSubmodule, runSubmittersKey, addrs, true)\n\tfor _, a := range addrs {\n\t\tif !present[a] {\n\t\t\trunSubmittersGrants.Add(address(a))\n\t\t}\n\t}\n\tchain.Emit(DelegateWriteEvent,\n\t\t\"key\", runSubmittersKey, \"realm\", runSubmittersMgr, \"op\", \"add\")\n}\n\n// RemoveRunSubmitters removes addresses from the run_submitters allowlist.\n//\n// Callable only by the delegated manager, and only for addresses that manager\n// added. Refusing rather than silently skipping is deliberate: a partial removal\n// that reported success would leave the caller believing an address was\n// de-listed when it was not.\nfunc RemoveRunSubmitters(cur realm, addrs []string) {\n\tassertDelegate(0, cur, runSubmittersMgr, \"the \"+runSubmittersKey+\" allowlist\")\n\tif len(addrs) == 0 {\n\t\treturn\n\t}\n\tfor _, a := range addrs {\n\t\tif !runSubmittersGrants.Has(address(a)) {\n\t\t\tpanic(\"cannot remove \" + a + \": not granted by \" + runSubmittersMgr +\n\t\t\t\t\", only GovDAO may remove it\")\n\t\t}\n\t}\n\n\t// The delegate may not empty the list, whatever it granted.\n\t//\n\t// An empty run_submitters means the gate is OFF -- anyone on the chain may\n\t// send MsgRun. So emptying it is not a smaller version of removing one\n\t// address, it is the opposite of what the delegation is for: it would let a\n\t// delegate authorized to curate a list unilaterally revoke the whole\n\t// restriction GovDAO voted for.\n\t//\n\t// Grant-scoping alone does not prevent this. It holds only while at least\n\t// one entry the delegate did not grant survives, and GovDAO replacing the\n\t// list wholesale can remove its own entries without touching the grant\n\t// record. A floor makes the invariant structural instead of emergent.\n\t//\n\t// Counted against the parameter, not the argument: the caller may name\n\t// addresses that are not listed, or name one twice, and neither shrinks the\n\t// list. Only GovDAO can go to zero, through the whole-list setter, where the\n\t// resulting list is on the ballot.\n\tremoving := make(map[string]bool, len(addrs))\n\tfor _, a := range addrs {\n\t\tremoving[a] = true\n\t}\n\tremaining := 0\n\tfor _, a := range GetRunSubmitters() {\n\t\tif !removing[a] {\n\t\t\tremaining++\n\t\t}\n\t}\n\tif remaining == 0 {\n\t\tpanic(\"refusing to empty the \" + runSubmittersKey +\n\t\t\t\" allowlist: an empty list disables the gate entirely, so only \" +\n\t\t\t\"GovDAO may do it\")\n\t}\n\n\tprms.UpdateSysParamStrings(vmModulePrefix, vmParamsSubmodule, runSubmittersKey, addrs, false)\n\tfor _, a := range addrs {\n\t\trunSubmittersGrants.Remove(address(a))\n\t}\n\tchain.Emit(DelegateWriteEvent,\n\t\t\"key\", runSubmittersKey, \"realm\", runSubmittersMgr, \"op\", \"remove\")\n}\n\n// GetRunSubmitters returns the current allowlist.\nfunc GetRunSubmitters() []string {\n\tvals, _ := prms.GetSysParamStrings(vmModulePrefix, vmParamsSubmodule, runSubmittersKey)\n\treturn vals\n}\n\n// ProposeSetRunSubmitters creates a GovDAO proposal replacing the whole\n// run_submitters allowlist.\n//\n// This is the only way to set the list by vote: the generic factories refuse\n// the key (see assertNotRunSubmittersKey), so every whole-list write comes\n// through here and carries the rule below.\n//\n// The proposer must be on the list they propose.\n//\n// An empty run_submitters means the gate is off and anyone may MsgRun. A\n// non-empty one therefore decides who may run code at all -- and a list naming\n// nobody who can create a GovDAO proposal cannot be undone, because creating a\n// proposal needs MsgRun: a ProposalRequest carries an Executor, and MsgCall\n// cannot build one from string arguments. The vote would end governance.\n//\n// Requiring the proposer's own address is a cheap way to prove the list is\n// usable rather than merely plausible. GovDAO refuses a proposal from a\n// non-member (PreCreateProposal, \"only members can create new proposals\"), so\n// if this proposal exists at all its author is a member -- and they just signed\n// the transaction that created it, so the address demonstrably holds a key. A\n// list that merely NAMES a member proves neither: the address may belong to\n// nobody, since any member can enroll an arbitrary address.\n//\n// Checked here, at proposal creation, rather than inside the executor. Nothing\n// in r/gov/dao recovers from an executor panic, so a check that fires at\n// execution turns a passed proposal into one that can never be executed. Here\n// the refusal reaches a person who can still fix the list and propose again.\n//\n// This is a floor, not an invariant. The proposer may resign from GovDAO later,\n// and the list is not re-checked when they do. It rules out arriving at a dead\n// list in one vote; it cannot rule out drifting into one.\nfunc ProposeSetRunSubmitters(cur realm, addrs []string) dao.ProposalRequest {\n\t// IsCurrent before Previous, as AGENTS.md requires and assertDelegate does.\n\t//\n\t// Belt and braces here rather than load-bearing: the compiler already\n\t// refuses anything but `cur` or `cross(rlm)` as the first argument to a\n\t// crossing function, so this cur is live by construction and the check\n\t// cannot fire. It earns its place if this ever becomes a helper taking an\n\t// ordinary realm parameter, which is the shape assertDelegate has and where\n\t// a stashed value really can be threaded in.\n\tif !cur.IsCurrent() {\n\t\tpanic(\"unauthorized: cur is not the caller's live realm\")\n\t}\n\tproposer := cur.Previous().Address()\n\tlisted := false\n\tfor _, a := range addrs {\n\t\tif a == proposer.String() {\n\t\t\tlisted = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif len(addrs) \u003e 0 \u0026\u0026 !listed {\n\t\tpanic(\"refusing to propose a \" + runSubmittersKey + \" allowlist that omits \" +\n\t\t\t\"the proposer \" + proposer.String() + \": a non-empty list that names nobody \" +\n\t\t\t\"who can create a proposal cannot be changed back\")\n\t}\n\n\ttitle := \"Set the \" + runSubmittersKey + \" allowlist\"\n\tdesc := \"Replace the \" + runSubmittersKey + \" allowlist, which gates who may \" +\n\t\t\"send MsgRun.\"\n\tif len(addrs) == 0 {\n\t\tdesc += \" This empties the list, which switches the gate OFF: anyone may \" +\n\t\t\t\"send MsgRun.\"\n\t} else {\n\t\tdesc += \" Only these addresses may send MsgRun. Creating a GovDAO proposal \" +\n\t\t\t\"needs MsgRun, so this list also decides who can govern.\"\n\t\tfor _, a := range addrs {\n\t\t\tdesc += \"\\n- \" + a\n\t\t}\n\t}\n\n\tcallback := func(cur realm) error {\n\t\tsetRunSubmitters(addrs)\n\t\tchain.Emit(\"SetRunSubmitters\", \"key\", runSubmittersKey, \"proposer\", proposer.String())\n\t\treturn nil\n\t}\n\treturn dao.NewProposalRequest(title, desc,\n\t\tdao.NewSimpleExecutor(0, cur, callback, \"\"))\n}\n\n// setRunSubmitters replaces the whole allowlist.\n//\n// SetSysParamStrings, not UpdateSysParamStrings: Update with add=true appends\n// non-duplicates onto what is already there, so it can never remove an address\n// or empty the list. This realm reserves the key from the generic factories, so\n// this is the only route by vote -- if it appended, the parameter would be\n// append-only chain-wide, a compromised address could never be de-listed, and\n// the gate could never be turned back off.\n//\n// A named function rather than the executor's body inline, so a test can reach\n// it: a closure held in a ProposalRequest cannot be called from outside.\nfunc setRunSubmitters(addrs []string) {\n\tprms.SetSysParamStrings(vmModulePrefix, vmParamsSubmodule, runSubmittersKey, addrs)\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, \"Remove unrestricted transfer accounts\", addrStrings)\n}\n"},{"name":"unlock_test.gno","body":"package params\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n\t\"gno.land/r/gov/dao\"\n\tini \"gno.land/r/gov/dao/init/v0\"\n)\n\nvar g1user = testutils.TestAddress(\"g1user\")\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(g1user))\n\tini.InitWithUsers(cross(cur), g1user)\n}\n\nfunc TestProUnlockTransfer(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(g1user))\n\n\tpr := ProposeUnlockTransferRequest(cur)\n\tid := dao.MustCreateProposal(cross(cur), pr)\n\tp, err := dao.GetProposal(id)\n\turequire.NoError(t, err)\n\turequire.Equal(t, unlockTransferTitle, p.Title())\n}\n\nfunc TestFailUnlockTransfer(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(g1user))\n\n\tpr := ProposeUnlockTransferRequest(cur)\n\tid := dao.MustCreateProposal(cross(cur), pr)\n\turequire.AbortsWithMessage(\n\t\tt, cur,\n\t\t\"proposal didn't reach supermajority yet: 66.66\",\n\t\tfunc() {\n\t\t\tdao.ExecuteProposal(cross(cur), id)\n\t\t},\n\t)\n}\n\nfunc TestExeUnlockTransfer(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(g1user))\n\n\tpr := ProposeUnlockTransferRequest(cur)\n\tid := dao.MustCreateProposal(cross(cur), pr)\n\t_, err := dao.GetProposal(id)\n\turequire.NoError(t, err)\n\t// urequire.True(t, dao.Active == p.Status()) // TODO\n\n\turequire.NotPanics(\n\t\tt, cur,\n\t\tfunc() {\n\t\t\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(id)))\n\t\t},\n\t)\n\n\turequire.NotPanics(\n\t\tt, cur,\n\t\tfunc() {\n\t\t\tdao.ExecuteProposal(cross(cur), id)\n\t\t},\n\t)\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/v0\"\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 = v0'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/v0\"\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 v0 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 v0 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/v0's internal\n// reads. Other realms making \"is X a validator\" decisions should\n// call v0.IsValidator, not this directly, so future changes to v0'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\n// assertValsetCaller authorizes the one realm permitted to write valset params.\n//\n// Delegates to assertDelegate (see delegate.gno) so this realm has exactly one\n// authorized-caller gate rather than two that can drift. The distinction that\n// remains is deliberate and is NOT expressible through a delegation slot: this\n// authorization is a compile-time const, so re-pointing it requires a chain\n// relaunch, and assertNotValsetKey locks GovDAO out of the key family entirely.\n// A vote-settable slot would let one supermajority re-point the valset writer at\n// an arbitrary realm and bypass v0's valoper-existence check, its KeepRunning\n// opt-out, and execution-time pubkey re-resolution.\nfunc assertValsetCaller(_ int, rlm realm) {\n\tassertDelegate(0, rlm, valsetAuthorizedRealm, \"valset params\")\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"4+J9JfQPnXj6P2Lnh83S8EONiXYpNfKa2becilbNw8BHLTRRb/tUgSkWTtNd0j2OG0KfJtiERjZwQpbqTmDgvw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l","package":{"name":"validators","path":"gno.land/r/sys/validators/v0","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/v0\"\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// (v0 imports valopers and reads them) — pulling would create an\n// import cycle, since valopers already imports v0 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 v0 → valopers\n// callbacks would make valopers see v0 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":"cache_test.gno","body":"package validators\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n\tsysparams \"gno.land/r/sys/params\"\n)\n\n// cur is a zero-value realm used as a placeholder when forwarding to\n// uassert/urequire dispatch helpers that gained an `rlm realm` param.\n// These tests pass `func()` callbacks (no crossing inside the callback),\n// so rlm is ignored — a nil realm here is safe.\nvar cur realm\n\n// resetCache clears valoperCache so subtests don't leak state.\nfunc resetCache() {\n\tvaloperCache = bptree.NewBPTree32()\n}\n\nfunc TestNotifyValoperChanged_HappyPath(cur realm, t *testing.T) {\n\tresetCache()\n\n\top := testutils.TestAddress(\"op-A\")\n\tsigningAddr := mustAddr(t, pubKeyA)\n\n\t// Caller realm = valopers; auth check passes.\n\ttesting.SetRealm(testing.NewCodeRealm(valopersRealmPath))\n\n\tNotifyValoperChanged(cross(cur), op, pubKeyA, signingAddr, true)\n\n\trawEntry := valoperCache.Get(op.String())\n\turequire.NotNil(t, rawEntry, \"cache entry must exist\")\n\tentry := rawEntry.(cacheEntry)\n\tuassert.Equal(t, pubKeyA, entry.SigningPubKey)\n\tuassert.Equal(t, signingAddr, entry.SigningAddress)\n\tuassert.Equal(t, true, entry.KeepRunning)\n\n\t// Subsequent call updates the same slot.\n\tsigningAddrB := mustAddr(t, pubKeyB)\n\tNotifyValoperChanged(cross(cur), op, pubKeyB, signingAddrB, false)\n\n\trawEntry = valoperCache.Get(op.String())\n\turequire.NotNil(t, rawEntry)\n\tentry = rawEntry.(cacheEntry)\n\tuassert.Equal(t, pubKeyB, entry.SigningPubKey)\n\tuassert.Equal(t, signingAddrB, entry.SigningAddress)\n\tuassert.Equal(t, false, entry.KeepRunning)\n}\n\nfunc TestNotifyValoperChanged_RejectsNonValopersCaller(cur realm, t *testing.T) {\n\tresetCache()\n\n\top := testutils.TestAddress(\"op-A\")\n\tsigningAddr := mustAddr(t, pubKeyA)\n\n\t// Caller realm = some other realm; auth check rejects.\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/attacker\"))\n\n\tuassert.AbortsContains(t, cur, \"caller realm must be \"+valopersRealmPath, func() {\n\t\tNotifyValoperChanged(cross(cur), op, pubKeyA, signingAddr, true)\n\t})\n\n\t// User MsgCall — UserRealm has empty pkgpath; rejected the same way.\n\ttesting.SetRealm(testing.NewUserRealm(testutils.TestAddress(\"user\")))\n\n\tuassert.AbortsContains(t, cur, \"caller realm must be \"+valopersRealmPath, func() {\n\t\tNotifyValoperChanged(cross(cur), op, pubKeyA, signingAddr, true)\n\t})\n}\n\nfunc TestRotateValoperSigningKey_AppliesRotation(cur realm, t *testing.T) {\n\tresetValset(t)\n\tresetCache()\n\n\t// Seed the valset with op-A signing under pubKeyA at power=10.\n\ttesting.SetSysParamStrings(module, submodule, currKey, []string{pubKeyA + \":10\"})\n\n\top := testutils.TestAddress(\"op-A\")\n\ttesting.SetRealm(testing.NewCodeRealm(valopersRealmPath))\n\n\tRotateValoperSigningKey(cross(cur), op, pubKeyA, pubKeyB)\n\n\t// After rotation the effective set (proposed-when-dirty) should\n\t// contain pubKeyB at the same power; pubKeyA should be gone.\n\tuassert.True(t, sysparams.ValsetDirty(), \"dirty bit set after RotateValoperSigningKey\")\n\n\teffective := sysparams.GetValsetEffective()\n\turequire.Equal(t, 1, len(effective))\n\tuassert.Equal(t, pubKeyB, effective[0].PubKey)\n\tuassert.Equal(t, uint64(10), effective[0].VotingPower)\n}\n\nfunc TestRotateValoperSigningKey_OperatorNotInValset_NoOp(cur realm, t *testing.T) {\n\tresetValset(t)\n\tresetCache()\n\n\t// Empty valset; the operator is not currently signing.\n\top := testutils.TestAddress(\"op-A\")\n\ttesting.SetRealm(testing.NewCodeRealm(valopersRealmPath))\n\n\t// Should not panic; no sysparams write expected (dirty stays false).\n\tuassert.NotAborts(t, cur, func() {\n\t\tRotateValoperSigningKey(cross(cur), op, pubKeyA, pubKeyB)\n\t})\n\n\tuassert.False(t, sysparams.ValsetDirty(), \"dirty bit must remain false on no-op rotation\")\n\tuassert.Equal(t, 0, len(sysparams.GetValsetEffective()))\n}\n\nfunc TestRotateValoperSigningKey_RejectsNonValopersCaller(cur realm, t *testing.T) {\n\tresetValset(t)\n\tresetCache()\n\ttesting.SetSysParamStrings(module, submodule, currKey, []string{pubKeyA + \":10\"})\n\n\top := testutils.TestAddress(\"op-A\")\n\n\t// Foreign realm — rejected.\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/attacker\"))\n\tuassert.AbortsContains(t, cur, \"caller realm must be \"+valopersRealmPath, func() {\n\t\tRotateValoperSigningKey(cross(cur), op, pubKeyA, pubKeyB)\n\t})\n\n\t// User MsgCall — UserRealm pkgpath is \"\" — rejected.\n\ttesting.SetRealm(testing.NewUserRealm(testutils.TestAddress(\"user\")))\n\tuassert.AbortsContains(t, cur, \"caller realm must be \"+valopersRealmPath, func() {\n\t\tRotateValoperSigningKey(cross(cur), op, pubKeyA, pubKeyB)\n\t})\n\n\t// Sanity: the valset wasn't mutated by the rejected calls (dirty\n\t// bit stays false; effective set still equals the seeded current).\n\tuassert.False(t, sysparams.ValsetDirty())\n\teffective := sysparams.GetValsetEffective()\n\turequire.Equal(t, 1, len(effective))\n\tuassert.Equal(t, pubKeyA, effective[0].PubKey)\n}\n\nfunc TestAssertGenesisValopersConsistent_HappyPath(cur realm, t *testing.T) {\n\tresetValset(t)\n\tresetCache()\n\ttesting.SetHeight(0) // assertion is genesis-mode only\n\n\t// Seed valset:current with two genesis validators.\n\ttesting.SetSysParamStrings(module, submodule, currKey, []string{\n\t\tpubKeyA + \":10\",\n\t\tpubKeyB + \":5\",\n\t})\n\n\t// Seed valoperCache with profiles whose SigningAddress matches.\n\topA := testutils.TestAddress(\"op-A\")\n\topB := testutils.TestAddress(\"op-B\")\n\tseedCache(t, []struct {\n\t\top          address\n\t\tpubKey      string\n\t\tkeepRunning bool\n\t}{\n\t\t{op: opA, pubKey: pubKeyA, keepRunning: true},\n\t\t{op: opB, pubKey: pubKeyB, keepRunning: true},\n\t})\n\n\tuassert.NotAborts(t, cur, func() {\n\t\tAssertGenesisValopersConsistent(cross(cur))\n\t})\n}\n\nfunc TestAssertGenesisValopersConsistent_PanicsOnMissingProfile(cur realm, t *testing.T) {\n\tresetValset(t)\n\tresetCache()\n\ttesting.SetHeight(0)\n\n\t// Two validators in valset:current but only one has a profile.\n\ttesting.SetSysParamStrings(module, submodule, currKey, []string{\n\t\tpubKeyA + \":10\",\n\t\tpubKeyB + \":5\",\n\t})\n\n\topA := testutils.TestAddress(\"op-A\")\n\tseedCache(t, []struct {\n\t\top          address\n\t\tpubKey      string\n\t\tkeepRunning bool\n\t}{\n\t\t{op: opA, pubKey: pubKeyA, keepRunning: true},\n\t\t// op for pubKeyB intentionally missing.\n\t})\n\n\tuassert.AbortsContains(t, cur, \"no corresponding valoper profile\", func() {\n\t\tAssertGenesisValopersConsistent(cross(cur))\n\t})\n}\n\nfunc TestAssertGenesisValopersConsistent_EmptyValsetTrivial(cur realm, t *testing.T) {\n\tresetValset(t)\n\tresetCache()\n\ttesting.SetHeight(0)\n\n\t// Empty valset → assertion trivially holds (no entries to check).\n\tuassert.NotAborts(t, cur, func() {\n\t\tAssertGenesisValopersConsistent(cross(cur))\n\t})\n}\n\nfunc TestAssertGenesisValopersConsistent_RejectsPostGenesis(cur realm, t *testing.T) {\n\tresetValset(t)\n\tresetCache()\n\t// Default test height is non-zero (123); the assertion must\n\t// refuse to run outside genesis-mode replay.\n\tuassert.AbortsContains(t, cur, \"only callable during genesis-mode replay\", func() {\n\t\tAssertGenesisValopersConsistent(cross(cur))\n\t})\n}\n\nfunc TestRotateValoperSigningKey_AccumulatesAcrossSameBlock(cur realm, t *testing.T) {\n\tresetValset(t)\n\tresetCache()\n\n\t// Seed valset with two operators signing under pubKeyA and pubKeyC.\n\ttesting.SetSysParamStrings(module, submodule, currKey, []string{\n\t\tpubKeyA + \":10\",\n\t\tpubKeyC + \":5\",\n\t})\n\n\topA := testutils.TestAddress(\"op-A\")\n\topC := testutils.TestAddress(\"op-C\")\n\ttesting.SetRealm(testing.NewCodeRealm(valopersRealmPath))\n\n\t// First rotation: A → B. Read-modify-write of GetValsetEffective.\n\tRotateValoperSigningKey(cross(cur), opA, pubKeyA, pubKeyB)\n\n\t// Second rotation in the same block: C → A. Should not clobber\n\t// the first rotation; should read proposed-when-dirty as baseline.\n\tRotateValoperSigningKey(cross(cur), opC, pubKeyC, pubKeyA)\n\n\t// Final effective set: {pubKeyB:10, pubKeyA:5}. Order may differ\n\t// because GetValsetEffective parses sysparams' sorted-string slot\n\t// and returns []Validator preserving that order.\n\teffective := sysparams.GetValsetEffective()\n\turequire.Equal(t, 2, len(effective))\n\n\t// Build a power-by-pubkey map and assert.\n\tpowerOf := map[string]uint64{}\n\tfor _, v := range effective {\n\t\tpowerOf[v.PubKey] = v.VotingPower\n\t}\n\tuassert.Equal(t, uint64(10), powerOf[pubKeyB])\n\tuassert.Equal(t, uint64(5), powerOf[pubKeyA])\n\t_, ok := powerOf[pubKeyC]\n\tuassert.False(t, ok, \"pubKeyC should be replaced by pubKeyA at the rotated slot\")\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/sys/validators/v0\"\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/v0\"\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 v0 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":"proposal_test.gno","body":"package validators\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n\tsysparams \"gno.land/r/sys/params\"\n)\n\n// seedCache populates valoperCache with the given (op, pubkey, kr)\n// tuples — used in tests to satisfy NewValidatorProposalRequest's\n// creation-time membership check without going through valopers.\n// asGovDAOProxy is the realm these tests stand in as before invoking an\n// executor directly. SimpleExecutor.Execute is invocable only from the\n// r/gov/dao namespace; on the live route dao.ExecuteProposal and\n// impl.ExecuteProposal thread the proxy's cur to it non-crossing, so\n// Previous() inside Execute is this path. Calling Execute from the test's\n// own realm is a route no transaction takes.\nconst asGovDAOProxy = \"gno.land/r/gov/dao\"\n\nfunc seedCache(t *testing.T, entries []struct {\n\top          address\n\tpubKey      string\n\tkeepRunning bool\n}) {\n\tt.Helper()\n\tfor _, e := range entries {\n\t\tsigningAddr := mustAddr(t, e.pubKey)\n\t\tvaloperCache.Set(e.op.String(), cacheEntry{\n\t\t\tSigningPubKey:  e.pubKey,\n\t\t\tSigningAddress: signingAddr,\n\t\t\tKeepRunning:    e.keepRunning,\n\t\t})\n\t}\n}\n\nfunc TestNewValidatorProposalRequest_RejectsUnknownOperator(cur realm, t *testing.T) {\n\tresetCache()\n\n\top := testutils.TestAddress(\"ghost-op\")\n\n\tuassert.PanicsContains(t, cur, \"unknown operator\", func() {\n\t\t_ = NewValidatorProposalRequest(cur,\n\t\t\t[]ValoperChange{{OperatorAddress: op, Power: 1}},\n\t\t\t\"add ghost\",\n\t\t\t\"\",\n\t\t)\n\t})\n}\n\nfunc TestNewValidatorProposalRequest_RejectsEmptyChanges(cur realm, t *testing.T) {\n\tresetCache()\n\n\tuassert.PanicsContains(t, cur, errNoValoperChanges, func() {\n\t\t_ = NewValidatorProposalRequest(cur, nil, \"title\", \"\")\n\t})\n}\n\nfunc TestNewValidatorProposalRequest_RejectsEmptyTitle(cur realm, t *testing.T) {\n\tresetCache()\n\top := testutils.TestAddress(\"op-A\")\n\tseedCache(t, []struct {\n\t\top          address\n\t\tpubKey      string\n\t\tkeepRunning bool\n\t}{{op: op, pubKey: pubKeyA, keepRunning: true}})\n\n\tuassert.PanicsContains(t, cur, \"proposal title is empty\", func() {\n\t\t_ = NewValidatorProposalRequest(cur,\n\t\t\t[]ValoperChange{{OperatorAddress: op, Power: 1}},\n\t\t\t\"   \",\n\t\t\t\"\",\n\t\t)\n\t})\n}\n\nfunc TestNewValidatorProposalRequest_RejectsTooManyChanges(cur realm, t *testing.T) {\n\tresetCache()\n\n\t// Seed 41 cache entries so the membership check passes; the\n\t// length cap should fire before the per-entry validation.\n\tchanges := make([]ValoperChange, 41)\n\tpubkeys := []string{pubKeyA, pubKeyB, pubKeyC}\n\tfor i := 0; i \u003c 41; i++ {\n\t\top := testutils.TestAddress(\"op-\" + strconv.Itoa(i))\n\t\tpk := pubkeys[i%3]\n\t\tvaloperCache.Set(op.String(), cacheEntry{\n\t\t\tSigningPubKey:  pk,\n\t\t\tSigningAddress: mustAddr(t, pk),\n\t\t\tKeepRunning:    true,\n\t\t})\n\t\tchanges[i] = ValoperChange{OperatorAddress: op, Power: 1}\n\t}\n\n\tuassert.PanicsContains(t, cur, \"max number of allowed validators per proposal is 40\", func() {\n\t\t_ = NewValidatorProposalRequest(cur, changes, \"too many\", \"\")\n\t})\n}\n\nfunc TestNewValidatorProposalRequest_DescriptionRendering(cur realm, t *testing.T) {\n\tresetCache()\n\topA := testutils.TestAddress(\"op-A\")\n\topB := testutils.TestAddress(\"op-B\")\n\tseedCache(t, []struct {\n\t\top          address\n\t\tpubKey      string\n\t\tkeepRunning bool\n\t}{\n\t\t{op: opA, pubKey: pubKeyA, keepRunning: true},\n\t\t{op: opB, pubKey: pubKeyB, keepRunning: true},\n\t})\n\n\tpr := NewValidatorProposalRequest(cur,\n\t\t[]ValoperChange{\n\t\t\t{OperatorAddress: opA, Power: 5},\n\t\t\t{OperatorAddress: opB, Power: 0},\n\t\t},\n\t\t\"mixed changes\",\n\t\t\"context line\",\n\t)\n\n\tdesc := pr.Description()\n\turequire.True(t, len(desc) \u003e 0)\n\tuassert.True(t, contains(desc, \"context line\"))\n\tuassert.True(t, contains(desc, \"## Validator Updates\"))\n\tuassert.True(t, contains(desc, opA.String()+\": add (power 5)\"))\n\tuassert.True(t, contains(desc, opB.String()+\": remove\"))\n}\n\nfunc TestNewValidatorProposalRequest_ExecutorReResolvesPubkey(cur realm, t *testing.T) {\n\t// Creation-time captured changes: ValoperChange refers to opA.\n\t// Cache for opA points to pubKeyA at creation. Before execution,\n\t// opA's cache entry is updated to pubKeyB. Executor must publish\n\t// the NEW pubkey, not the creation-time one.\n\tresetValset(t)\n\tresetCache()\n\n\topA := testutils.TestAddress(\"op-A\")\n\tseedCache(t, []struct {\n\t\top          address\n\t\tpubKey      string\n\t\tkeepRunning bool\n\t}{{op: opA, pubKey: pubKeyA, keepRunning: true}})\n\n\tchanges := []ValoperChange{{OperatorAddress: opA, Power: 7}}\n\n\t// Build the executor; it captures `changes` by reference (slice\n\t// of structs) but resolves SigningPubKey at run-time via cache.\n\texec := newValoperChangeExecutor(cur, changes)\n\n\t// Simulate operator rotation: opA's cache entry now points to\n\t// pubKeyB. Captured changes slice is unchanged.\n\tvaloperCache.Set(opA.String(), cacheEntry{\n\t\tSigningPubKey:  pubKeyB,\n\t\tSigningAddress: mustAddr(t, pubKeyB),\n\t\tKeepRunning:    true,\n\t})\n\n\ttesting.SetRealm(testing.NewCodeRealm(asGovDAOProxy))\n\n\turequire.NoError(t, exec.Execute(cross(cur)))\n\n\t// Effective valset should contain pubKeyB (post-rotation), not\n\t// pubKeyA (creation-time).\n\teffective := sysparams.GetValsetEffective()\n\turequire.Equal(t, 1, len(effective))\n\tuassert.Equal(t, pubKeyB, effective[0].PubKey)\n\tuassert.Equal(t, uint64(7), effective[0].VotingPower)\n}\n\nfunc TestNewValidatorProposalRequest_RemoveOperator(cur realm, t *testing.T) {\n\tresetValset(t)\n\tresetCache()\n\n\t// Seed valset with opA already signing under pubKeyA.\n\ttesting.SetSysParamStrings(module, submodule, currKey, []string{pubKeyA + \":10\"})\n\n\topA := testutils.TestAddress(\"op-A\")\n\tseedCache(t, []struct {\n\t\top          address\n\t\tpubKey      string\n\t\tkeepRunning bool\n\t}{{op: opA, pubKey: pubKeyA, keepRunning: false}})\n\n\t// Liveness floor: removing the only validator empties the set.\n\t// Executor runs inside a crossing dao.Executor.Execute call, so\n\t// the panic surfaces as an abort, not a regular panic.\n\ttesting.SetRealm(testing.NewCodeRealm(asGovDAOProxy))\n\n\tuassert.AbortsContains(t, cur, \"would empty the validator set\", func() {\n\t\t_ = newValoperChangeExecutor(cur, []ValoperChange{{OperatorAddress: opA, Power: 0}}).Execute(cross(cur))\n\t})\n}\n\nfunc TestNewValidatorProposalRequest_RemoveLeavesOthers(cur realm, t *testing.T) {\n\tresetValset(t)\n\tresetCache()\n\n\t// Seed valset with two validators.\n\ttesting.SetSysParamStrings(module, submodule, currKey, []string{\n\t\tpubKeyA + \":10\",\n\t\tpubKeyB + \":5\",\n\t})\n\n\topA := testutils.TestAddress(\"op-A\")\n\topB := testutils.TestAddress(\"op-B\")\n\tseedCache(t, []struct {\n\t\top          address\n\t\tpubKey      string\n\t\tkeepRunning bool\n\t}{\n\t\t{op: opA, pubKey: pubKeyA, keepRunning: true},\n\t\t{op: opB, pubKey: pubKeyB, keepRunning: true},\n\t})\n\n\tchanges := []ValoperChange{{OperatorAddress: opA, Power: 0}}\n\t// Build the executor directly (private function, same package).\n\ttesting.SetRealm(testing.NewCodeRealm(asGovDAOProxy))\n\n\turequire.NoError(t, newValoperChangeExecutor(cur, changes).Execute(cross(cur)))\n\n\t// Effective set: only opB / pubKeyB remains.\n\teffective := sysparams.GetValsetEffective()\n\turequire.Equal(t, 1, len(effective))\n\tuassert.Equal(t, pubKeyB, effective[0].PubKey)\n}\n\nfunc TestNewValidatorProposalRequest_AllowsFullValsetReplacement(cur realm, t *testing.T) {\n\tresetValset(t)\n\tresetCache()\n\n\ttesting.SetSysParamStrings(module, submodule, currKey, []string{pubKeyA + \":10\"})\n\n\topA := testutils.TestAddress(\"op-A\")\n\topB := testutils.TestAddress(\"op-B\")\n\tseedCache(t, []struct {\n\t\top          address\n\t\tpubKey      string\n\t\tkeepRunning bool\n\t}{\n\t\t{op: opA, pubKey: pubKeyA, keepRunning: false},\n\t\t{op: opB, pubKey: pubKeyB, keepRunning: true},\n\t})\n\n\tchanges := []ValoperChange{\n\t\t{OperatorAddress: opA, Power: 0},\n\t\t{OperatorAddress: opB, Power: 10},\n\t}\n\ttesting.SetRealm(testing.NewCodeRealm(asGovDAOProxy))\n\n\turequire.NoError(t, newValoperChangeExecutor(cur, changes).Execute(cross(cur)))\n\n\teffective := sysparams.GetValsetEffective()\n\turequire.Equal(t, 1, len(effective))\n\tuassert.Equal(t, pubKeyB, effective[0].PubKey)\n\tuassert.Equal(t, uint64(10), effective[0].VotingPower)\n}\n\nfunc TestNewValidatorProposalRequest_RejectsKeepRunningFalseAtCreation(cur realm, t *testing.T) {\n\tresetCache()\n\top := testutils.TestAddress(\"op-A\")\n\tseedCache(t, []struct {\n\t\top          address\n\t\tpubKey      string\n\t\tkeepRunning bool\n\t}{{op: op, pubKey: pubKeyA, keepRunning: false}})\n\n\tuassert.PanicsContains(t, cur, \"KeepRunning=false\", func() {\n\t\t_ = NewValidatorProposalRequest(cur,\n\t\t\t[]ValoperChange{{OperatorAddress: op, Power: 1}},\n\t\t\t\"add opted-out\", \"\",\n\t\t)\n\t})\n}\n\nfunc TestNewValidatorProposalRequest_AllowsRemoveOfKeepRunningFalse(cur realm, t *testing.T) {\n\t// KeepRunning=false is the operator's opt-out signal; removing\n\t// such an operator must still be allowed (it's the standard\n\t// exit path). Only adds are gated.\n\tresetValset(t)\n\tresetCache()\n\n\t// Seed the valset with two operators so removing one doesn't\n\t// trip the empty-valset liveness floor.\n\ttesting.SetSysParamStrings(module, submodule, currKey, []string{\n\t\tpubKeyA + \":10\",\n\t\tpubKeyB + \":5\",\n\t})\n\n\topA := testutils.TestAddress(\"op-A\")\n\topB := testutils.TestAddress(\"op-B\")\n\tseedCache(t, []struct {\n\t\top          address\n\t\tpubKey      string\n\t\tkeepRunning bool\n\t}{\n\t\t{op: opA, pubKey: pubKeyA, keepRunning: false}, // opted out\n\t\t{op: opB, pubKey: pubKeyB, keepRunning: true},\n\t})\n\n\t// Build proposal succeeds (remove path: Power=0 ignores KeepRunning).\n\tpr := NewValidatorProposalRequest(cur,\n\t\t[]ValoperChange{{OperatorAddress: opA, Power: 0}},\n\t\t\"remove opted-out opA\", \"\",\n\t)\n\t_ = pr\n\n\t// Executor also succeeds.\n\ttesting.SetRealm(testing.NewCodeRealm(asGovDAOProxy))\n\n\turequire.NoError(t, newValoperChangeExecutor(cur, []ValoperChange{{OperatorAddress: opA, Power: 0}}).Execute(cross(cur)))\n}\n\nfunc TestNewValidatorProposalRequest_RejectsDuplicateOp(cur realm, t *testing.T) {\n\t// Each operator may appear at most once per proposal; any shape\n\t// that mentions the same op twice must panic at create-time.\n\tresetCache()\n\top := testutils.TestAddress(\"op-A\")\n\tseedCache(t, []struct {\n\t\top          address\n\t\tpubKey      string\n\t\tkeepRunning bool\n\t}{{op: op, pubKey: pubKeyA, keepRunning: true}})\n\n\tcases := [][]ValoperChange{\n\t\t{{OperatorAddress: op, Power: 0}, {OperatorAddress: op, Power: 7}}, // remove + re-add\n\t\t{{OperatorAddress: op, Power: 7}, {OperatorAddress: op, Power: 8}}, // double add\n\t\t{{OperatorAddress: op, Power: 0}, {OperatorAddress: op, Power: 0}}, // double remove\n\t}\n\tfor _, changes := range cases {\n\t\tuassert.PanicsContains(t, cur, \"duplicate operator in proposal\", func() {\n\t\t\t_ = NewValidatorProposalRequest(cur, changes, \"dup\", \"\")\n\t\t})\n\t}\n}\n\nfunc TestNewValidatorProposalRequest_RejectsPowerUpdatePairForOptedOutOp(cur realm, t *testing.T) {\n\t// KeepRunning=false is binding: no proposal shape can keep an\n\t// opted-out operator in the active set. The dedupe rejection\n\t// fires before the KR check is even reached.\n\tresetCache()\n\top := testutils.TestAddress(\"op-A\")\n\tseedCache(t, []struct {\n\t\top          address\n\t\tpubKey      string\n\t\tkeepRunning bool\n\t}{{op: op, pubKey: pubKeyA, keepRunning: false}})\n\n\tuassert.PanicsContains(t, cur, \"duplicate operator in proposal\", func() {\n\t\t_ = NewValidatorProposalRequest(cur,\n\t\t\t[]ValoperChange{\n\t\t\t\t{OperatorAddress: op, Power: 0},\n\t\t\t\t{OperatorAddress: op, Power: 7},\n\t\t\t},\n\t\t\t\"bypass attempt\", \"\",\n\t\t)\n\t})\n}\n\nfunc TestNewValidatorProposalRequest_UpsertExistingValidator(cur realm, t *testing.T) {\n\t// Single-entry {op, newPower} against an op already in the\n\t// effective valset must upsert: the existing entry's power is\n\t// overwritten, no remove/re-add ceremony required.\n\tresetValset(t)\n\tresetCache()\n\n\t// Seed valset with two validators; we upsert opA's power.\n\ttesting.SetSysParamStrings(module, submodule, currKey, []string{\n\t\tpubKeyA + \":1\",\n\t\tpubKeyB + \":1\",\n\t})\n\n\topA := testutils.TestAddress(\"op-A\")\n\topB := testutils.TestAddress(\"op-B\")\n\tseedCache(t, []struct {\n\t\top          address\n\t\tpubKey      string\n\t\tkeepRunning bool\n\t}{\n\t\t{op: opA, pubKey: pubKeyA, keepRunning: true},\n\t\t{op: opB, pubKey: pubKeyB, keepRunning: true},\n\t})\n\n\tchanges := []ValoperChange{{OperatorAddress: opA, Power: 9}}\n\ttesting.SetRealm(testing.NewCodeRealm(asGovDAOProxy))\n\n\turequire.NoError(t, newValoperChangeExecutor(cur, changes).Execute(cross(cur)))\n\n\teffective := sysparams.GetValsetEffective()\n\tpowerOf := map[string]uint64{}\n\tfor _, v := range effective {\n\t\tpowerOf[v.PubKey] = v.VotingPower\n\t}\n\tuassert.Equal(t, uint64(9), powerOf[pubKeyA], \"opA power upserted from 1 to 9\")\n\tuassert.Equal(t, uint64(1), powerOf[pubKeyB], \"opB unchanged\")\n}\n\nfunc TestNewValidatorProposalRequest_ExecutorRejectsRaceFlippedKeepRunning(cur realm, t *testing.T) {\n\t// KeepRunning=true at proposal-create time; operator flips to\n\t// false BEFORE the executor runs. Race-safety check rejects.\n\tresetValset(t)\n\tresetCache()\n\n\top := testutils.TestAddress(\"op-A\")\n\tseedCache(t, []struct {\n\t\top          address\n\t\tpubKey      string\n\t\tkeepRunning bool\n\t}{{op: op, pubKey: pubKeyA, keepRunning: true}})\n\n\tchanges := []ValoperChange{{OperatorAddress: op, Power: 5}}\n\texec := newValoperChangeExecutor(cur, changes)\n\n\t// Operator flips KeepRunning=false BEFORE the executor runs.\n\tvaloperCache.Set(op.String(), cacheEntry{\n\t\tSigningPubKey:  pubKeyA,\n\t\tSigningAddress: mustAddr(t, pubKeyA),\n\t\tKeepRunning:    false,\n\t})\n\n\ttesting.SetRealm(testing.NewCodeRealm(asGovDAOProxy))\n\n\tuassert.AbortsContains(t, cur, \"KeepRunning=false at execution\", func() {\n\t\t_ = exec.Execute(cross(cur))\n\t})\n}\n\nfunc TestNewValidatorProposalRequest_NaturalRotationFlow_NoGhost(cur realm, t *testing.T) {\n\t// RotateValoperSigningKey publishes valset:proposed before any\n\t// subsequent executor reads, so baseline always reflects the\n\t// post-rotation state. A later power-update upserts at NEW only;\n\t// no OLD ghost.\n\tresetValset(t)\n\tresetCache()\n\n\ttesting.SetSysParamStrings(module, submodule, currKey, []string{\n\t\tpubKeyA + \":1\",\n\t\tpubKeyB + \":5\",\n\t})\n\topA := testutils.TestAddress(\"op-A\")\n\topB := testutils.TestAddress(\"op-B\")\n\tseedCache(t, []struct {\n\t\top          address\n\t\tpubKey      string\n\t\tkeepRunning bool\n\t}{\n\t\t{op: opA, pubKey: pubKeyA, keepRunning: true},\n\t\t{op: opB, pubKey: pubKeyB, keepRunning: true},\n\t})\n\n\ttesting.SetRealm(testing.NewCodeRealm(valopersRealmPath))\n\tRotateValoperSigningKey(cross(cur), opA, pubKeyA, pubKeyC)\n\tNotifyValoperChanged(cross(cur), opA, pubKeyC, mustAddr(t, pubKeyC), true)\n\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao/impl/v0\"))\n\turequire.NoError(t, newValoperChangeExecutor(cur,\n\t\t[]ValoperChange{{OperatorAddress: opA, Power: 2}},\n\t).Execute(cross(cur)))\n\n\teffective := sysparams.GetValsetEffective()\n\tpowerOf := map[string]uint64{}\n\tfor _, v := range effective {\n\t\tpowerOf[v.PubKey] = v.VotingPower\n\t}\n\tuassert.Equal(t, uint64(2), powerOf[pubKeyC], \"opA published at NEW power=2\")\n\tuassert.Equal(t, uint64(5), powerOf[pubKeyB], \"opB unchanged\")\n\t_, ghost := powerOf[pubKeyA]\n\tuassert.False(t, ghost, \"OLD signing key (pubKeyA) must not linger in valset\")\n\turequire.Equal(t, 2, len(effective), \"exactly two entries — opA(NEW), opB\")\n}\n\nfunc TestNewValidatorProposalRequest_PhantomBaselineDocumentsUnreachableState(cur realm, t *testing.T) {\n\t// Pin executor behavior on a phantom state (cache=NEW,\n\t// valset:current=OLD, dirty=false) — unreachable via natural\n\t// flow because RotateValoperSigningKey publishes proposed\n\t// before NotifyValoperChanged updates the cache. If a future\n\t// code path ever updates the cache without going through\n\t// Rotate, the asymmetry would be a real bug; this test pins\n\t// the current behavior so the divergence surfaces.\n\tresetValset(t)\n\tresetCache()\n\n\ttesting.SetSysParamStrings(module, submodule, currKey, []string{\n\t\tpubKeyA + \":1\",\n\t\tpubKeyB + \":5\",\n\t})\n\n\topA := testutils.TestAddress(\"op-A\")\n\topB := testutils.TestAddress(\"op-B\")\n\tseedCache(t, []struct {\n\t\top          address\n\t\tpubKey      string\n\t\tkeepRunning bool\n\t}{\n\t\t{op: opA, pubKey: pubKeyC, keepRunning: true},\n\t\t{op: opB, pubKey: pubKeyB, keepRunning: true},\n\t})\n\n\ttesting.SetRealm(testing.NewCodeRealm(asGovDAOProxy))\n\n\turequire.NoError(t, newValoperChangeExecutor(cur,\n\t\t[]ValoperChange{{OperatorAddress: opA, Power: 2}},\n\t).Execute(cross(cur)))\n\n\teffective := sysparams.GetValsetEffective()\n\tpowerOf := map[string]uint64{}\n\tfor _, v := range effective {\n\t\tpowerOf[v.PubKey] = v.VotingPower\n\t}\n\tuassert.Equal(t, uint64(1), powerOf[pubKeyA], \"phantom OLD lingers from baseline\")\n\tuassert.Equal(t, uint64(2), powerOf[pubKeyC], \"executor upserts at NEW\")\n\tuassert.Equal(t, uint64(5), powerOf[pubKeyB], \"opB unchanged\")\n\turequire.Equal(t, 3, len(effective),\n\t\t\"three entries — phantom-state ghost; this state is unreachable via natural flow\")\n}\n\nfunc TestNewValidatorProposalRequest_SameBlockExecuteThenRotate(cur realm, t *testing.T) {\n\t// Same-block ordering: proposal-execute writes proposed\n\t// (dirty=true); a subsequent rotation reads proposed-when-dirty\n\t// and accumulates the prior power change rather than clobbering\n\t// back to current.\n\tresetValset(t)\n\tresetCache()\n\n\ttesting.SetSysParamStrings(module, submodule, currKey, []string{\n\t\tpubKeyA + \":1\",\n\t\tpubKeyB + \":5\",\n\t})\n\n\topA := testutils.TestAddress(\"op-A\")\n\topB := testutils.TestAddress(\"op-B\")\n\tseedCache(t, []struct {\n\t\top          address\n\t\tpubKey      string\n\t\tkeepRunning bool\n\t}{\n\t\t{op: opA, pubKey: pubKeyA, keepRunning: true},\n\t\t{op: opB, pubKey: pubKeyB, keepRunning: true},\n\t})\n\n\ttesting.SetRealm(testing.NewCodeRealm(asGovDAOProxy))\n\n\turequire.NoError(t, newValoperChangeExecutor(cur,\n\t\t[]ValoperChange{{OperatorAddress: opA, Power: 3}},\n\t).Execute(cross(cur)))\n\n\ttesting.SetRealm(testing.NewCodeRealm(valopersRealmPath))\n\tRotateValoperSigningKey(cross(cur), opA, pubKeyA, pubKeyC)\n\tNotifyValoperChanged(cross(cur), opA, pubKeyC, mustAddr(t, pubKeyC), true)\n\n\teffective := sysparams.GetValsetEffective()\n\tpowerOf := map[string]uint64{}\n\tfor _, v := range effective {\n\t\tpowerOf[v.PubKey] = v.VotingPower\n\t}\n\tuassert.Equal(t, uint64(3), powerOf[pubKeyC], \"rotation accumulates with prior upsert; final power=3\")\n\tuassert.Equal(t, uint64(5), powerOf[pubKeyB], \"opB unchanged\")\n\t_, gotOld := powerOf[pubKeyA]\n\tuassert.False(t, gotOld, \"OLD signing key (pubKeyA) must not appear after rotation\")\n\turequire.Equal(t, 2, len(effective), \"exactly two entries — opA(NEW) and opB\")\n}\n\n// TestNewValidatorProposalRequest_ExecutorVanishedCacheEntryPanics pins\n// the \"operator vanished from valoperCache between propose and execute\"\n// branch. No production path deletes from valoperCache today (only Set\n// is called by NotifyValoperChanged), but bptree.BPTree exposes Remove\n// so the underlying data structure does support deletion. This test\n// simulates that hypothetical state directly via the package-private\n// cache var to confirm the executor panics with the documented message\n// rather than silently mis-publishing an empty/wrong valset.\n//\n// If a future commit ever introduces a public cache-delete path, this\n// test still passes — it's a contract-pinning test for the panic itself.\n// If the team decides the branch is unreachable enough to drop, this\n// test is the first thing to break.\nfunc TestNewValidatorProposalRequest_ExecutorVanishedCacheEntryPanics(cur realm, t *testing.T) {\n\tresetValset(t)\n\tresetCache()\n\n\top := testutils.TestAddress(\"op-A\")\n\tseedCache(t, []struct {\n\t\top          address\n\t\tpubKey      string\n\t\tkeepRunning bool\n\t}{{op: op, pubKey: pubKeyA, keepRunning: true}})\n\n\texec := newValoperChangeExecutor(cur,\n\t\t[]ValoperChange{{OperatorAddress: op, Power: 5}},\n\t)\n\n\t// Simulate the unreachable-today state: cache entry deleted between\n\t// proposal-create and proposal-execute. Direct package-private mutation\n\t// (NOT a public API) — production code has no path here today.\n\t_, removed := valoperCache.Remove(op.String())\n\turequire.True(t, removed, \"fixture: cache entry must have been present before Remove\")\n\n\ttesting.SetRealm(testing.NewCodeRealm(asGovDAOProxy))\n\n\tuassert.AbortsContains(t, cur, \"operator vanished from valoperCache between propose and execute\", func() {\n\t\t_ = exec.Execute(cross(cur))\n\t})\n}\n\n// contains is a tiny strings.Contains shim so tests don't import a\n// new package.\nfunc contains(s, substr string) bool {\n\tfor i := 0; i+len(substr) \u003c= len(s); i++ {\n\t\tif s[i:i+len(substr)] == substr {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\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/v0\"\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 v0 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"},{"name":"validators_test.gno","body":"// Tests for stateless v0.\n//\n// State isolation: gno's test runner constructs a fresh testParams\n// per top-level TestXxx, but t.Run subtests share the same map.\n// Each subtest below seeds (or clears) the valset slots it cares\n// about up front; do the same when adding cases.\npackage validators\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nconst (\n\t// Three distinct deterministic test pubkeys (and the addresses\n\t// they derive to) used across the cases below. Stable across\n\t// runs.\n\tpubKeyA = \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zq3ds6sdvc0shfkq02h6xx5g0jp04aadexfnpsmgjxu72xz9y30aqfrlpny\"\n\tpubKeyB = \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqwpdwpd0f9fvqla089ndw5g9hcsufad77fml2vlu73fk8q8sh8v72cza5p\"\n\tpubKeyC = \"gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pqddddqg2glc8x4fl7vxjlnr7p5a3czm5kcdp4239sg6yqdc4rc2r5cjrffs\"\n\n\tmodule    = \"node\"\n\tsubmodule = \"valset\"\n\tcurrKey   = \"current\"\n\tpropKey   = \"proposed\"\n\tdirtyKey  = \"dirty\"\n)\n\nfunc resetValset(t *testing.T) {\n\tt.Helper()\n\ttesting.SetSysParamStrings(module, submodule, currKey, []string{})\n\ttesting.SetSysParamStrings(module, submodule, propKey, []string{})\n\ttesting.SetSysParamBool(module, submodule, dirtyKey, false)\n}\n\nfunc mustAddr(t *testing.T, pk string) address {\n\tt.Helper()\n\ta, err := chain.PubKeyAddress(pk)\n\turequire.NoError(t, err, \"valid pubkey\")\n\treturn a\n}\n\nfunc TestEffectiveView_DirtyFalseReadsCurrent(cur realm, t *testing.T) {\n\tresetValset(t)\n\ttesting.SetSysParamStrings(module, submodule, currKey, []string{pubKeyA + \":10\"})\n\n\taddrA := mustAddr(t, pubKeyA)\n\turequire.True(t, IsValidator(addrA), \"A in current\")\n\turequire.Equal(t, 1, len(GetValidators()))\n}\n\nfunc TestEffectiveView_DirtyTrueReadsProposed(cur realm, t *testing.T) {\n\tresetValset(t)\n\ttesting.SetSysParamStrings(module, submodule, currKey, []string{pubKeyA + \":10\"})\n\ttesting.SetSysParamStrings(module, submodule, propKey, []string{pubKeyA + \":10\", pubKeyB + \":1\"})\n\ttesting.SetSysParamBool(module, submodule, dirtyKey, true)\n\n\taddrA := mustAddr(t, pubKeyA)\n\taddrB := mustAddr(t, pubKeyB)\n\turequire.True(t, IsValidator(addrA), \"A still visible\")\n\turequire.True(t, IsValidator(addrB), \"B visible via proposed\")\n\turequire.Equal(t, 2, len(GetValidators()))\n}\n\nfunc TestGetValidator_ReturnsParsedEntry(cur realm, t *testing.T) {\n\tresetValset(t)\n\ttesting.SetSysParamStrings(module, submodule, currKey, []string{pubKeyA + \":42\"})\n\n\tv := GetValidator(mustAddr(t, pubKeyA))\n\turequire.Equal(t, uint64(42), v.VotingPower)\n\turequire.Equal(t, pubKeyA, v.PubKey)\n}\n\nfunc TestRender_DefaultTestHeight(cur realm, t *testing.T) {\n\tresetValset(t)\n\ttesting.SetSysParamStrings(module, submodule, currKey, []string{pubKeyA + \":10\"})\n\n\tout := Render(\"\")\n\t// gnovm/pkg/test/test.go sets DefaultHeight = 123.\n\turequire.Equal(t,\n\t\t\"## Valset at height 123\\n\\n- #0: \"+mustAddr(t, pubKeyA).String()+\" (10)\\n\",\n\t\tout)\n}\n\n// The same-block accumulation, panic-on-remove-of-missing,\n// panic-on-add-of-existing, and pubkey-validation tests for the\n// signing-keyed NewProposalRequest/newValsetChangeExecutor were\n// removed alongside the function itself. Equivalent coverage for\n// the operator-keyed flow lives in proposal_test.gno\n// (TestNewValidatorProposalRequest_*) and cache_test.gno\n// (TestRotateValoperSigningKey_AccumulatesAcrossSameBlock).\n//\n// Empty-final-set rejection in the executor still applies; it's\n// covered by TestNewValidatorProposalRequest_RemoveOperator in\n// proposal_test.gno.\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"lZ9AXn+YI3zfzcMd2ZTZrIKmAtEI8R2AEweDGqQxi84uKmPZTSiuDw0CdX11We39JbD6jl4kleEclBHuyroxPQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7","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/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/gov/dao\"\n)\n\nvar auth *authz.Authorizer\n\n// Auth returns a read-only description of the realm's current governance\n// authority (for rendering / inspection). It renders as\n//\n//\tcontract_authority[contract=gno.land/r/gov/dao,proposer=contract-identity]\n//\n// i.e. \"GovDAO governs this realm, and only GovDAO may drive it\" — a\n// statement an on-chain reader can act on. The proposer half is\n// load-bearing: without it the string is byte-identical whether the\n// authority is gated or wide open (see ContractAuthority.String).\n//\n// It deliberately does NOT return the live *authz.Authorizer: an\n// exported handle to the authority would let any realm reach its\n// mutators (Transfer, DoByPrevious, ...) directly. Returning a\n// description instead of the handle keeps that surface unreachable from\n// here in the first place. Valoper.AuthOwner follows the same rule for\n// per-operator auth lists.\nfunc Auth() string {\n\treturn auth.String()\n}\n\n// updateInstructions is the realm's only privileged write. It authorizes\n// as the realm that CROSSED INTO valopers, not as valopers itself.\n//\n// Why DoByPrevious and not DoByCurrent: own-path + DoByCurrent is a\n// tautology (see NewContractAuthority's godoc for the mechanism, once).\n// `rlm.Previous()` is the caller — on the legitimate path r/gov/dao's\n// executor frame, which is what init.gno's authority is pointed at.\n//\n// What it buys: it is the code-level guard against this realm's most\n// likely regression, a maintainer later adding an exported crossing\n// entrypoint that reaches here. Under DoByCurrent such an entrypoint\n// authorizes for any caller; under DoByPrevious the caller's own address\n// is the principal and the gate rejects it. Pinned by\n// filetests/z_govdao_only_principal_filetest.gno.\n//\n// Two things it does NOT cover on its own, both closed elsewhere:\n// re-exporting the privileged closure (guarded by the seal in\n// NewInstructionsProposalRequest), and exporting a function whose\n// signature is assignable to dao's `func(realm) error` callback type,\n// which the proxy would invoke on an attacker's behalf. The second was a\n// live bypass of this gate until SimpleExecutor.Execute began rejecting\n// invocation from outside r/gov/dao (r/gov/dao/types.gno); keep BOTH\n// conditions in mind before adding an exported symbol here.\n//\n// Returns the authorization error rather than panicking on it. That matters\n// on the governance path: impl.ExecuteProposal turns an executor ERROR into\n// status Denied plus a DeniedReason, but it cannot see a panic — a panic\n// aborts the whole ExecuteProposal transaction, burning its gas and leaving\n// the proposal stuck Accepted, retryable forever and never deniable. The\n// refusal is currently unreachable on the approved route (Previous() there\n// is always r/gov/dao), but init.gno's handler is explicitly offered as the\n// seam for an intent check (timelock, quorum, audit trail), and the first\n// such check to return an error would hit exactly that.\nfunc updateInstructions(_ int, rlm realm, newInstructions string) error {\n\treturn auth.DoByPrevious(0, rlm, \"update-instructions\", func() error {\n\t\tinstructions = newInstructions\n\t\treturn nil\n\t})\n}\n\n// NewInstructionsProposalRequest builds the GovDAO proposal that rewrites\n// this realm's instructions. It replaces the previously exported\n// NewInstructionsProposalCallback.\n//\n// SECURITY: the privileged closure must never leave this package.\n//\n// The VM mints a crossing frame's `cur` from the CALLEE's declaring\n// package, so a closure declared here always runs with valopers'\n// identity no matter who invokes it — and whoever holds the closure\n// chooses its Previous() by wrapping it in an executor of their own.\n// Handing such a closure to a caller therefore hands out the ability to\n// satisfy this realm's gate whichever principal it is pointed at: the\n// handler in init.gno runs the action and `instructions` is rewritten\n// with no proposal and no vote. That is what the exported callback did.\n//\n// Returning a dao.ProposalRequest instead seals the capability: the\n// executor is an unexported field with no accessor (dao.ProposalRequest\n// exposes only Title/Description/Filter), so the only way to reach the\n// closure is for GovDAO to execute the proposal that contains it.\n//\n// Note this is deliberately stronger than returning a dao.Executor —\n// even a dao.NewSafeExecutor-wrapped one. Executor.Execute is an\n// exported method, so a returned Executor stays directly invocable by\n// its holder, and SafeExecutor's only gate (InAllowedDAOs) FAILS OPEN\n// while the allowedDAOs list is empty, which is the documented\n// bootstrap state (see r/gov/dao/loader/v0). A sealed request has no\n// such conditional.\n//\n// The rule for this realm: no exported function may return a crossing\n// closure, a dao.Executor, or anything else that carries valopers'\n// frame identity — and no exported function may itself BE such a thing,\n// i.e. have a signature assignable to dao's `func(realm) error` callback\n// type, since the proxy would then invoke it on an attacker's behalf.\n// filetests/z_foreign_realm_capability_filetest.gno pins the first;\n// filetests/z_govdao_only_principal_filetest.gno pins the second.\nfunc NewInstructionsProposalRequest(cur realm, newInstructions string) dao.ProposalRequest {\n\tcb := func(cur realm) error {\n\t\treturn updateInstructions(0, cur, newInstructions)\n\t}\n\n\ttitle := instructionsProposalTitle\n\tdescription := ufmt.Sprintf(\"Update the instructions to: \\n\\n%s\", newInstructions)\n\n\treturn dao.NewProposalRequest(title, description, dao.NewSimpleExecutor(0, cur, cb, \"\"))\n}\n\n// instructionsProposalTitle is the on-chain title of the instructions\n// proposal, named as a constant so a test can pin it: it is rendered to\n// GovDAO voters and is the string off-chain tooling matches on. It was\n// \"/p/gnops/valopers: ...\" before this realm's proposal construction moved\n// here; the \"/p/\" was simply wrong (valopers is an /r/ realm).\nconst instructionsProposalTitle = \"/r/gnops/valopers: Update instructions\"\n\n// rotateAuthority replaces this realm's governance authority.\n//\n// Unexported, and reachable only through the sealed request built by\n// NewAuthorityRotationProposalRequest — same discipline as\n// updateInstructions, for the same reason.\n//\n// authz.Authorizer.Transfer derives its principal as\n// rlm.Previous().Address(), so inside a GovDAO-executed callback declared\n// here Previous() is r/gov/dao — exactly the principal init.gno's authority\n// asserts. No same-package cross() trick is needed.\nfunc rotateAuthority(_ int, rlm realm, newContractPath string) error {\n\treturn auth.Transfer(0, rlm, authz.NewContractAuthority(\n\t\tnewContractPath,\n\t\tfunc(_ string, action authz.PrivilegedAction) error {\n\t\t\treturn action()\n\t\t},\n\t))\n}\n\n// NewAuthorityRotationProposalRequest builds the GovDAO proposal that\n// re-points this realm's governance authority at `newContractPath`.\n//\n// WHY THIS EXISTS. Without it the authority installed by init.gno is\n// permanently frozen: nothing else calls auth.Transfer, Auth() returns a\n// description rather than the live Authorizer, and Transfer routes through\n// the authority's own gate — so if the asserted principal ever stops being\n// presentable, `instructions` becomes unwritable with no on-chain recovery.\n// That is not hypothetical: the r/gov/dao proxy path is a governance\n// artifact that can be superseded, and a well-formed but wrong path (say\n// the impl path instead of the proxy) is accepted at construction and dead\n// forever. Redeploying instead is expensive — r/sys/validators/v0/cache.gno\n// hardcodes `const valopersRealmPath`, so a new pkgpath means editing a\n// second genesis realm and re-registering every valoper. It must exist\n// before deploy, because code cannot be added to a deployed realm.\n//\n// WHY A PATH AND NOT AN authz.Authority. Taking an Authority would make\n// this an open-interface input (Class-3 impl-substitution): a proposal\n// could install an always-approve or always-deny authority, and readers of\n// Auth() could not tell from the rendered description alone. Taking a path\n// keeps the installed authority canonical by construction — always a\n// ContractAuthority with the pass-through handler and the contract-identity\n// gate — so the only thing governance can change is WHICH principal is\n// asserted. The path is validated by authz.NewContractAuthority, which\n// panics on a malformed one, aborting the proposal execution rather than\n// bricking the realm.\n//\n// This remains a privileged surface: a majority that can pass this proposal\n// can point the authority at a principal it controls. That is the same\n// power a majority already has over `instructions`, and it is the price of\n// being recoverable at all.\nfunc NewAuthorityRotationProposalRequest(cur realm, newContractPath string) dao.ProposalRequest {\n\tcb := func(cur realm) error {\n\t\treturn rotateAuthority(0, cur, newContractPath)\n\t}\n\n\ttitle := \"/r/gnops/valopers: Rotate governance authority\"\n\tdescription := ufmt.Sprintf(\n\t\t\"Re-point the valopers governance authority at: \\n\\n%s\\n\\n\"+\n\t\t\t\"The authority stays a contract-identity ContractAuthority; only the asserted principal changes.\",\n\t\tnewContractPath,\n\t)\n\n\treturn dao.NewProposalRequest(title, description, dao.NewSimpleExecutor(0, cur, cb, \"\"))\n}\n\n// NewInstructionsProposalCallback was removed, not deprecated: it was the\n// capability leak described above, so an inert shim would be pointless and\n// a working one would reopen the hole. Precedent + replay check: the\n// min-fee callback below was already removed the same way even though it\n// HAD been used on gnoland1 (GovDAO Prop #19), whereas the instructions\n// callback has no on-chain trace at all — the live realm's instructions\n// are byte-identical to the genesis default and none of gnoland1's 26\n// GovDAO proposals is an \"Update instructions\" one.\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":"admin_test.gno","body":"package valopers\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\n// TestAuthDescribesGovDAOGate pins init.gno's configuration, including the\n// proposer half — which is the load-bearing half.\n//\n// Deliberately does NOT reassign `auth`: an earlier version of this test\n// built its own ContractAuthority and then claimed to \"mirror init.gno\",\n// which meant changing the contract path in init.gno left it green.\n//\n// Before ContractAuthority.String() rendered the proposer, this assertion\n// was blind to the drift that matters: swapping init.gno for\n// NewRestrictedContractAuthority(path, handler, NewAutoAcceptAuthority())\n// — i.e. reopening the hole for this realm — left the rendered string\n// byte-identical and the whole valopers suite green.\n//\n// It is also blind no longer to a proposer that merely CLAIMS to be the\n// contract-identity default: authz renders any non-canonical Authority\n// wrapped as custom_authority[...], so a spoofing impl cannot reproduce\n// these bytes. See authz's TestSpoofedProposerCannotImpersonateContractIdentity.\nfunc TestAuthDescribesGovDAOGate(cur realm, t *testing.T) {\n\tuassert.Equal(t,\n\t\t\"contract_authority[contract=gno.land/r/gov/dao,proposer=contract-identity]\",\n\t\tAuth())\n}\n\n// TestUpdateInstructionsRejectsNonGovDAO is the negative pin for the\n// repointed authority, and it is the one that would have caught the\n// regression class this realm fears.\n//\n// The authority is a ContractAuthority for gno.land/r/gov/dao driven by\n// DoByPrevious, so the principal is whoever crossed into valopers. Here\n// that is the test's own frame, not GovDAO's executor — so the privileged\n// write must be refused and `instructions` must not move.\n//\n// Note what this test could NOT assert before the repoint: under the\n// previous ContractAuthority(ownPath) + DoByCurrent pairing, rlm.Address()\n// inside any valopers crossing frame is unconditionally valopers' own\n// address, so this exact call SUCCEEDED and the old version of this test\n// asserted NotPanics. That is why it never pinned anything: the gate it was\n// exercising could not reject.\n//\n// The positive half — that GovDAO can still perform the write — lives in\n// proposal/filetests/z_governed_instructions_filetest.gno, which runs\n// propose -\u003e vote -\u003e execute end to end.\nfunc TestUpdateInstructionsRejectsNonGovDAO(cur realm, t *testing.T) {\n\tbefore := instructions\n\n\t// An ERROR, not a panic: the refusal has to travel back to\n\t// impl.ExecuteProposal as a value so a passed-but-refused proposal can\n\t// be marked Denied instead of aborting the execute transaction. See\n\t// updateInstructions.\n\terr := updateInstructions(0, cur, \"SHOULD-NOT-LAND\")\n\n\tuassert.ErrorContains(t, err, \"unauthorized\",\n\t\t\"a non-GovDAO principal must be refused\")\n\tuassert.Equal(t, before, instructions,\n\t\t\"instructions must not move when the gate refuses\")\n}\n\n// The instructions proposal's on-chain title is rendered to GovDAO voters\n// and is what off-chain tooling matches on, so it is pinned rather than\n// left to drift silently. It changed in this realm's history from\n// \"/p/gnops/valopers: ...\" (wrong — valopers is an /r/ realm) when\n// proposal construction moved out of r/gnops/valopers/proposal.\nfunc TestInstructionsProposalTitleIsPinned(cur realm, t *testing.T) {\n\tuassert.Equal(t, \"/r/gnops/valopers: Update instructions\", instructionsProposalTitle)\n\n\treq := NewInstructionsProposalRequest(cross(cur), \"whatever\")\n\tuassert.Equal(t, instructionsProposalTitle, req.Title())\n}\n\n// The authority must be rotatable by GovDAO, or a principal that stops\n// being presentable freezes `instructions` with no on-chain recovery (and\n// redeploying is expensive: r/sys/validators/v0/cache.gno hardcodes\n// valopersRealmPath). This pins that the rotation path exists, is sealed\n// the same way the instructions write is, and that a non-GovDAO principal\n// cannot drive it.\nfunc TestRotateAuthorityRejectsNonGovDAO(cur realm, t *testing.T) {\n\tbefore := Auth()\n\n\terr := rotateAuthority(0, cur, \"gno.land/r/gov/dao/v4\")\n\n\tuassert.ErrorContains(t, err, \"unauthorized\",\n\t\t\"a non-GovDAO principal must not be able to rotate the authority\")\n\tuassert.Equal(t, before, Auth(), \"the authority must be unchanged\")\n}\n\n// The rotation request is built here and returned sealed: dao.ProposalRequest\n// exposes only Title/Description/Filter, so a holder cannot reach the\n// executor. Anyone may build one; only GovDAO can adopt and run it.\nfunc TestRotationProposalRequestIsSealed(cur realm, t *testing.T) {\n\treq := NewAuthorityRotationProposalRequest(cross(cur), \"gno.land/r/gov/dao/v4\")\n\n\tuassert.Equal(t, \"/r/gnops/valopers: Rotate governance authority\", req.Title())\n\tuassert.NotEqual(t, \"\", req.Description())\n\n\t// Building the request must not itself perform the rotation.\n\tuassert.Equal(t,\n\t\t\"contract_authority[contract=gno.land/r/gov/dao,proposer=contract-identity]\",\n\t\tAuth())\n}\n\n// TestUpdateMinFee was removed: register_fee now lives in sysparams\n// (node:valoper:register_fee), governed via the generic\n// NewSysParamUint64PropRequest factory. See proposal.gno for the\n// replacement flow.\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/v0\"\n\t\"gno.land/p/moul/txlink/v0\"\n\t\"gno.land/p/nt/avl/v0\"\n)\n\nfunc init() {\n\tvalopers = avl.NewTree()\n\n\t// The authority is pointed at GovDAO, NOT at this realm.\n\t//\n\t// Pointing it here instead would make the gate compare valopers'\n\t// address to valopers' address: every crossing frame of this package\n\t// has rlm.Address() == PackageAddress(ownPath), so the check could\n\t// never reject. Pointed at r/gov/dao and driven by DoByPrevious\n\t// (see updateInstructions), the principal is the realm that crossed\n\t// in — GovDAO's executor on the legitimate path, and the caller\n\t// itself on any other, which is what makes the check real.\n\t//\n\t// The handler is a pass-through: this is where an intent check\n\t// (timelock, quorum, audit trail) would go if one is ever wanted.\n\t// It is NOT where the caller check lives — that is the authority's\n\t// contract-identity gate, upstream of here.\n\tauth = authz.NewWithAuthority(\n\t\tauthz.NewContractAuthority(\n\t\t\t\"gno.land/r/gov/dao\",\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":"pubkey_type_test.gno","body":"package valopers\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nconst (\n\ttestEd25519PubKey   = \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqkn3nln2k8glpf9kpqtz4h63n7cd0jvxkq6rpk4dxzp4sq527mhqwv40hr\"\n\ttestSecp256k1PubKey = \"gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pq0skzdkmzu0r9h6gny6eg8c9dc303xrrudee6z4he4y7cs5rnjwmyf40yaj\"\n)\n\nfunc TestPubKeyTypeURL(t *testing.T) {\n\tgot, err := pubKeyTypeURL(testEd25519PubKey)\n\tuassert.NoError(t, err)\n\tuassert.Equal(t, \"/tm.PubKeyEd25519\", got)\n\n\tgot, err = pubKeyTypeURL(testSecp256k1PubKey)\n\tuassert.NoError(t, err)\n\tuassert.Equal(t, \"/tm.PubKeySecp256k1\", got)\n\n\t_, err = pubKeyTypeURL(\"not-a-valid-bech32-pubkey\")\n\tuassert.Error(t, err)\n}\n\nfunc TestAssertPubKeyTypeAllowed(t *testing.T) {\n\t// Empty allow-list accepts any well-formed type.\n\ttesting.SetSysParamStrings(\"node\", \"valset\", \"pubkey_types\", []string{})\n\tuassert.NotPanics(t, cur, func() { assertPubKeyTypeAllowed(testSecp256k1PubKey) })\n\n\t// ed25519-only allow-list: ed25519 accepted, secp256k1 rejected.\n\ttesting.SetSysParamStrings(\"node\", \"valset\", \"pubkey_types\", []string{\"/tm.PubKeyEd25519\"})\n\tuassert.NotPanics(t, cur, func() { assertPubKeyTypeAllowed(testEd25519PubKey) })\n\tuassert.PanicsWithMessage(t, cur, ErrDisallowedPubKeyType.Error(), func() {\n\t\tassertPubKeyTypeAllowed(testSecp256k1PubKey)\n\t})\n\n\t// Reset so the non-empty allow-list doesn't leak into other tests.\n\ttesting.SetSysParamStrings(\"node\", \"valset\", \"pubkey_types\", []string{})\n}\n"},{"name":"rotate_test.gno","body":"package valopers\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nconst (\n\t// Second valid pubkey used as the rotation target. Distinct from\n\t// validValidatorInfo's pubkey so the signingRegistry uniqueness\n\t// check is exercised.\n\trotateTargetPubKey = \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zq3ds6sdvc0shfkq02h6xx5g0jp04aadexfnpsmgjxu72xz9y30aqfrlpny\"\n\n\t// Test-local mirror of the rotation_period_blocks sys-param's\n\t// default. resetState() seeds the same value into sysparams.\n\ttestRotationPeriodBlocks = int64(600)\n)\n\n// registerForRotation does the boilerplate setup that every rotation\n// test starts from: clear state, register the valoper as info.Address,\n// and return the info struct. rlm is threaded into Register via\n// cross(rlm) — it's a non-first parameter so the helper stays a\n// regular (non-crossing) function.\nfunc registerForRotation(t *testing.T, rlm realm) struct {\n\tMoniker     string\n\tDescription string\n\tServerType  string\n\tAddress     address\n\tPubKey      string\n} {\n\tt.Helper()\n\tresetState()\n\tinfo := validValidatorInfo(t)\n\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\ttesting.SetOriginSend(chain.Coins{minFee})\n\tRegister(cross(rlm), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\treturn info\n}\n\nfunc TestUpdateSigningKey_HappyPath(cur realm, t *testing.T) {\n\tinfo := registerForRotation(t, cur)\n\n\t// Advance past the throttle window. SkipHeights doesn't preserve\n\t// the realm context across the boundary, so re-set it before the\n\t// next cross-call.\n\ttesting.SkipHeights(testRotationPeriodBlocks + 1)\n\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\n\tuassert.NotAborts(t, cur, func() {\n\t\tUpdateSigningKey(cross(cur), info.Address, rotateTargetPubKey)\n\t})\n\n\tv := GetByAddr(info.Address)\n\tuassert.Equal(t, rotateTargetPubKey, v.SigningPubKey)\n\n\tnewSigningAddr, err := chain.PubKeyAddress(rotateTargetPubKey)\n\turequire.NoError(t, err)\n\tuassert.Equal(t, newSigningAddr, v.SigningAddress)\n\n\t// New entry active in registry.\n\trawNew := signingRegistry.Get(newSigningAddr.String())\n\turequire.NotNil(t, rawNew, \"new signing addr in registry\")\n\tnewEntry := rawNew.(regEntry)\n\tuassert.Equal(t, info.Address, newEntry.OperatorAddress)\n\tuassert.Equal(t, int64(0), newEntry.RetiredAtHeight)\n\n\t// Old entry retired (still present, RetiredAtHeight \u003e 0).\n\toldSigningAddr, err := chain.PubKeyAddress(info.PubKey)\n\turequire.NoError(t, err)\n\trawOld := signingRegistry.Get(oldSigningAddr.String())\n\turequire.NotNil(t, rawOld, \"old signing addr retained as retired\")\n\toldEntry := rawOld.(regEntry)\n\tuassert.Equal(t, info.Address, oldEntry.OperatorAddress)\n\tuassert.True(t, oldEntry.RetiredAtHeight \u003e 0, \"old entry must be retired (RetiredAtHeight \u003e 0)\")\n}\n\nfunc TestUpdateSigningKey_ThrottleRejection(cur realm, t *testing.T) {\n\tinfo := registerForRotation(t, cur)\n\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\n\t// Try to rotate immediately, before throttle elapses.\n\tuassert.AbortsWithMessage(t, cur, ErrRotationThrottled.Error(), func() {\n\t\tUpdateSigningKey(cross(cur), info.Address, rotateTargetPubKey)\n\t})\n\n\t// Advance just shy of the threshold; still rejected.\n\ttesting.SkipHeights(testRotationPeriodBlocks - 1)\n\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\tuassert.AbortsWithMessage(t, cur, ErrRotationThrottled.Error(), func() {\n\t\tUpdateSigningKey(cross(cur), info.Address, rotateTargetPubKey)\n\t})\n\n\t// One more block — now exactly at the threshold; allowed.\n\ttesting.SkipHeights(1)\n\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\tuassert.NotAborts(t, cur, func() {\n\t\tUpdateSigningKey(cross(cur), info.Address, rotateTargetPubKey)\n\t})\n}\n\nfunc TestUpdateSigningKey_RejectsNonAuthListCaller(cur realm, t *testing.T) {\n\tinfo := registerForRotation(t, cur)\n\ttesting.SkipHeights(testRotationPeriodBlocks + 1)\n\n\t// Switch to a caller not on the auth list.\n\ttesting.SetRealm(testing.NewUserRealm(testutils.TestAddress(\"attacker\")))\n\n\t// Pin the exact authorizable error so this catches an auth\n\t// regression rather than any abort. Empty-substring match (the\n\t// previous form) accepted any panic, including unrelated ones.\n\tuassert.AbortsContains(t, cur, \"not in authorized list\", func() {\n\t\tUpdateSigningKey(cross(cur), info.Address, rotateTargetPubKey)\n\t})\n}\n\nfunc TestUpdateSigningKey_RejectsReuseOfActiveKey(cur realm, t *testing.T) {\n\tinfo := registerForRotation(t, cur)\n\ttesting.SkipHeights(testRotationPeriodBlocks + 1)\n\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\n\t// Try to \"rotate\" to the same key — already in registry.\n\tuassert.AbortsWithMessage(t, cur, ErrSigningKeyTaken.Error(), func() {\n\t\tUpdateSigningKey(cross(cur), info.Address, info.PubKey)\n\t})\n}\n\nfunc TestUpdateSigningKey_RejectsRotationOntoActiveValidator(cur realm, t *testing.T) {\n\t// Front-running guard: a rotation whose derived signing address is\n\t// already an active validator must be rejected. Mirrors the same\n\t// guard in Register (valopers.gno: ErrFrontrunValidator). Without\n\t// this, any valoper could rotate onto an unregistered (e.g.\n\t// genesis-seeded) validator slot — v0's executor would overwrite\n\t// the active validator's entry in the effective valset with this\n\t// operator's claim, then a subsequent govDAO remove-op proposal\n\t// would delete it. signingRegistry uniqueness alone doesn't catch\n\t// this case: a genesis-seeded validator never went through\n\t// Register or UpdateSigningKey, so its signing address is absent\n\t// from signingRegistry.\n\tinfo := registerForRotation(t, cur)\n\n\t// Seed v0's valset:current with the address that rotateTargetPubKey\n\t// derives to. ValsetEffective is read from valset:current when the\n\t// dirty flag is unset, which is the default.\n\ttesting.SetSysParamStrings(\"node\", \"valset\", \"current\",\n\t\t[]string{rotateTargetPubKey + \":1\"})\n\n\ttesting.SkipHeights(testRotationPeriodBlocks + 1)\n\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\n\tuassert.AbortsWithMessage(t, cur, ErrFrontrunValidator.Error(), func() {\n\t\tUpdateSigningKey(cross(cur), info.Address, rotateTargetPubKey)\n\t})\n\n\t// Cleanup: clear the seeded valset to avoid leaking into sibling\n\t// subtests if the runner doesn't reset package state between them.\n\ttesting.SetSysParamStrings(\"node\", \"valset\", \"current\", []string{})\n}\n\nfunc TestUpdateSigningKey_RejectsReuseOfRetiredKey(cur realm, t *testing.T) {\n\tinfo := registerForRotation(t, cur)\n\n\t// First rotation: info.PubKey -\u003e rotateTargetPubKey. info.PubKey\n\t// becomes retired.\n\ttesting.SkipHeights(testRotationPeriodBlocks + 1)\n\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\tUpdateSigningKey(cross(cur), info.Address, rotateTargetPubKey)\n\n\t// Second rotation back to info.PubKey must fail — it's retired\n\t// but signingRegistry retains it forever.\n\ttesting.SkipHeights(testRotationPeriodBlocks + 1)\n\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\tuassert.AbortsWithMessage(t, cur, ErrSigningKeyTaken.Error(), func() {\n\t\tUpdateSigningKey(cross(cur), info.Address, info.PubKey)\n\t})\n}\n\nfunc TestUpdateSigningKey_LastRotationHeightAdvances(cur realm, t *testing.T) {\n\tinfo := registerForRotation(t, cur)\n\ttesting.SkipHeights(testRotationPeriodBlocks + 1)\n\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\tUpdateSigningKey(cross(cur), info.Address, rotateTargetPubKey)\n\n\tv := GetByAddr(info.Address)\n\tuassert.True(t, v.LastRotationHeight \u003e 0, \"LastRotationHeight set on rotation\")\n\n\t// Immediately after, throttle rejects again.\n\tuassert.AbortsWithMessage(t, cur, ErrRotationThrottled.Error(), func() {\n\t\tUpdateSigningKey(cross(cur), info.Address, \"gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pqddddqg2glc8x4fl7vxjlnr7p5a3czm5kcdp4239sg6yqdc4rc2r5cjrffs\")\n\t})\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\"math\"\n\t\"regexp\"\n\n\t\"gno.land/p/moul/realmpath/v0\"\n\t\"gno.land/p/nt/avl/pager/v0\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/combinederr/v0\"\n\t\"gno.land/p/nt/ownable/exts/authorizable/v0\"\n\t\"gno.land/p/nt/ownable/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\tsysparams \"gno.land/r/sys/params\"\n\tvalidators \"gno.land/r/sys/validators/v0\"\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\tErrPaidCallNotDirect    = errors.New(\"a fee is configured: call this directly as a user (maketx call), not from a realm or a maketx run script\")\n\tErrFeeParamOutOfRange   = errors.New(\"configured fee exceeds the maximum representable coin amount\")\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\n// AuthOwner returns the operator address that owns this profile's auth\n// list. Read-only by construction: it copies out an address rather than\n// returning the live *authorizable.Authorizable.\n//\n// SECURITY: the previous `Auth() *authorizable.Authorizable` was a\n// capability leak of the same class this realm's governance authority\n// closes by returning a description instead of the live authority.\n// `Valoper` is returned BY VALUE from the exported, non-crossing\n// GetByAddr, but `auth` is a pointer field, so the copy shared the\n// callee's Authorizable. Any realm could therefore obtain a live,\n// mutable handle and write through it.\n//\n// That was a privilege escalation, not merely a wider surface.\n// Authorizable's gates read `rlm.Previous().Address()`, so inside a\n// hostile realm's frame `Previous()` is whoever called it. When a\n// valoper OPERATOR called any function of a hostile realm (faucet,\n// airdrop, mint), that realm could reach\n// `GetByAddr(operator).Auth().AddToAuthList(...)` and — because\n// Previous() was then the operator, the Authorizable's own owner —\n// persist itself onto the operator's auth list. From the next\n// transaction on it acted alone: UpdateKeepRunning to drain the\n// validator, UpdateSigningKey to rotate the consensus key.\n//\n// The exported wrappers below are NOT equivalent to the raw handle and\n// were never the hole: there `cur.Previous()` is the hostile realm\n// itself rather than the operator, so the owner check rejects it.\nfunc (v Valoper) AuthOwner() address {\n\treturn v.auth.Owner()\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 for the SQUAT guard (no\n// IsUserCall): squatting requires the attacker to satisfy\n// OriginCaller==victim, which requires the victim's signing key.\n//\n// The PAYMENT check is a different matter and does need IsUserCall —\n// see assertPaidCallIsDirect. An earlier version of this comment\n// claimed the fee was \"validated against banker.OriginSend in a way\n// that's symmetric to IsUserCall via direct comparison\"; there was no\n// such comparison, and the fee was bypassable (reported by @D4ryl00).\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\tassertPaidCallIsDirect(0, cur)\n\t\tminFee := minFeeCoin(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 v0'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 v0.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 v0'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; v0 emits\n// remove+add to sysparams via RotateValoperSigningKey; v0 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 v0.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\tassertPaidCallIsDirect(0, cur)\n\t\tminFee := minFeeCoin(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: v0.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 v0 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 v0, then refresh v0'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// assertPaidCallIsDirect requires the immediate caller to be a plain EOA,\n// and must be paired with every unsafe.OriginSend() amount check in this\n// realm. Called only when a fee is actually configured.\n//\n// unsafe.OriginSend() reports the transaction's declared send ENVELOPE, not\n// what this realm received. Only a direct `maketx call` on valopers credits\n// the envelope to this realm's address. For a `maketx run` the keeper sets\n// pkgAddr := caller (gno.land/pkg/sdk/vm/keeper.go), so the coins move from\n// the caller to the caller and never land anywhere at all, while\n// OriginSend() still reports the full amount to us. See\n// gno.land/adr/pr6062_payable_send_check.md: \"MsgRun is exempt: the coins\n// are moved from the caller to the caller, so nothing actually moves.\" So\n// the amount check alone verifies intent, never receipt.\n//\n// Reported by @D4ryl00, with a local-chain reproduction:\n// governance sets register_fee to 1000ugnot, an operator runs a script\n// calling Register with `-send 1000ugnot`, registration succeeds, valopers\n// receives zero, and the operator still holds the coins. No hostile script\n// is needed — the envelope is free by construction. Both fees default to 0,\n// so nothing was live.\n//\n// Why IsUserCall and not an address comparison: a run script's realm is\n// address-INDISTINGUISHABLE from its user. Inside `main(cur realm)` the\n// ephemeral realm's own address IS the caller's EOA address, so every\n// address-based guard in this realm accepts a `maketx run` — Register's\n// squat guard (OriginCaller == addr) and UpdateSigningKey's auth-list check\n// (Previous().Address() on the list) both pass. Only the pkgPath differs,\n// and IsUserCall() is the check that reads it. It is therefore the only\n// PreviousRealm shape where the envelope is guaranteed to have landed here:\n// it excludes intermediate code realms and user-run ephemeral realms alike.\n// Same reasoning and same pairing as r/sys/namereg/v0.Register, which reads\n// OriginSend for its anti-squatting payment; see the two-guard comment there.\n//\n// Deliberately inside the `fee \u003e 0` branch rather than at the top of the\n// function: while a fee is unset there is no payment to establish receipt\n// of, and gating unconditionally would break the operator-authored\n// `maketx run` flows that the squat guard is happy to accept. The\n// restriction appears exactly when, and only when, money is involved.\n//\n// KNOWN LIMIT. AddToAuthList takes a plain `member address`, so a REALM\n// can sit on an operator's auth list, and once rotation_fee is nonzero\n// such a member can no longer drive UpdateSigningKey — it is not a user\n// call. An EOA rotation bot is unaffected, which is the shape the\n// auth-list docs describe (\"HSM-bound or rotation-bot\"), so nothing\n// documented breaks; a realm-mediated rotation would.\n//\n// The alternative that would keep realms working is a true receipt check:\n// track cumulative fees in realm state and require the realm's own\n// balance to have risen by `fee` since the last one. That works for any\n// caller shape, but it puts new persisted state and a banker into a\n// genesis realm, and lets anyone pre-pay another operator's fee by\n// donating to the realm address. Not worth it while both fees are 0 and\n// collected fees are unwithdrawable anyway (this realm has no banker, so\n// they strand at its address). Revisit if a realm-mediated paid rotation\n// is ever actually wanted.\nfunc assertPaidCallIsDirect(_ int, rlm realm) {\n\t// IsCurrent before Previous, as AGENTS.md requires and every other\n\t// (_ int, rlm realm) helper in this tree does (authorizable,\n\t// sys/params/delegate, gov/dao/types, sys/validators/v0/cache).\n\t// Both call sites pass their live cur, so this is defense-in-depth\n\t// against a future caller threading a stale or stashed realm value:\n\t// Previous() reads the prev field verbatim, so on a non-live token\n\t// it answers for the wrong frame.\n\tif !rlm.IsCurrent() {\n\t\tpanic(ErrPaidCallNotDirect)\n\t}\n\tif !rlm.Previous().IsUserCall() {\n\t\tpanic(ErrPaidCallNotDirect)\n\t}\n}\n\n// minFeeCoin converts a configured fee (uint64, from sysparams) into the\n// ugnot Coin the amount checks compare against.\n//\n// Coin.Amount is an int64. A fee above MaxInt64 wraps negative, and\n// `sentCoins[0].IsLT(minFee)` is then false for every payment — the realm\n// reads the fee as configured, runs assertPaidCallIsDirect, and collects\n// 1ugnot while reporting \"payment must not be less than -1ugnot\". Failing\n// closed here keeps an unrepresentable fee from opening the paid path\n// instead of closing it.\n//\n// Reachable two ways today, neither requiring malice: governance can set\n// any uint64 through the generic r/sys/params factories, and\n// valopers/proposal.ProposeNewMinFeeProposalRequest takes an int64 and\n// stores uint64(newMinFee), so a negative fee — the obvious way to write\n// \"disable the fee\" — round-trips to 2^64-1. That signature is preserved\n// for historical-replay compatibility and is deliberately left alone; the\n// bound belongs at the consumption site, which covers both paths.\nfunc minFeeCoin(fee uint64) chain.Coin {\n\tif fee \u003e math.MaxInt64 {\n\t\tpanic(ErrFeeParamOutOfRange)\n\t}\n\treturn chain.NewCoin(\"ugnot\", int64(fee))\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"},{"name":"valopers_test.gno","body":"package valopers\n\nimport (\n\t\"chain\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/ownable/exts/authorizable/v0\"\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Test-local fee constant. Production reads register_fee from sysparams;\n// these tests use this value as a reference Coin for OriginSend setup\n// and (when needed) seed it via testing.SetSysParamUint64.\nvar minFee = chain.NewCoin(\"ugnot\", 20*1_000_000)\n\n// cur is a zero-value realm used as a placeholder when forwarding to\n// uassert dispatch helpers that gained an `rlm realm` param. These tests\n// pass `func()` callbacks (no crossing inside the callback), so rlm is\n// ignored — a nil realm here is safe.\nvar cur realm\n\n// resetState clears realm-level state and zeroes the valoper sys-params\n// so subtests don't leak through valopers (operator slots),\n// signingRegistry (signing-address uniqueness), or sys-param values.\nfunc resetState() {\n\tvalopers = avl.NewTree()\n\tsigningRegistry = bptree.NewBPTree32()\n\ttesting.SetSysParamUint64(\"node\", \"valoper\", \"register_fee\", 0)\n\ttesting.SetSysParamUint64(\"node\", \"valoper\", \"rotation_fee\", 0)\n\ttesting.SetSysParamInt64(\"node\", \"valoper\", \"rotation_period_blocks\", 600)\n}\n\n// enableRegisterFee seeds register_fee = minFee.Amount in sysparams\n// so the Register fee path fires. Subtests that exercise fee\n// rejection or sufficient-fee acceptance call this after resetState.\nfunc enableRegisterFee() {\n\ttesting.SetSysParamUint64(\"node\", \"valoper\", \"register_fee\", uint64(minFee.Amount))\n}\n\nfunc validValidatorInfo(t *testing.T) struct {\n\tMoniker     string\n\tDescription string\n\tServerType  string\n\tAddress     address\n\tPubKey      string\n} {\n\tt.Helper()\n\n\treturn struct {\n\t\tMoniker     string\n\t\tDescription string\n\t\tServerType  string\n\t\tAddress     address\n\t\tPubKey      string\n\t}{\n\t\tMoniker:     \"test-1\",\n\t\tDescription: \"test-1's description\",\n\t\tServerType:  ServerTypeOnPrem,\n\t\tAddress:     address(\"g1sp8v98h2gadm5jggtzz9w5ksexqn68ympsd68h\"),\n\t\tPubKey:      \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqwpdwpd0f9fvqla089ndw5g9hcsufad77fml2vlu73fk8q8sh8v72cza5p\",\n\t}\n}\n\nfunc TestValopers_Register(cur realm, t *testing.T) {\n\tt.Run(\"already a valoper\", func(cur realm, t *testing.T) {\n\t\tresetState()\n\n\t\tinfo := validValidatorInfo(t)\n\t\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\n\t\tv := Valoper{\n\t\t\tMoniker:         info.Moniker,\n\t\t\tDescription:     info.Description,\n\t\t\tServerType:      info.ServerType,\n\t\t\tOperatorAddress: info.Address,\n\t\t\tSigningPubKey:   info.PubKey,\n\t\t\tKeepRunning:     true,\n\t\t}\n\n\t\t// Add the valoper directly to the slot.\n\t\tvalopers.Set(v.OperatorAddress.String(), v)\n\n\t\t// Send coins.\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\tuassert.AbortsWithMessage(t, cur, ErrValoperExists.Error(), func() {\n\t\t\tRegister(cross(cur), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\t})\n\n\tt.Run(\"no coins deposited\", func(cur realm, t *testing.T) {\n\t\tresetState()\n\t\tenableRegisterFee()\n\n\t\tinfo := validValidatorInfo(t)\n\t\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\n\t\t// Send no coins.\n\t\ttesting.SetOriginSend(chain.Coins{chain.NewCoin(\"ugnot\", 0)})\n\n\t\tuassert.AbortsWithMessage(t, cur, ufmt.Sprintf(\"payment must not be less than %d%s\", minFee.Amount, minFee.Denom), func() {\n\t\t\tRegister(cross(cur), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\t})\n\n\tt.Run(\"insufficient coins amount deposited\", func(cur realm, t *testing.T) {\n\t\tresetState()\n\t\tenableRegisterFee()\n\n\t\tinfo := validValidatorInfo(t)\n\t\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\n\t\t// Send invalid coins.\n\t\ttesting.SetOriginSend(chain.Coins{chain.NewCoin(\"ugnot\", minFee.Amount-1)})\n\n\t\tuassert.AbortsWithMessage(t, cur, ufmt.Sprintf(\"payment must not be less than %d%s\", minFee.Amount, minFee.Denom), func() {\n\t\t\tRegister(cross(cur), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\t})\n\n\tt.Run(\"coin amount deposited is not ugnot\", func(cur realm, t *testing.T) {\n\t\tresetState()\n\t\tenableRegisterFee()\n\n\t\tinfo := validValidatorInfo(t)\n\t\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\n\t\t// Send invalid coins.\n\t\ttesting.SetOriginSend(chain.Coins{chain.NewCoin(\"gnogno\", minFee.Amount)})\n\n\t\tuassert.AbortsWithMessage(t, cur, \"incompatible coin denominations: gnogno, ugnot\", func() {\n\t\t\tRegister(cross(cur), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\t})\n\n\t// A fee above MaxInt64 wraps negative in Coin.Amount, which would make\n\t// the IsLT amount check false for every payment: the realm would read\n\t// the fee as configured and collect 1ugnot. uint64(int64(-1)) is what\n\t// valopers/proposal.ProposeNewMinFeeProposalRequest stores for a\n\t// negative newMinFee, so this is reachable from one governance typo.\n\tt.Run(\"unrepresentable fee fails closed\", func(cur realm, t *testing.T) {\n\t\tresetState()\n\t\ttesting.SetSysParamUint64(\"node\", \"valoper\", \"register_fee\", ^uint64(0))\n\n\t\tinfo := validValidatorInfo(t)\n\t\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\n\t\t// Nowhere near the configured fee, and previously accepted.\n\t\ttesting.SetOriginSend(chain.Coins{chain.NewCoin(\"ugnot\", 1)})\n\n\t\tuassert.AbortsWithMessage(t, cur, ErrFeeParamOutOfRange.Error(), func() {\n\t\t\tRegister(cross(cur), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\t\tuassert.False(t, isValoper(info.Address), \"must not register\")\n\t})\n\n\tt.Run(\"squat guard rejects mismatched OriginCaller\", func(cur realm, t *testing.T) {\n\t\tresetState()\n\n\t\tinfo := validValidatorInfo(t)\n\t\t// Caller is NOT info.Address: post-genesis squat guard fires.\n\t\ttesting.SetRealm(testing.NewUserRealm(testutils.TestAddress(\"attacker\")))\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\tuassert.AbortsWithMessage(t, cur, ErrOperatorSquatGuard.Error(), func() {\n\t\t\tRegister(cross(cur), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\t})\n\n\tt.Run(\"successful registration\", func(cur realm, t *testing.T) {\n\t\tresetState()\n\n\t\tinfo := validValidatorInfo(t)\n\t\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\n\t\t// Send coins.\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\tuassert.NotAborts(t, cur, func() {\n\t\t\tRegister(cross(cur), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tvaloper := GetByAddr(info.Address)\n\n\t\t\tuassert.Equal(t, info.Moniker, valoper.Moniker)\n\t\t\tuassert.Equal(t, info.Description, valoper.Description)\n\t\t\tuassert.Equal(t, info.ServerType, valoper.ServerType)\n\t\t\tuassert.Equal(t, info.Address, valoper.OperatorAddress)\n\t\t\tuassert.Equal(t, info.PubKey, valoper.SigningPubKey)\n\t\t\tuassert.Equal(t, true, valoper.KeepRunning)\n\n\t\t\t// SigningAddress is derived from the pubkey and present.\n\t\t\tderived, err := chain.PubKeyAddress(info.PubKey)\n\t\t\tuassert.NoError(t, err)\n\t\t\tuassert.Equal(t, derived, valoper.SigningAddress)\n\n\t\t\t// signingRegistry tracks the active entry.\n\t\t\tuassert.True(t, signingRegistry.Has(derived.String()), \"signingRegistry must contain the active entry\")\n\t\t})\n\t})\n\n\tt.Run(\"signing-key reuse rejected\", func(cur realm, t *testing.T) {\n\t\tresetState()\n\n\t\tinfo := validValidatorInfo(t)\n\t\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\t// First registration succeeds.\n\t\tuassert.NotAborts(t, cur, func() {\n\t\t\tRegister(cross(cur), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\t// Second attempt with a different operator addr but the same\n\t\t// pubkey must fail signingRegistry uniqueness.\n\t\tother := testutils.TestAddress(\"other-op\")\n\t\ttesting.SetRealm(testing.NewUserRealm(other))\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\tuassert.AbortsWithMessage(t, cur, ErrSigningKeyTaken.Error(), func() {\n\t\t\tRegister(cross(cur), info.Moniker, info.Description, info.ServerType, other, info.PubKey)\n\t\t})\n\t})\n\n\tt.Run(\"front-running guard rejects post-genesis if signing addr already validates\", func(cur realm, t *testing.T) {\n\t\tresetState()\n\n\t\tinfo := validValidatorInfo(t)\n\t\t// Seed v0's valset:current with the very signing address that\n\t\t// `info.PubKey` derives to (g1sp8v98...). Any post-genesis\n\t\t// Register attempting the same pubkey now trips\n\t\t// `ChainHeight()\u003e0 \u0026\u0026 validators.IsValidator(signingAddr)`.\n\t\ttesting.SetSysParamStrings(\"node\", \"valset\", \"current\", []string{info.PubKey + \":1\"})\n\n\t\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\tuassert.AbortsWithMessage(t, cur, ErrFrontrunValidator.Error(), func() {\n\t\t\tRegister(cross(cur), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\t// Cleanup: clear the seeded valset to avoid leaking into\n\t\t// later subtests if the package state isn't reset between\n\t\t// them in this test runner mode.\n\t\ttesting.SetSysParamStrings(\"node\", \"valset\", \"current\", []string{})\n\t})\n}\n\nfunc TestValopers_Register_AuthOwnerIsOperatorAddress(cur realm, t *testing.T) {\n\t// Pin: the Authorizable owner is bound to the OperatorAddress\n\t// (the addr arg), NOT to OriginCaller. This matters in the\n\t// genesis-mode deployer pattern: one signer (e.g., the hardfork\n\t// ceremony deployer) registers profiles for many operators.\n\t// Each operator must end up on their own profile's auth list\n\t// so they can manage it post-genesis without depending on the\n\t// deployer.\n\tt.Run(\"genesis deployer pattern: operator (not deployer) is owner\", func(cur realm, t *testing.T) {\n\t\tresetState()\n\n\t\tinfo := validValidatorInfo(t)\n\t\tdeployer := testutils.TestAddress(\"deployer\")\n\n\t\t// Genesis mode: ChainHeight()==0 bypasses the squat guard so\n\t\t// deployer (OriginCaller) can register a profile for a\n\t\t// different operator addr.\n\t\ttesting.SetHeight(0)\n\t\ttesting.SetRealm(testing.NewUserRealm(deployer))\n\t\ttesting.SetOriginCaller(deployer)\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tRegister(cross(cur), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\t// Auth owner must be the operator addr (info.Address), NOT\n\t\t// the deployer.\n\t\t//\n\t\t// Asserted through the exported AuthOwner() accessor, not the\n\t\t// private v.auth: AuthOwner is the API that replaced the removed\n\t\t// Auth() capability leak, so it is what external readers see. A\n\t\t// second assertion against the underlying Authorizable pins that\n\t\t// the accessor actually reports the auth list's owner rather\n\t\t// than echoing OperatorAddress, which is what a plausible\n\t\t// \"simplification\" of AuthOwner would do.\n\t\tv := GetByAddr(info.Address)\n\t\tuassert.Equal(t, info.Address.String(), v.AuthOwner().String(),\n\t\t\t\"auth owner must equal OperatorAddress, not OriginCaller\")\n\t\tuassert.Equal(t, v.auth.Owner().String(), v.AuthOwner().String(),\n\t\t\t\"AuthOwner must report the Authorizable's owner\")\n\n\t\t// Operator can manage their own profile post-genesis.\n\t\ttesting.SetHeight(100)\n\t\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\t\ttesting.SetOriginCaller(info.Address)\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tUpdateMoniker(cross(cur), info.Address, \"operator-renamed\")\n\t\t})\n\t\tuassert.Equal(t, \"operator-renamed\", GetByAddr(info.Address).Moniker)\n\n\t\t// Deployer cannot manage the operator's profile (not on the\n\t\t// auth list).\n\t\ttesting.SetRealm(testing.NewUserRealm(deployer))\n\t\ttesting.SetOriginCaller(deployer)\n\t\tuassert.AbortsContains(t, cur, \"caller is not in authorized list\", func() {\n\t\t\tUpdateMoniker(cross(cur), info.Address, \"deployer-attempt\")\n\t\t})\n\t})\n\n\tt.Run(\"post-genesis self-Register: operator is owner\", func(cur realm, t *testing.T) {\n\t\t// At H\u003e0 the squat guard forces OriginCaller==addr, so owner\n\t\t// would be the same regardless. This subtest pins that the\n\t\t// post-genesis behavior is unchanged.\n\t\tresetState()\n\n\t\tinfo := validValidatorInfo(t)\n\t\ttesting.SetHeight(100)\n\t\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\t\ttesting.SetOriginCaller(info.Address)\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tRegister(cross(cur), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\tv := GetByAddr(info.Address)\n\t\tuassert.Equal(t, info.Address.String(), v.AuthOwner().String())\n\t})\n}\n\nfunc TestValopers_UpdateAuthMembers(cur realm, t *testing.T) {\n\ttest2Address := testutils.TestAddress(\"test2\")\n\n\tt.Run(\"unauthorized member adds member\", func(cur realm, t *testing.T) {\n\t\tresetState()\n\n\t\tinfo := validValidatorInfo(t)\n\t\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\t// Add the valoper.\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tRegister(cross(cur), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\t// A different caller (not on the auth list) tries to add a member.\n\t\ttesting.SetRealm(testing.NewUserRealm(test2Address))\n\n\t\tuassert.AbortsWithMessage(t, cur, authorizable.ErrNotSuperuser.Error(), func() {\n\t\t\tAddToAuthList(cross(cur), info.Address, test2Address)\n\t\t})\n\t})\n\n\tt.Run(\"unauthorized member deletes member\", func(cur realm, t *testing.T) {\n\t\tresetState()\n\n\t\tinfo := validValidatorInfo(t)\n\t\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tRegister(cross(cur), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tAddToAuthList(cross(cur), info.Address, test2Address)\n\t\t})\n\n\t\t// A different caller tries to delete a member.\n\t\ttesting.SetRealm(testing.NewUserRealm(testutils.TestAddress(\"attacker\")))\n\n\t\tuassert.AbortsWithMessage(t, cur, authorizable.ErrNotSuperuser.Error(), func() {\n\t\t\tDeleteFromAuthList(cross(cur), info.Address, test2Address)\n\t\t})\n\t})\n\n\tt.Run(\"authorized member adds member\", func(cur realm, t *testing.T) {\n\t\tresetState()\n\n\t\tinfo := validValidatorInfo(t)\n\t\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tRegister(cross(cur), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tAddToAuthList(cross(cur), info.Address, test2Address)\n\t\t})\n\n\t\ttesting.SetRealm(testing.NewUserRealm(test2Address))\n\n\t\tnewMoniker := \"new moniker\"\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tUpdateMoniker(cross(cur), info.Address, newMoniker)\n\t\t})\n\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tvaloper := GetByAddr(info.Address)\n\t\t\tuassert.Equal(t, newMoniker, valoper.Moniker)\n\t\t})\n\t})\n}\n\nfunc TestValopers_UpdateMoniker(cur realm, t *testing.T) {\n\ttest2Address := testutils.TestAddress(\"test2\")\n\n\tt.Run(\"non-existing valoper\", func(cur realm, t *testing.T) {\n\t\tresetState()\n\n\t\tinfo := validValidatorInfo(t)\n\n\t\tuassert.AbortsWithMessage(t, cur, ErrValoperMissing.Error(), func() {\n\t\t\tUpdateMoniker(cross(cur), info.Address, \"new moniker\")\n\t\t})\n\t})\n\n\tt.Run(\"invalid caller\", func(cur realm, t *testing.T) {\n\t\tresetState()\n\n\t\tinfo := validValidatorInfo(t)\n\t\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tRegister(cross(cur), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\t// Change the caller to someone not on the auth list.\n\t\ttesting.SetRealm(testing.NewUserRealm(test2Address))\n\n\t\tuassert.AbortsWithMessage(t, cur, authorizable.ErrNotInAuthList.Error(), func() {\n\t\t\tUpdateMoniker(cross(cur), info.Address, \"new moniker\")\n\t\t})\n\t})\n\n\tt.Run(\"invalid moniker\", func(cur realm, t *testing.T) {\n\t\tresetState()\n\n\t\tinfo := validValidatorInfo(t)\n\t\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tRegister(cross(cur), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\tinvalidMonikers := []string{\n\t\t\t\"\",     // Empty\n\t\t\t\"    \", // Whitespace\n\t\t\t\"a\",    // Too short\n\t\t\t\"a very long moniker that is longer than 32 characters\", // Too long\n\t\t\t\"!@#$%^\u0026*()+{}|:\u003c\u003e?/.,;'\",                               // Invalid characters\n\t\t\t\" space in front\",\n\t\t\t\"space in back \",\n\t\t}\n\n\t\tfor _, invalidMoniker := range invalidMonikers {\n\t\t\tuassert.AbortsWithMessage(t, cur, ErrInvalidMoniker.Error(), func() {\n\t\t\t\tUpdateMoniker(cross(cur), info.Address, invalidMoniker)\n\t\t\t})\n\t\t}\n\t})\n\n\tt.Run(\"too long moniker\", func(cur realm, t *testing.T) {\n\t\tresetState()\n\n\t\tinfo := validValidatorInfo(t)\n\t\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tRegister(cross(cur), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\tuassert.AbortsWithMessage(t, cur, ErrInvalidMoniker.Error(), func() {\n\t\t\tUpdateMoniker(cross(cur), info.Address, strings.Repeat(\"a\", MonikerMaxLength+1))\n\t\t})\n\t})\n\n\tt.Run(\"successful update\", func(cur realm, t *testing.T) {\n\t\tresetState()\n\n\t\tinfo := validValidatorInfo(t)\n\t\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tRegister(cross(cur), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\tnewMoniker := \"new moniker\"\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tUpdateMoniker(cross(cur), info.Address, newMoniker)\n\t\t})\n\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tvaloper := GetByAddr(info.Address)\n\t\t\tuassert.Equal(t, newMoniker, valoper.Moniker)\n\t\t})\n\t})\n}\n\nfunc TestValopers_UpdateDescription(cur realm, t *testing.T) {\n\ttest2Address := testutils.TestAddress(\"test2\")\n\n\tt.Run(\"non-existing valoper\", func(cur realm, t *testing.T) {\n\t\tresetState()\n\n\t\tuassert.AbortsWithMessage(t, cur, ErrValoperMissing.Error(), func() {\n\t\t\tUpdateDescription(cross(cur), validValidatorInfo(t).Address, \"new description\")\n\t\t})\n\t})\n\n\tt.Run(\"invalid caller\", func(cur realm, t *testing.T) {\n\t\tresetState()\n\n\t\tinfo := validValidatorInfo(t)\n\t\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tRegister(cross(cur), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\ttesting.SetRealm(testing.NewUserRealm(test2Address))\n\n\t\tuassert.AbortsWithMessage(t, cur, authorizable.ErrNotInAuthList.Error(), func() {\n\t\t\tUpdateDescription(cross(cur), info.Address, \"new description\")\n\t\t})\n\t})\n\n\tt.Run(\"empty description\", func(cur realm, t *testing.T) {\n\t\tresetState()\n\n\t\tinfo := validValidatorInfo(t)\n\t\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tRegister(cross(cur), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\tuassert.AbortsWithMessage(t, cur, ErrInvalidDescription.Error(), func() {\n\t\t\tUpdateDescription(cross(cur), info.Address, \"\")\n\t\t})\n\t})\n\n\tt.Run(\"too long description\", func(cur realm, t *testing.T) {\n\t\tresetState()\n\n\t\tinfo := validValidatorInfo(t)\n\t\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tRegister(cross(cur), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\tuassert.AbortsWithMessage(t, cur, ErrInvalidDescription.Error(), func() {\n\t\t\tUpdateDescription(cross(cur), info.Address, strings.Repeat(\"a\", DescriptionMaxLength+1))\n\t\t})\n\t})\n\n\tt.Run(\"successful update\", func(cur realm, t *testing.T) {\n\t\tresetState()\n\n\t\tinfo := validValidatorInfo(t)\n\t\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tRegister(cross(cur), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\tnewDescription := \"new description\"\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tUpdateDescription(cross(cur), info.Address, newDescription)\n\t\t})\n\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tvaloper := GetByAddr(info.Address)\n\t\t\tuassert.Equal(t, newDescription, valoper.Description)\n\t\t})\n\t})\n}\n\nfunc TestValopers_UpdateKeepRunning(cur realm, t *testing.T) {\n\ttest2Address := testutils.TestAddress(\"test2\")\n\n\tt.Run(\"non-existing valoper\", func(cur realm, t *testing.T) {\n\t\tresetState()\n\n\t\tuassert.AbortsWithMessage(t, cur, ErrValoperMissing.Error(), func() {\n\t\t\tUpdateKeepRunning(cross(cur), validValidatorInfo(t).Address, false)\n\t\t})\n\t})\n\n\tt.Run(\"invalid caller\", func(cur realm, t *testing.T) {\n\t\tresetState()\n\n\t\tinfo := validValidatorInfo(t)\n\t\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tRegister(cross(cur), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\ttesting.SetRealm(testing.NewUserRealm(test2Address))\n\n\t\tuassert.AbortsWithMessage(t, cur, authorizable.ErrNotInAuthList.Error(), func() {\n\t\t\tUpdateKeepRunning(cross(cur), info.Address, false)\n\t\t})\n\t})\n\n\tt.Run(\"successful update\", func(cur realm, t *testing.T) {\n\t\tresetState()\n\n\t\tinfo := validValidatorInfo(t)\n\t\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tRegister(cross(cur), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tUpdateKeepRunning(cross(cur), info.Address, false)\n\t\t})\n\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tvaloper := GetByAddr(info.Address)\n\t\t\tuassert.Equal(t, false, valoper.KeepRunning)\n\t\t})\n\t})\n}\n\nfunc TestValopers_UpdateServerType(cur realm, t *testing.T) {\n\ttest2Address := testutils.TestAddress(\"test2\")\n\n\tt.Run(\"non-existing valoper\", func(cur realm, t *testing.T) {\n\t\tresetState()\n\n\t\tuassert.AbortsWithMessage(t, cur, ErrValoperMissing.Error(), func() {\n\t\t\tUpdateServerType(cross(cur), validValidatorInfo(t).Address, ServerTypeCloud)\n\t\t})\n\t})\n\n\tt.Run(\"invalid caller\", func(cur realm, t *testing.T) {\n\t\tresetState()\n\n\t\tinfo := validValidatorInfo(t)\n\t\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tRegister(cross(cur), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\ttesting.SetRealm(testing.NewUserRealm(test2Address))\n\n\t\tuassert.AbortsWithMessage(t, cur, authorizable.ErrNotInAuthList.Error(), func() {\n\t\t\tUpdateServerType(cross(cur), info.Address, ServerTypeCloud)\n\t\t})\n\t})\n\n\tt.Run(\"invalid server type\", func(cur realm, t *testing.T) {\n\t\tresetState()\n\n\t\tinfo := validValidatorInfo(t)\n\t\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tRegister(cross(cur), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\tinvalidServerTypes := []string{\n\t\t\t\"\",\n\t\t\t\"invalid\",\n\t\t\t\"Cloud\",      // case sensitive\n\t\t\t\"ON-PREM\",    // case sensitive\n\t\t\t\"datacenter\", // wrong format\n\t\t}\n\n\t\tfor _, invalidType := range invalidServerTypes {\n\t\t\tuassert.AbortsWithMessage(t, cur, ErrInvalidServerType.Error(), func() {\n\t\t\t\tUpdateServerType(cross(cur), info.Address, invalidType)\n\t\t\t})\n\t\t}\n\t})\n\n\tt.Run(\"successful update\", func(cur realm, t *testing.T) {\n\t\tresetState()\n\n\t\tinfo := validValidatorInfo(t)\n\t\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tRegister(cross(cur), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tUpdateServerType(cross(cur), info.Address, ServerTypeCloud)\n\t\t})\n\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tvaloper := GetByAddr(info.Address)\n\t\t\tuassert.Equal(t, ServerTypeCloud, valoper.ServerType)\n\t\t})\n\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tUpdateServerType(cross(cur), info.Address, ServerTypeDataCenter)\n\t\t})\n\n\t\tuassert.NotPanics(t, cur, func() {\n\t\t\tvaloper := GetByAddr(info.Address)\n\t\t\tuassert.Equal(t, ServerTypeDataCenter, valoper.ServerType)\n\t\t})\n\t})\n}\n"},{"name":"z_1_filetest.gno","body":"// PKGPATH: gno.land/r/gnops/valopers/filetests/z_1\n// SEND: 20000000ugnot\n\npackage z_1\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/r/gnops/valopers\"\n)\n\nvar g1user = testutils.TestAddress(\"g1user\") // g1vuch2um9wf047h6lta047h6lta047h6l2ewm6w\n\nconst (\n\tvalidMoniker     = \"test-1\"\n\tvalidDescription = \"test-1's description\"\n\tvalidServerType  = valopers.ServerTypeOnPrem\n\tvalidAddress     = address(\"g1sp8v98h2gadm5jggtzz9w5ksexqn68ympsd68h\")\n\tvalidPubKey      = \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqwpdwpd0f9fvqla089ndw5g9hcsufad77fml2vlu73fk8q8sh8v72cza5p\"\n)\n\nfunc init(cur realm) {\n\t// OriginCaller must equal the operator address (post-genesis squat guard).\n\ttesting.SetOriginCaller(validAddress)\n\n\t// Register a validator and add the proposal\n\tvalopers.Register(cross(cur), validMoniker, validDescription, validServerType, validAddress, validPubKey)\n}\n\nfunc main() {\n\tprintln(valopers.Render(\"\"))\n}\n\n// Output:\n//\n// # Welcome to the **Valopers** realm\n//\n// ## 📌 Purpose of this Contract\n//\n// The **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//\n// By 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//\n// To add your validator node to the registry, use the [**Register**](/r/gnops/valopers$help\u0026func=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//\n// Please provide detailed answers to the following questions to ensure transparency and improve your chances of being accepted:\n//\n// 1. The name of your validator\n// 2. Networks you are currently validating and your total AuM (assets under management)\n// 3. 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.\n// 4. Contact details\n// 5. Why are you interested in validating on **gno.land**?\n// 6. What contributions have you made or are willing to make to **gno.land**?\n//\n// ---\n//\n// ## 🔄 Updating Your Validator Information\n//\n// After 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//\n// Once 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//\n// If you are a GovDAO member, you can nominate yourself by executing the following function: [**r/gnops/valopers/proposal.ProposeNewValidator**](/r/gnops/valopers/proposal$help\u0026func=ProposeNewValidator)\n//\n// This 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//\n// Read more: [How to become a validator](https://github.com/gnolang/gno/tree/master/gno.land/cmd/gnoland#become-a-validator)\n//\n// Disclaimer: 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//  * [test-1](/r/gnops/valopers:g1sp8v98h2gadm5jggtzz9w5ksexqn68ympsd68h) - [profile](/r/demo/profile:u/g1sp8v98h2gadm5jggtzz9w5ksexqn68ympsd68h)\n"},{"name":"z_2_filetest.gno","body":"// PKGPATH: gno.land/r/gnops/valopers/filetests/z_2\n// SEND: 20000000ugnot\n\npackage z_2\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/r/gnops/valopers\"\n)\n\nvar g1user = testutils.TestAddress(\"g1user\")\n\nconst (\n\tvalidMoniker     = \"test-1\"\n\tvalidDescription = \"test-1's description\"\n\tvalidServerType  = valopers.ServerTypeOnPrem\n\tvalidAddress     = address(\"g1sp8v98h2gadm5jggtzz9w5ksexqn68ympsd68h\")\n\tvalidPubKey      = \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqwpdwpd0f9fvqla089ndw5g9hcsufad77fml2vlu73fk8q8sh8v72cza5p\"\n)\n\nfunc init(cur realm) {\n\t// OriginCaller must equal the operator address (post-genesis squat guard).\n\t// validAddress here was chosen so that derive(validPubKey) == validAddress,\n\t// so OperatorAddress == SigningAddress in this fixture.\n\ttesting.SetOriginCaller(validAddress)\n\n\t// Register a validator and add the proposal\n\tvalopers.Register(cross(cur), validMoniker, validDescription, validServerType, validAddress, validPubKey)\n}\n\nfunc main() {\n\t// Simulate clicking on the validator\n\tprintln(valopers.Render(validAddress.String()))\n}\n\n// Output:\n// Valoper's details:\n// ## test-1\n// test-1's description\n//\n// - Operator Address: g1sp8v98h2gadm5jggtzz9w5ksexqn68ympsd68h\n// - Signing Address: g1sp8v98h2gadm5jggtzz9w5ksexqn68ympsd68h\n// - Signing PubKey: gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqwpdwpd0f9fvqla089ndw5g9hcsufad77fml2vlu73fk8q8sh8v72cza5p\n// - Server Type: on-prem\n//\n// [Profile link](/r/demo/profile:u/g1sp8v98h2gadm5jggtzz9w5ksexqn68ympsd68h)\n//\n"},{"name":"z_auth_readonly_filetest.gno","body":"// PKGPATH: gno.land/r/authreadonly\n//\n// Auth() exposes the realm's governance authority for rendering and\n// inspection only. It returns a read-only string, never the live\n// *authz.Authorizer.\n//\n// SCOPE. This asserts two things, one of which is now security-relevant:\n//\n//  1. Auth()'s RETURN TYPE — that the mutable handle is gone from the\n//     type system. This is what makes an\n//     Auth().Transfer(...) PoC stop compiling.\n//  2. The PROPOSER, since ContractAuthority.String() now renders it.\n//     Previously the string was byte-identical whether the authority was\n//     the gated default or a wide-open AutoAccept proposer, so reverting\n//     the fix left this file green. It no longer does.\n//\n// Reachability is still pinned elsewhere, not here:\n//   - p/moul/authz TestDefaultProposerIsContractOnly,\n//     TestExternalTransferRejected, TestExternalDoByPreviousRejected,\n//     TestContractPathIsolation,\n//     TestForeignFrameCannotDriveContractAuthority and\n//     TestContractAuthorityUnauthorizedCaller — the contract-identity\n//     gate. Reverting it fails those 6 plus TestAuthorityString,\n//     TestExplicitProposerReplacesIdentityGate,\n//     TestSpoofedProposerCannotImpersonateContractIdentity and\n//     filetests/z_contract_authority_shape_filetest.gno, since the\n//     rendered proposer is now part of what is pinned.\n//   - ./z_foreign_realm_capability_filetest.gno — the consumer-level\n//     one: a foreign realm cannot reach the privileged capability.\n//   - ./z_govdao_only_principal_filetest.gno — that the repointed\n//     authority names GovDAO and that the principal cannot be MINTED:\n//     it drives the forged-executor route and MustCreateProposal, and\n//     asserts both are refused.\npackage authreadonly\n\nimport (\n\t\"gno.land/r/gnops/valopers\"\n)\n\nfunc main(cur realm) {\n\t// An external realm observes a description; there is no mutator to\n\t// reach from here.\n\tprintln(\"authority:\", valopers.Auth())\n}\n\n// Output:\n// authority: contract_authority[contract=gno.land/r/gov/dao,proposer=contract-identity]\n"},{"name":"z_foreign_realm_capability_filetest.gno","body":"// PKGPATH: gno.land/r/zzattacker\n//\n// Consumer-level regression pin for the governance capability leak.\n//\n// The package-level pins live in p/moul/authz. This is the consumer-level\n// one: an unrelated realm must not be able to drive a valopers privileged\n// action.\n//\n// Before the fix this file could not exist as a negative test — the\n// realm exported NewInstructionsProposalCallback, a crossing closure\n// declared in valopers. The VM mints a crossing frame's `cur` from the\n// CALLEE's declaring package, so invoking it from anywhere ran\n// updateInstructions with cur.Address() == the ContractAuthority's\n// contractAddr. The contract-identity gate accepted, the init.gno\n// handler ran the action, and `instructions` was rewritten with no\n// proposal and no vote:\n//\n//\terr: undefined\n//\tPWNED-BY-ATTACKER\n//\n// The capability is now sealed inside a dao.ProposalRequest, whose\n// executor field is unexported with no accessor. This file asserts the\n// two halves of that: the leak is gone from the type system, and the\n// request an attacker can still construct is inert in their hands.\n//\n// NOTE for whoever edits valopers next: the property under test is\n// \"no exported function returns anything carrying this realm's frame\n// identity\". Re-exporting a func(realm) error, or returning a\n// dao.Executor (whose Execute method is exported and directly\n// invocable), reopens the hole even though this file would still\n// compile.\npackage zzattacker\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/r/gnops/valopers\"\n\t\"gno.land/r/gov/dao\"\n\tdaov3init \"gno.land/r/gov/dao/init/v0\"\n)\n\n// A real GovDAO exists and the attacker is not in it.\nvar govdaoMember address = testutils.TestAddress(\"govdao-member\")\n\nfunc init(cur realm) {\n\tdaov3init.InitWithUsers(cross(cur), govdaoMember)\n}\n\nfunc main(cur realm) {\n\t// 1. The bare capability is gone. Uncommenting this must not compile:\n\t//\n\t//\tcb := valopers.NewInstructionsProposalCallback(\"PWNED-BY-ATTACKER\")\n\t//\tcb(cross(cur))\n\t//\n\t// What remains is the sealed request. An unrelated realm may still\n\t// build one — construction is harmless — but dao.ProposalRequest\n\t// exposes only Title/Description/Filter, so there is no way to reach\n\t// the executor and run it. Only GovDAO executing the proposal can.\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/zzattacker\"))\n\n\treq := valopers.NewInstructionsProposalRequest(cross(cur), \"PWNED-BY-ATTACKER\")\n\tprintln(\"attacker built a request:\", req.Title())\n\n\t// 2. The privileged write did not happen: the instructions are still\n\t//    the genesis text, not the attacker's. Under the exported callback\n\t//    this line already printed \"defaced: true\".\n\trendered := valopers.Render(\"\")\n\tprintln(\"instructions defaced:\", strings.Contains(rendered, \"PWNED-BY-ATTACKER\"))\n\tprintln(\"instructions intact:\", strings.Contains(rendered, \"Welcome to the **Valopers** realm\"))\n\n\t// 3. Runtime pin, not merely a symbol-absence one. dao.ProposalRequest\n\t//    exposes Title/Description/Filter and nothing that yields the\n\t//    executor, so the one remaining route for a holder is to ask\n\t//    GovDAO to adopt the request. A hostile realm cannot: this aborts.\n\t//    (A GovDAO *member* still can, and then it needs the votes — that\n\t//    is the legitimate path, covered by\n\t//    r/gnops/valopers/proposal/filetests/z_governed_instructions_filetest.gno.)\n\tdao.MustCreateProposal(cross(cur), req)\n\tprintln(\"UNREACHABLE: outsider got the proposal adopted\")\n}\n\n// Output:\n// attacker built a request: /r/gnops/valopers: Update instructions\n// instructions defaced: false\n// instructions intact: true\n\n// Error:\n// proposal creation must be done directly by a user or through the r/gov/dao proxy. caller realm: realm{gno.land/r/gov/dao:g1p84dvfh4wrplyxx4zsmy77a8rxcnjup2j7zv5r}; caller's previous: realm{gno.land/r/zzattacker:g1q577nx395ehscyn9ucutvfpj03a5tuk7x4v3ad}\n"},{"name":"z_govdao_only_principal_filetest.gno","body":"// PKGPATH: gno.land/r/zzprincipal\n//\n// The repointed authority names GovDAO, so a foreign realm crossing into\n// valopers is NOT the principal the gate accepts.\n//\n// This is the runtime counterpart to\n// ./z_foreign_realm_capability_filetest.gno. That file pins that the\n// privileged CAPABILITY cannot be obtained; this one pins that the\n// PRINCIPAL cannot be minted — which is the other half of what the gate\n// depends on, and the half that was actually broken.\n//\n// An earlier version of this file asserted only the rendered description\n// plus \"instructions were not defaced\" without ever attempting a write, so\n// it went red on a repoint revert purely because the golden path string\n// changed, and stayed GREEN with the authority swapped for a fully\n// permissive one. It now drives the two routes an attacker actually has.\n//\n// Why it matters: under the previous ContractAuthority(ownPath) +\n// DoByCurrent pairing this property did not hold at all. rlm.Address()\n// inside any valopers crossing frame is unconditionally valopers' own\n// address, so the gate accepted every caller and its only real defence was\n// that no exported entrypoint happened to reach updateInstructions.\n//\n// And pointing the gate at r/gov/dao only helps while that path is\n// unmintable. dao.NewSimpleExecutor + SimpleExecutor.Execute are both\n// exported, and Execute is declared in r/gov/dao, so before Execute gated\n// on its invoker ANY realm could mint a frame whose Previous() was\n// r/gov/dao and satisfy this gate — directly for a closure it obtained,\n// and for any exported valopers function whose signature was assignable to\n// `func(realm) error`. Section 2 below is the pin for that.\npackage zzprincipal\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/r/gnops/valopers\"\n\t\"gno.land/r/gov/dao\"\n\tdaov3init \"gno.land/r/gov/dao/init/v0\"\n)\n\n// A real GovDAO exists and this realm is not a member of it, so the final\n// step fails on the proposal-creation gate rather than on \"DAO not\n// initialized\" -- which would pass for the wrong reason.\nvar govdaoMember address = testutils.TestAddress(\"govdao-member\")\n\nfunc init(cur realm) {\n\tdaov3init.InitWithUsers(cross(cur), govdaoMember)\n}\n\n// reachInstructions is the shape an attacker needs: something declared\n// where it will run with a useful frame. Declared HERE, so it carries the\n// attacker's identity, not valopers' — the point being that even with the\n// governance principal minted for it, it cannot reach the write.\nfunc reachInstructions(cur realm) error {\n\t// Nothing exported by valopers reaches updateInstructions, so the most\n\t// an attacker can do from a forged governance frame is build the sealed\n\t// request again. Build it, to prove construction is not the hole.\n\t_ = valopers.NewInstructionsProposalRequest(cross(cur), \"PWNED-VIA-FORGED-PRINCIPAL\")\n\treturn nil\n}\n\nfunc main(cur realm) {\n\t// 1. The authority names GovDAO, and says so in a way a reader can act\n\t//    on. A non-canonical proposer cannot reproduce these bytes.\n\tprintln(\"authority:\", valopers.Auth())\n\n\t// 2. The governance principal is not mintable. Wrapping any callback in\n\t//    dao's own exported executor used to hand out a frame whose\n\t//    Previous() is r/gov/dao; Execute now refuses an invoker from\n\t//    outside the proxy.\n\te := dao.NewSimpleExecutor(0, cur, reachInstructions, \"\")\n\tprintln(\"forged-principal Execute:\", e.Execute(cross(cur)))\n\n\t// 3. The only remaining route is to ask GovDAO to adopt a request. A\n\t//    hostile realm cannot: this aborts (a GovDAO member still can, and\n\t//    then needs the votes — covered by\n\t//    r/gnops/valopers/proposal/filetests/z_governed_instructions_filetest.gno).\n\trendered := valopers.Render(\"\")\n\tprintln(\"instructions defaced:\", strings.Contains(rendered, \"PWNED\"))\n\tprintln(\"authority unchanged:\", valopers.Auth())\n\n\tdao.MustCreateProposal(cross(cur),\n\t\tvalopers.NewAuthorityRotationProposalRequest(cross(cur), \"gno.land/r/attacker/dao\"))\n\tprintln(\"UNREACHABLE: outsider rotated the authority\")\n}\n\n// Output:\n// authority: contract_authority[contract=gno.land/r/gov/dao,proposer=contract-identity]\n// forged-principal Execute: execution denied: executors are only invocable by gno.land/r/gov/dao\n// instructions defaced: false\n// authority unchanged: contract_authority[contract=gno.land/r/gov/dao,proposer=contract-identity]\n\n// Error:\n// proposal creation must be done directly by a user or through the r/gov/dao proxy. caller realm: realm{gno.land/r/gov/dao:g1p84dvfh4wrplyxx4zsmy77a8rxcnjup2j7zv5r}; caller's previous: realm{gno.land/r/zzprincipal:g1036vewy55meppdzpngzsufahr389mldycavect}\n"},{"name":"z_operator_authlist_capability_filetest.gno","body":"// PKGPATH: gno.land/r/zzoperatorauth\n//\n// Regression pin for the per-operator auth-list capability leak\n// (reported by @thehowl; pre-existing, fixed here because it is the same\n// class as the governance capability leak, and the same audit that found\n// that one should have found it).\n//\n// THE HOLE. `Valoper.Auth() *authorizable.Authorizable` was exported and\n// `GetByAddr` is exported and non-crossing. Valoper is returned BY\n// VALUE, but `auth` is a pointer field, so the copy shared the realm's\n// live Authorizable. Authorizable's gates read\n// `rlm.Previous().Address()`, so inside a hostile realm's frame\n// Previous() is whoever called it. A valoper OPERATOR merely calling any\n// function of a hostile realm (faucet, airdrop, mint) let that realm run\n//\n//\tvalopers.GetByAddr(operator).Auth().AddToAuthList(0, cur, attacker)\n//\n// with Previous() == the operator, i.e. the Authorizable's own owner.\n// The write was accepted and PERSISTED. From the next transaction on,\n// the attacker acted alone: UpdateKeepRunning to drain the validator,\n// UpdateSigningKey to rotate its consensus signing key.\n//\n// THE FIX is the one layer 2 applies to the realm's governance\n// authority: never export the live capability. `Auth()` is removed;\n// `AuthOwner()` copies out an address.\n//\n// WHAT THIS FILE PINS, honestly scoped. The raw-handle attack is a\n// compile-time absence now, so — like the foreign-realm filetest —\n// reverting the fix fails this on a build error rather than an\n// assertion. What it\n// drives at RUNTIME is the property that makes the removal safe: the\n// exported wrapper is NOT an equivalent route. There `cur.Previous()` is\n// the hostile realm rather than the operator, so the superuser check\n// refuses, and it refuses even in the exact scenario that used to work —\n// the operator calling the hostile realm themselves.\n//\n// NOTE for whoever edits valopers next: re-adding any exported accessor\n// that returns *authorizable.Authorizable (or the *ownable.Ownable\n// inside it) reopens this, and this file would still compile.\npackage zzoperatorauth\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/r/gnops/valopers\"\n)\n\nvar (\n\toperator = testutils.TestAddress(\"operator\")\n\tattacker = chain.PackageAddress(\"gno.land/r/zzoperatorauth\")\n\tpubKey   = \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqwpdwpd0f9fvqla089ndw5g9hcsufad77fml2vlu73fk8q8sh8v72cza5p\"\n)\n\n// Pwn is an ordinary exported function of a hostile realm — the airdrop\n// the operator was told to claim. It needs no privilege of its own; it\n// used to just read the public handle and write through it.\nfunc Pwn(cur realm, victim address) {\n\t// The attack the fix removes. Uncommenting must not compile:\n\t//\n\t//\tvalopers.GetByAddr(victim).Auth().AddToAuthList(0, cur, attacker)\n\t//\t                           ^^^^^^ undefined\n\t//\n\t// What remains is the exported wrapper. It is not equivalent: it\n\t// derives the principal from THIS frame, where Previous() is\n\t// gno.land/r/zzoperatorauth and not the operator. Aborts.\n\tvalopers.AddToAuthList(cross(cur), victim, attacker)\n\tprintln(\"UNREACHABLE: hostile realm joined the operator's auth list\")\n}\n\nfunc main(cur realm) {\n\t// The operator registers their profile normally.\n\ttesting.SetOriginCaller(operator)\n\ttesting.SetRealm(testing.NewUserRealm(operator))\n\tvalopers.Register(cross(cur), \"Op\",\n\t\t\"a valid description for the operator profile\", \"cloud\", operator, pubKey)\n\n\t// The auth list is owned by the operator, and AuthOwner hands back\n\t// an address — a value, not a capability.\n\tprintln(\"owner is operator:\",\n\t\tvalopers.GetByAddr(operator).AuthOwner() == operator)\n\tprintln(\"attacker is not owner:\",\n\t\tvalopers.GetByAddr(operator).AuthOwner() != attacker)\n\n\t// The operator calls the hostile realm once. This single step used\n\t// to be the whole attack.\n\ttesting.SetOriginCaller(operator)\n\ttesting.SetRealm(testing.NewUserRealm(operator))\n\tPwn(cross(cur), operator)\n}\n\n// Output:\n// owner is operator: true\n// attacker is not owner: true\n\n// Error:\n// authorizable: caller is not superuser\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"7onz+SaoiYoTKsKBtLnr2A4qpdavAvDF72afVuqi91EvDfIwJieCnLCKzb0gtcREYA5s9yQFE5LGNF/NLwbAQw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7","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/v0\"\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// v0's operator-keyed NewValidatorProposalRequest with a single-element\n// slice; v0'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 v0'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.\n//\n// Signature is preserved; the body now delegates. Building the request\n// here required valopers to export the privileged closure, which handed\n// this realm's frame identity to any caller. The\n// closure and the request are now built inside valopers, which returns\n// the request with its executor sealed — see valopers.NewInstructionsProposalRequest.\nfunc ProposeNewInstructionsProposalRequest(cur realm, newInstructions string) dao.ProposalRequest {\n\treturn valopers.NewInstructionsProposalRequest(cross(cur), newInstructions)\n}\n\n// ProposeAuthorityRotationProposalRequest creates a proposal to the GovDAO\n// for re-pointing the valopers realm's governance authority.\n//\n// Delegates, for the same reason as above: the rotation closure reaches\n// auth.Transfer, so it must never leave the valopers package. Exposed here\n// because this package is where proposal factories for the realm live —\n// see valopers.NewAuthorityRotationProposalRequest for why it takes a path\n// rather than an authz.Authority, and why it exists at all.\nfunc ProposeAuthorityRotationProposalRequest(cur realm, newContractPath string) dao.ProposalRequest {\n\treturn valopers.NewAuthorityRotationProposalRequest(cross(cur), newContractPath)\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"},{"name":"proposal_test.gno","body":"package proposal\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n\t\"gno.land/r/gnops/valopers\"\n\t\"gno.land/r/gov/dao\"\n\tdaoinit \"gno.land/r/gov/dao/init/v0\" // so that the govdao initializer is executed\n)\n\n// cur is a zero-value realm used as a placeholder when forwarding to\n// uassert/urequire dispatch helpers that gained an `rlm realm` param.\n// These tests pass `func()` callbacks (no crossing inside the callback),\n// so rlm is ignored — a nil realm here is safe.\nvar cur realm\nvar g1user = testutils.TestAddress(\"g1user\")\n\nfunc init(cur realm) {\n\tdaoinit.InitWithUsers(cross(cur), g1user)\n}\n\nfunc TestValopers_ProposeNewValidator(cur realm, t *testing.T) {\n\tconst (\n\t\tregisterMinFee int64 = 20 * 1_000_000 // minimum gnot must be paid to register.\n\t\tproposalMinFee int64 = 100 * 1_000_000\n\n\t\tmoniker     string = \"moniker\"\n\t\tdescription string = \"description\"\n\t\tserverType  string = valopers.ServerTypeOnPrem\n\t\tpubKey             = \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqwpdwpd0f9fvqla089ndw5g9hcsufad77fml2vlu73fk8q8sh8v72cza5p\"\n\t)\n\n\t// signingAddr is derived from the pubkey and is what\n\t// v0.IsValidator/GetValidator return after a proposal lands\n\t// (parseEntry in r/sys/params canonicalizes from PubKey). Used\n\t// for the \"ErrSameValues\" subtest's post-execute check.\n\tsigningAddr, err := chain.PubKeyAddress(pubKey)\n\turequire.NoError(t, err, \"valid pubkey\")\n\n\t// The operator address is g1user (the GovDAO member); the\n\t// signing address is derived separately from pubKey. Splitting\n\t// them exercises the operator/signer separation.\n\topAddr := g1user\n\n\t// Set origin caller for valoper operations: must equal the\n\t// operator address (post-genesis squat guard in Register).\n\ttesting.SetRealm(testing.NewUserRealm(opAddr))\n\n\tt.Run(\"remove an unexisting validator\", func(t *testing.T) {\n\t\t// Send coins to be able to register a valoper\n\t\ttesting.SetOriginSend(chain.Coins{chain.NewCoin(\"ugnot\", registerMinFee)})\n\n\t\turequire.NotPanics(t, cur, func() {\n\t\t\tvalopers.Register(cross(cur), moniker, description, serverType, opAddr, pubKey)\n\t\t\tvalopers.UpdateKeepRunning(cross(cur), opAddr, false)\n\t\t})\n\n\t\turequire.NotPanics(t, cur, func() {\n\t\t\tvalopers.GetByAddr(opAddr)\n\t\t})\n\n\t\t// Send coins to be able to make a proposal\n\t\ttesting.SetOriginSend(chain.Coins{chain.NewCoin(\"ugnot\", proposalMinFee)})\n\n\t\turequire.AbortsWithMessage(t, cur, ErrValidatorMissing.Error(), func(cur realm) {\n\t\t\tpr := NewValidatorProposalRequest(cur, opAddr)\n\n\t\t\tdao.MustCreateProposal(cross(cur), pr)\n\t\t})\n\t})\n\n\tt.Run(\"proposal successfully created\", func(t *testing.T) {\n\t\t// Send coins to be able to register a valoper\n\t\ttesting.SetOriginSend(chain.Coins{chain.NewCoin(\"ugnot\", registerMinFee)})\n\n\t\turequire.NotPanics(t, cur, func() {\n\t\t\tvalopers.UpdateKeepRunning(cross(cur), opAddr, true)\n\t\t})\n\n\t\tvar valoper valopers.Valoper\n\n\t\turequire.NotPanics(t, cur, func() {\n\t\t\tvaloper = valopers.GetByAddr(opAddr)\n\t\t})\n\n\t\t// Send coins to be able to make a proposal\n\t\ttesting.SetOriginSend(chain.Coins{chain.NewCoin(\"ugnot\", proposalMinFee)})\n\n\t\tvar pid dao.ProposalID\n\t\turequire.NotPanics(t, cur, func(cur realm) {\n\t\t\ttesting.SetRealm(testing.NewUserRealm(g1user))\n\t\t\tpr := NewValidatorProposalRequest(cur, opAddr)\n\n\t\t\tpid = dao.MustCreateProposal(cross(cur), pr)\n\t\t})\n\n\t\tproposal, err := dao.GetProposal(pid) // index starts from 0\n\t\turequire.NoError(t, err, \"proposal not found\")\n\n\t\t// The proposal description is now operator-keyed (matches the\n\t\t// new v0.NewValidatorProposalRequest format): both the profile\n\t\t// link and the validator-updates section reference the\n\t\t// OPERATOR address with explicit power.\n\t\tdescription := ufmt.Sprintf(\n\t\t\t\"Valoper profile: [%s](/r/gnops/valopers:%s)\\n\\n%s\\n\\n## Validator Updates\\n- %s: add (power 1)\\n\",\n\t\t\tvaloper.Moniker,\n\t\t\tvaloper.OperatorAddress,\n\t\t\tvaloper.Render(),\n\t\t\tvaloper.OperatorAddress,\n\t\t)\n\n\t\t// Check that the proposal is correct\n\t\turequire.Equal(t, description, proposal.Description())\n\t})\n\n\tt.Run(\"try to update a validator with the same values\", func(t *testing.T) {\n\t\t// Send coins to be able to register a valoper\n\t\ttesting.SetOriginSend(chain.Coins{chain.NewCoin(\"ugnot\", registerMinFee)})\n\n\t\turequire.NotPanics(t, cur, func() {\n\t\t\tvalopers.GetByAddr(opAddr)\n\t\t})\n\n\t\turequire.NotPanics(t, cur, func() {\n\t\t\t// Vote the proposal created in the previous test\n\t\t\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(0)))\n\n\t\t\t// Execute the proposal — callback writes valset:proposed +\n\t\t\t// dirty=true. v0.IsValidator(signingAddr) below reads the\n\t\t\t// effective view (proposed-when-dirty) and finds the\n\t\t\t// just-added validator → enters the ErrSameValues branch.\n\t\t\tdao.ExecuteProposal(cross(cur), dao.ProposalID(0))\n\t\t})\n\n\t\t// Verify the just-added entry exists at the SIGNING address.\n\t\t_ = signingAddr\n\n\t\t// Send coins to be able to make a proposal\n\t\ttesting.SetOriginSend(chain.Coins{chain.NewCoin(\"ugnot\", proposalMinFee)})\n\n\t\turequire.AbortsWithMessage(t, cur, ErrSameValues.Error(), func() {\n\t\t\tpr := NewValidatorProposalRequest(cross(cur), opAddr)\n\t\t\tdao.MustCreateProposal(cross(cur), pr)\n\t\t})\n\t})\n}\n\nfunc TestValopers_ProposeNewInstructions(cur realm, t *testing.T) {\n\tconst proposalMinFee int64 = 100 * 1_000_000\n\n\tnewInstructions := \"new instructions\"\n\tdescription := ufmt.Sprintf(\"Update the instructions to: \\n\\n%s\", newInstructions)\n\n\t// Set origin caller\n\ttesting.SetRealm(testing.NewUserRealm(g1user))\n\n\t// Send coins to be able to make a proposal\n\ttesting.SetOriginSend(chain.Coins{chain.NewCoin(\"ugnot\", proposalMinFee)})\n\n\tvar pid dao.ProposalID\n\turequire.NotPanics(t, cur, func() {\n\t\tpr := ProposeNewInstructionsProposalRequest(cross(cur), newInstructions)\n\n\t\tpid = dao.MustCreateProposal(cross(cur), pr)\n\t})\n\n\tproposal, err := dao.GetProposal(pid) // index starts from 0\n\turequire.NoError(t, err, \"proposal not found\")\n\tif proposal == nil {\n\t\tpanic(\"PROPOSAL NOT FOUND\")\n\t}\n\n\t// Check that the proposal is correct\n\turequire.Equal(t, description, proposal.Description())\n}\n\nfunc TestValopers_ProposeNewMinFee(cur realm, t *testing.T) {\n\tconst proposalMinFee int64 = 100 * 1_000_000\n\tnewMinFee := int64(10)\n\t// ProposeNewMinFeeProposalRequest now delegates to the generic\n\t// sys/params factory; description is the standard one from\n\t// r/sys/params.newPropRequest, keyed on node:valoper:register_fee.\n\tdescription := \"This proposal wants to add a new key to sys/params: node:valoper:register_fee\"\n\n\t// Set origin caller\n\ttesting.SetRealm(testing.NewUserRealm(g1user))\n\n\t// Send coins to be able to make a proposal\n\ttesting.SetOriginSend(chain.Coins{chain.NewCoin(\"ugnot\", proposalMinFee)})\n\n\tvar pid dao.ProposalID\n\turequire.NotPanics(t, cur, func() {\n\t\tpr := ProposeNewMinFeeProposalRequest(cross(cur), newMinFee)\n\n\t\tpid = dao.MustCreateProposal(cross(cur), pr)\n\t})\n\n\tproposal, err := dao.GetProposal(pid) // index starts from 0\n\turequire.NoError(t, err, \"proposal not found\")\n\t// Check that the proposal is correct\n\turequire.Equal(t, description, proposal.Description())\n}\n\n/* TODO fix this @moul\nfunc TestValopers_ProposeNewValidator2(cur realm, t *testing.T) {\n\tconst (\n\t\tregisterMinFee int64 = 20 * 1_000_000 // minimum gnot must be paid to register.\n\t\tproposalMinFee int64 = 100 * 1_000_000\n\n\t\tmoniker     string = \"moniker\"\n\t\tdescription string = \"description\"\n\t\tpubKey             = \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqwpdwpd0f9fvqla089ndw5g9hcsufad77fml2vlu73fk8q8sh8v72cza5p\"\n\t)\n\n\t// Set origin caller\n\ttesting.SetRealm(std.NewUserRealm(g1user))\n\n\tt.Run(\"create valid proposal\", func(t *testing.T) {\n\t\t// Validator exists, should not panic\n\t\turequire.NotPanics(t, cur, func() {\n\t\t\t_ = valopers.MustGetValoper(g1user)\n\t\t})\n\n\t\t// Create the proposal\n\t\turequire.NotPanics(t, cur, func() {\n\t\t\tcross(valopers.Register)(moniker, description, g1user, pubKey)\n\t\t})\n\n\t\t// Verify proposal details\n\t\turequire.NotPanics(t, cur, func() {\n\t\t\tvaloper := valopers.MustGetValoper(g1user)\n\t\t\turequire.Equal(t, moniker, valoper.Moniker)\n\t\t\turequire.Equal(t, description, valoper.Description)\n\t\t})\n\t\t// Execute proposal with admin rights\n\t\turequire.NotPanics(t, cur, func() {\n\t\t\tstd.TestSetOrigCaller(std.Admin)\n\t\t\tcross(dao.ExecuteProposal)(dao.ProposalID(0))\n\t\t})\n\t\t// Check if valoper was updated\n\t\turequire.NotPanics(t, cur, func() {\n\t\t\tvaloper := valopers.MustGetValoper(g1user)\n\t\t\turequire.Equal(t, moniker, valoper.Moniker)\n\t\t\turequire.Equal(t, description, valoper.Description)\n\t\t})\n\n\t\t// Expect ExecuteProposal to pass\n\t\turequire.NotPanics(t, cur, func() {\n\t\t\tcross(dao.ExecuteProposal)(dao.ProposalID(0))\n\t\t})\n\t\t// Check if valoper was updated\n\t\turequire.NotPanics(t, cur, func() {\n\t\t\tvaloper := valopers.MustGetValoper(g1user)\n\t\t\turequire.Equal(t, moniker, valoper.Moniker)\n\t\t\turequire.Equal(t, description, valoper.Description)\n\t\t})\n\t\t// Execute proposal with admin rights\n\t\turequire.NotPanics(t, cur, func() {\n\t\t\tstd.TestSetOrigCaller(std.Admin)\n\t\t\tcross(dao.ExecuteProposal)(dao.ProposalID(0))\n\t\t})\n\t})\n}\n*/\n"},{"name":"z_0_a_filetest.gno","body":"// PKGPATH: gno.land/r/test/proposal\n// SEND: 20000000ugnot\n\npackage proposal\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/r/gnops/valopers\"\n\t\"gno.land/r/gnops/valopers/proposal\"\n\t\"gno.land/r/gov/dao\"\n\tdaoinit \"gno.land/r/gov/dao/init/v0\"\n)\n\nvar g1user = testutils.TestAddress(\"g1user\")\n\nconst (\n\tvalidMoniker     = \"test-1\"\n\tvalidDescription = \"test-1's description\"\n\tvalidServerType  = valopers.ServerTypeOnPrem\n\tvalidAddress     = address(\"g1sp8v98h2gadm5jggtzz9w5ksexqn68ympsd68h\")\n\totherAddress     = address(\"g1juz2yxmdsa6audkp6ep9vfv80c8p5u76e03vvh\")\n\tvalidPubKey      = \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqwpdwpd0f9fvqla089ndw5g9hcsufad77fml2vlu73fk8q8sh8v72cza5p\"\n)\n\nfunc init(cur realm) {\n\ttesting.SetOriginCaller(g1user)\n\tdaoinit.InitWithUsers(cross(cur), g1user)\n}\n\nfunc main(cur realm) {\n\t// Register a validator: OriginCaller must equal operator addr (squat guard).\n\ttesting.SetOriginCaller(validAddress)\n\tvalopers.Register(cross(cur), validMoniker, validDescription, validServerType, validAddress, validPubKey)\n\n\t// Switch back to g1user for the GovDAO proposal submission.\n\ttesting.SetOriginCaller(g1user)\n\n\t// Try to make a proposal for a non-existing validator\n\tif err := revive(func() {\n\t\tpr := proposal.NewValidatorProposalRequest(cross(cur), otherAddress)\n\t\tdao.MustCreateProposal(cross(cur), pr)\n\t}); err != nil {\n\t\tprintln(\"r: \", err)\n\t}\n}\n\n// Output:\n// r:  valoper does not exist\n"},{"name":"z_1_filetest.gno","body":"// PKGPATH: gno.land/r/test/proposal\n// SEND: 100000000ugnot\n\npackage proposal\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/r/gnops/valopers\"\n\t\"gno.land/r/gnops/valopers/proposal\"\n\t\"gno.land/r/gov/dao\"\n\tdaoinit \"gno.land/r/gov/dao/init/v0\" // so that the govdao initializer is executed\n)\n\nvar g1user = testutils.TestAddress(\"g1user\") // g1vuch2um9wf047h6lta047h6lta047h6l2ewm6w\n\nconst (\n\tvalidMoniker     = \"test-1\"\n\tvalidDescription = \"test-1's description\"\n\tvalidServerType  = valopers.ServerTypeOnPrem\n\tvalidAddress     = address(\"g1sp8v98h2gadm5jggtzz9w5ksexqn68ympsd68h\")\n\tvalidPubKey      = \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqwpdwpd0f9fvqla089ndw5g9hcsufad77fml2vlu73fk8q8sh8v72cza5p\"\n)\n\nfunc init(cur realm) {\n\ttesting.SetOriginCaller(g1user)\n\tdaoinit.InitWithUsers(cross(cur), g1user)\n\n\t// Register a validator: OriginCaller must equal operator addr (squat guard).\n\ttesting.SetOriginCaller(validAddress)\n\tvalopers.Register(cross(cur), validMoniker, validDescription, validServerType, validAddress, validPubKey)\n\n\t// Switch back to g1user for the GovDAO submission.\n\ttesting.SetOriginCaller(g1user)\n\n\tif err := revive(func() {\n\t\tpr := proposal.NewValidatorProposalRequest(cross(cur), validAddress)\n\t\tdao.MustCreateProposal(cross(cur), pr)\n\t}); err != nil {\n\t\tprintln(\"r: \", err)\n\t} else {\n\t\tprintln(\"OK\")\n\t}\n}\n\nfunc main(cur realm) {\n\tprintln(dao.Render(cross(cur), \"\"))\n}\n\n// Output:\n// OK\n// # GovDAO\n// ## Members\n// [\u003e Go to Memberstore \u003c](/r/gov/dao/memberstore/v0)\n// ## Proposals\n// ### [Prop #0 - Add valoper test\\-1 to the valset](/r/gov/dao:0)\n// Author: g1vuch2um9wf047h6lta047h6lta047h6l2ewm6w\n//\n// Status: ACTIVE\n//\n// Tiers eligible to vote: T1, T2, T3\n//\n// ---\n"},{"name":"z_governed_instructions_filetest.gno","body":"// PKGPATH: gno.land/r/gnops/valopers/proposal/filetests/z_governed_instructions_filetest\n//\n// Positive counterpart to\n// r/gnops/valopers/filetests/z_foreign_realm_capability_filetest.gno.\n//\n// Sealing the privileged closure inside a dao.ProposalRequest closes the\n// capability leak, but it would be worthless if it\n// also broke the legitimate path. This runs the real thing end to end —\n// propose, vote, execute — and asserts the instructions are actually\n// rewritten.\n//\n// Read the two files together: same write, but reachable ONLY with a\n// GovDAO majority behind it.\npackage z_governed_instructions_filetest\n\nimport (\n\t\"chain/runtime/unsafe\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/r/gnops/valopers\"\n\t\"gno.land/r/gnops/valopers/proposal\"\n\t\"gno.land/r/gov/dao\"\n\tdaov3init \"gno.land/r/gov/dao/init/v0\"\n)\n\nvar member address = unsafe.OriginCaller()\n\nfunc init(cur realm) {\n\tdaov3init.InitWithUsers(cross(cur), member)\n\n\ttesting.SetOriginCaller(member)\n\ttesting.SetRealm(testing.NewUserRealm(member))\n\n\tpr := proposal.ProposeNewInstructionsProposalRequest(cross(cur), \"GOVERNED-INSTRUCTIONS\")\n\tdao.MustCreateProposal(cross(cur), pr)\n}\n\nfunc main(cur realm) {\n\ttesting.SetOriginCaller(member)\n\n\tprintln(\"before:\", strings.Contains(valopers.Render(\"\"), \"GOVERNED-INSTRUCTIONS\"))\n\n\t// Voter-facing disclosure (the pr6068 anti-phishing line, rendered as\n\t// \"Executor created in: ...\"). It moved from r/gnops/valopers/proposal\n\t// to r/gnops/valopers when request construction moved into the realm,\n\t// so pin it rather than let it drift unobserved.\n\tprop, err := dao.GetProposal(dao.ProposalID(0))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tprintln(\"title:\", prop.Title())\n\tprintln(\"executor created in:\", prop.ExecutorCreationRealm())\n\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(0)))\n\tprintln(\"executed:\", dao.ExecuteProposal(cross(cur), dao.ProposalID(0)))\n\n\tprintln(\"after:\", strings.Contains(valopers.Render(\"\"), \"GOVERNED-INSTRUCTIONS\"))\n}\n\n// Output:\n// before: false\n// title: /r/gnops/valopers: Update instructions\n// executor created in: gno.land/r/gnops/valopers\n// executed: true\n// after: true\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"veK83TMFL9zP1V8rsYdzlPbpwPZcr8wEJiROnjmhXFQZvfWWVuqAY5Xb6OGXxKE4QyerHZplc+TC+hw41Td1/g=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"treasury","path":"gno.land/r/gov/dao/treasury/v1","files":[{"name":"doc.gno","body":"// Package treasury (v1) is the GovDAO treasury built on\n// gno.land/p/nt/treasury/v1: its GRC20 banker pays gno.land/p/nt/grc20/v1\n// tokens listed in gno.land/r/nt/grc20reg/v1, and every amount is a uint256.\n// Coin payments are range-checked down to int64 when they are built.\n//\n// It is a separate realm from r/gov/dao/treasury/v0, with its own address:\n// funds held by v0 stay there until a v0 payment proposal moves them here.\n// Only realms in r/gov/dao's AllowedDAOs may send, which after the impl/v1\n// install are impl/v0 and impl/v1.\npackage treasury\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/gov/dao/treasury/v1\"\ngno = \"0.9\"\n"},{"name":"treasury.gno","body":"package treasury\n\nimport (\n\t\"chain/banker\"\n\n\t\"gno.land/p/nt/grc20/v1\"\n\tt \"gno.land/p/nt/treasury/v1\"\n\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/nt/grc20reg/v1\"\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/treasury/v1:.../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\t// Crossing function, so cur is a live minted cur and this is redundant\n\t// today; kept to match the documented rule that caller identity is\n\t// derived from cur.Previous() only under an IsCurrent() guard.\n\tif !cur.IsCurrent() {\n\t\tpanic(\"realm value is not the caller's live cur\")\n\t}\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\t// Copied, not aliased: the grc20Lister closure re-reads tokenKeys on every\n\t// Balances() and every GRC20 payment, so storing the caller's array as\n\t// given would leave it a permanent write handle on this realm's token set.\n\ttokenKeys = make([]string, len(keys))\n\tcopy(tokenKeys, keys)\n}\n\n// Send sends a payment using the treasury instance.\nfunc Send(cur realm, payment t.Payment) {\n\t// See SetTokenKeys: redundant under the crossing-function guarantee, kept\n\t// for conformance with the IsCurrent()-before-Previous() rule.\n\tif !cur.IsCurrent() {\n\t\tpanic(\"realm value is not the caller's live cur\")\n\t}\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"WPFEh0iKy+bkGIGi3LAOuysdZDwfHbgEKVQGFc77hRBoUrKl11YE9lkhHCvvZ81kim5kekaiCh1n4MRhPl/qSA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1g73v2anukg4ej7axwqpthsatzrxjsh0wk797da","package":{"name":"impl","path":"gno.land/r/gov/dao/impl/v1","files":[{"name":"clamp.gno","body":"package impl\n\n// Attacker-controlled strings are clamped before they reach the sanitizer, so\n// escaping costs a bounded constant rather than scaling with input.\n//\n// Sanitizing runs ~11,310 gas/byte (InlineCode) against ~31 for the raw\n// concatenation it replaced, and Render is reachable unauthenticated through\n// vm/qrender under maxGasQuery = 3_000_000_000. ExecutorCreationRealm is\n// dispatched through the public dao.Executor interface, so a hostile executor\n// computes it per call while storing almost nothing. Measured on the real\n// render path: a 250KB value costs 2,839,117,770 gas unclamped against\n// 18,749,984 clamped. 250KB is just under the cap; about 265KB crosses it\n// (3,008,818,394), and past that the page cannot be rendered at all by\n// anyone. Choosing the larger number costs the attacker nothing, since the\n// value is computed per call and almost nothing is stored. Removing that\n// amplification is the point of this file.\n//\n// It does NOT make the page safe, and nothing here should be read as claiming\n// so. The executor's method body runs inside the same query and is unbounded:\n// an executor that simply burns CPU before returning a short string still\n// renders the proposal page permanently un-queryable, for a few hundred bytes\n// of on-chain storage. That predates this change — Render has always called\n// ExecutorString() and ExecutorCreationRealm() through the public interface —\n// and bounding it needs a gas budget around executor dispatch, not a clamp.\nconst (\n\t// A realm path. The longest deployable realm path in examples/ is 35 bytes\n\t// (gno.land/r/gov/dao/treasury/test/v1), so this leaves about seven times\n\t// the room anything real needs. A hostile executor returns any length it\n\t// likes, which is the reason to clamp at all. It is headroom, not a\n\t// guarantee: the package-path grammar puts no ceiling on how many segments\n\t// a path may have, so a deeply nested realm could still be cut. That costs\n\t// a truncation marker on the page and nothing else.\n\tmaxRenderedRealm = 256\n\t// \"execution failed: \" plus an executor's error message. Also bounds what\n\t// govdao.gno stores, so the realm never holds a reason it cannot show.\n\tmaxRenderedReason = 1024\n\t// A strconv error that echoes the caller's own path segment. Everything in\n\t// it is fixed text except the quoted segment, whose length the caller picks.\n\tmaxRenderedError = 256\n\t// A proposal title. The longest in examples/ is about 40 bytes, so this is\n\t// roughly nine times anything real, and long for a heading. Titles are\n\t// escaped on both the proposal page and the list page, and the list page\n\t// escapes one per proposal shown, so this is the bound that keeps a single\n\t// oversized title from pricing the whole list out of the query cap.\n\tmaxRenderedTitle = 512\n\t// A Payment's String(). The canonical impls render a coin set or an\n\t// amount plus a token key, then \" to \" and a bech32 address; a coin set\n\t// is bounded by the number of denoms a proposer cares to list and each\n\t// denom's length is unbounded, which is the reason to clamp at all.\n\tmaxRenderedPayment = 512\n)\n\n// Always clamp first — before escaping, and before any other pass over the\n// value. Trimming used to run before the clamp, which meant it walked every\n// byte the executor returned: 250KB of spaces cost 1,368,719,824 gas to render\n// nothing, and 560KB cost 3,048,904,520, past the query cap. Clamping first\n// bounds that scan and brought the same 560KB down to 16,657,792.\n//\n// Never clamp after escaping either. The escapers size their wrapper\n// from the string they are handed — InlineCode picks a fence long enough to\n// outscan the backticks it can see. Cutting a value that has already been\n// escaped can slice the closing fence off and leave the span hanging open,\n// which is worse than not clamping at all. Both call sites read\n// InlineCode(clampField(...)) for that reason, and the enormous-value case in\n// filetests/executor_disclosure_filetest.gno fails if the two are swapped.\n//\n// clampField cuts s to at most max bytes, backing off to a rune boundary so a\n// well-formed multi-byte character is not split, and marks the result so a\n// reader can tell it was cut. Input that is already invalid UTF-8 can still\n// leave a dangling lead byte; the sanitizer tolerates that. The marker avoids markdown punctuation:\n// these values are escaped downstream, and parentheses would come back as\n// \"\\(truncated\\)\".\nfunc clampField(s string, max int) string {\n\tif len(s) \u003c= max {\n\t\treturn s\n\t}\n\n\tend := max\n\t// UTF-8 continuation bytes are 0b10xxxxxx.\n\tfor end \u003e 0 \u0026\u0026 s[end]\u00260xC0 == 0x80 {\n\t\tend--\n\t}\n\n\treturn s[:end] + \"… truncated\"\n}\n"},{"name":"clamp_test.gno","body":"package impl\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\n// Sanitizing costs ~11,310 gas/byte, and rendering is reachable unauthenticated\n// through vm/qrender under a 3,000,000,000 gas cap. ExecutorCreationRealm is\n// dispatched through the public dao.Executor interface, so a hostile executor\n// computes it per call and stores almost nothing: unclamped, a ~250KB value\n// costs over 3G gas in one render and bricks the proposal page for everyone.\n// Measured on the real render path: 2,839,117,770 gas unclamped versus\n// 18,749,984 clamped. About 265KB of input crosses the query cap entirely.\nfunc TestClampFieldBoundsSanitizerInput(t *testing.T) {\n\tuassert.Equal(t, \"short\", clampField(\"short\", maxRenderedRealm),\n\t\t\"a value within the bound must pass through untouched\")\n\n\thuge := strings.Repeat(\"a\", 250000)\n\tgot := clampField(huge, maxRenderedRealm)\n\tuassert.True(t, len(got) \u003c maxRenderedRealm+32,\n\t\t\"the clamped value must be bounded by the limit, not by the input\")\n\tuassert.True(t, strings.HasSuffix(got, \"… truncated\"),\n\t\t\"a clamped value must say it was cut rather than look authored short\")\n}\n\n// Cutting at a byte offset can land inside a multi-byte rune; the sanitizer\n// tolerates invalid UTF-8, but handing it a split rune is sloppy and would\n// render a replacement character mid-path.\nfunc TestClampFieldCutsOnRuneBoundary(t *testing.T) {\n\t// 3-byte runes, so a 256-byte cut lands mid-rune (256 = 85*3 + 1).\n\tgot := clampField(strings.Repeat(\"世\", 200), maxRenderedRealm)\n\tbody := strings.TrimSuffix(got, \"… truncated\")\n\n\tuassert.True(t, len(body)%3 == 0,\n\t\t\"the cut must land on a rune boundary\")\n\tuassert.Equal(t, strings.Repeat(\"世\", len(body)/3), body,\n\t\t\"every retained rune must be intact\")\n}\n\n// Boundary and malformed input. clampField does byte arithmetic and backs off\n// over UTF-8 continuation bytes, so the interesting cases are the ones where\n// that loop has nowhere to back off to.\nfunc TestClampFieldBoundaries(t *testing.T) {\n\texact := strings.Repeat(\"a\", maxRenderedRealm)\n\tuassert.Equal(t, exact, clampField(exact, maxRenderedRealm),\n\t\t\"a value exactly at the bound must not be marked truncated\")\n\n\tover := strings.Repeat(\"a\", maxRenderedRealm+1)\n\tuassert.True(t, strings.HasSuffix(clampField(over, maxRenderedRealm), \"… truncated\"),\n\t\t\"one byte over the bound must be cut\")\n\n\t// Already-invalid UTF-8: every byte is a continuation, so the backoff\n\t// walks to zero. Must degrade to the marker rather than panic or loop.\n\tcont := strings.Repeat(\"\\x80\", maxRenderedRealm+10)\n\tuassert.Equal(t, \"… truncated\", clampField(cont, maxRenderedRealm),\n\t\t\"an all-continuation string must degrade to just the marker\")\n\n\tuassert.Equal(t, \"… truncated\", clampField(\"abc\", 0),\n\t\t\"a zero bound must still terminate\")\n}\n"},{"name":"denied_reason_test.gno","body":"package impl\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\n)\n\n// DeniedReason is \"execution failed: \" + err.Error() (govdao.gno), and that\n// error comes from the proposal's executor callback — which any third-party\n// realm can supply (see govdao_execute_reject_proposal.txtar, where the\n// executor lives in gno.land/r/test/request). Rendered raw it let a failing\n// proposal write markdown straight beneath the \"PROPOSAL HAS BEEN DENIED\"\n// line, including a forged heading and vote tally.\n//\n// Operates on proposalStatus.String directly rather than through a real\n// proposal, so it does not disturb the proposal ids and counts that the other\n// tests in this package assert against shared realm state.\nfunc TestDeniedReasonCannotForgePageStructure(cur realm, t *testing.T) {\n\tps := newProposalStatus([]string{memberstore.T1})\n\tps.Denied = true\n\tps.DeniedReason = \"execution failed: boom\\n\\n### Stats\\n\\n- **PROPOSAL HAS BEEN ACCEPTED**\\n\\n---\\n\u003cdiv\u003eswallow the tally\"\n\n\tout := ps.String(0, cur)\n\n\tuassert.False(t, strings.Contains(out, \"\\n### Stats\"),\n\t\t\"an injected heading must not survive into the rendered stats block\")\n\tuassert.False(t, strings.Contains(out, \"\\n- **PROPOSAL HAS BEEN ACCEPTED**\"),\n\t\t\"an injected list item must not forge an acceptance line\")\n\tuassert.False(t, strings.Contains(out, \"\\n---\\n\"),\n\t\t\"an injected horizontal rule must not survive\")\n\n\t// Emphasis and raw HTML are residue that sanitize.Block would have left\n\t// live; InlineText escapes both. A reader must not see a bold\n\t// \"ACCEPTED\" on a denied proposal, and a \u003cdiv\u003e must not open an HTML\n\t// block that swallows the vote tally rendered after it.\n\tuassert.False(t, strings.Contains(out, \"**PROPOSAL HAS BEEN ACCEPTED**\"),\n\t\t\"injected bold text must not forge an acceptance line\")\n\tuassert.False(t, strings.Contains(out, \"\u003cdiv\u003e\"),\n\t\t\"injected raw HTML must not open a block that swallows the tally\")\n\n\t// Folding is the property InlineText adds over the alternatives: the whole\n\t// reason must land on the REASON: line, so nothing after a newline can be\n\t// read as new top-level markdown. Asserted by requiring the payload's LAST\n\t// segment on the same line as its first.\n\treason := out[strings.Index(out, \"REASON: \"):]\n\tfirstLine := reason[:strings.Index(reason, \"\\n\")]\n\tuassert.True(t, strings.Contains(firstLine, \"swallow the tally\"),\n\t\t\"the entire reason must be folded onto the REASON line\")\n}\n\n// ...and ordinary error text stays readable. InlineText backslash-escapes\n// markdown punctuation, so the raw output carries \"Boom\\!\" — but a\n// backslash-escaped punctuation mark renders as the bare character, so a\n// reader sees \"Boom!\". Words and spacing are untouched.\nfunc TestDeniedReasonKeepsPlainTextIntact(cur realm, t *testing.T) {\n\tps := newProposalStatus([]string{memberstore.T1})\n\tps.Denied = true\n\tps.DeniedReason = \"execution failed: Boom!\"\n\n\tuassert.True(t, strings.Contains(ps.String(0, cur), `REASON: execution failed: Boom\\!`),\n\t\t\"a plain denial reason must stay on the REASON line, escaped but readable\")\n}\n\n// The reason reaches an unauthenticated render path and InlineText costs\n// ~6,990 gas/byte, so it must be bounded before it is escaped, not after.\n// Asserts the clamp at the call site, not clampField in isolation.\n//\n// The payload is exclamation marks, not letters, and that choice is the whole\n// test. InlineText escapes \"!\" to \"\\!\" but leaves letters alone, so with a\n// letter payload the escaped text is the same length as the raw text and both\n// orderings produce identical output — the test would pass either way and\n// prove nothing. With punctuation, escaping doubles the length, and the two\n// orderings become tellable apart.\nfunc TestDeniedReasonIsClampedBeforeSanitizing(cur realm, t *testing.T) {\n\tps := newProposalStatus([]string{memberstore.T1})\n\tps.Denied = true\n\tps.DeniedReason = strings.Repeat(\"!\", 50000)\n\n\tout := ps.String(0, cur)\n\n\t// The clamp bounds what goes INTO the escaper, so a full maxRenderedReason\n\t// characters survive and the escaped output is about twice that. Clamping\n\t// the escaped text instead would leave only half as many, and the escaper\n\t// would still have processed all 50,000 characters — exactly the cost the\n\t// clamp exists to avoid.\n\tuassert.True(t, strings.Count(out, \"!\") \u003e= maxRenderedReason,\n\t\t\"the clamp must bound the escaper's input, not its output\")\n\tuassert.True(t, len(out) \u003c 4*maxRenderedReason,\n\t\t\"the rendered stats block must still be bounded by the clamp\")\n\tuassert.True(t, strings.Contains(out, \"… truncated\"),\n\t\t\"a clamped reason must be marked as cut\")\n}\n"},{"name":"doc.gno","body":"// Package impl (v1) is the GovDAO implementation whose treasury proposals use\n// gno.land/r/gov/dao/treasury/v1 (uint256 amounts, grc20 v1 tokens). It is\n// otherwise impl/v0, with the same member store (memberstore/v0) and proposal\n// builders. Its law cannot be read from impl/v0 (v0 exposes no accessor), so\n// the install proposal carries the law impl/v1 will use, voted on under v0's\n// law and in force from impl/v1's first decision (see NewInstallRequest).\n//\n// Proposal filters fail closed: impl/v0's FilterByTier is enforced with the\n// same rules as impl/v1's (v0 builders stay usable after the install), and\n// any other filter is refused at proposal creation.\n//\n// Governance installs it by voting on NewInstallRequest(cur, reason, law), which asks\n// dao.UpdateImpl for AllowedDAOs = [impl/v0, impl/v1]. Proposal statuses live\n// on the implementation instance, so proposals still open under impl/v0 when\n// the install executes cannot be voted on or executed afterwards.\npackage impl\n"},{"name":"executor_disclosure_filetest.gno","body":"// PKGPATH: gno.land/r/test/disclosure\npackage disclosure\n\n// Covers the executor-disclosure changes in an isolated realm. The unit tests\n// in the impl package share proposal ids across files (govdao_test.gno asserts\n// a hard-coded id) and swap the DAO implementation partway through, so these\n// live here instead.\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/impl/v1\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\n)\n\nconst user address = \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"\n\nfunc init(cur realm) {\n\tmemberstore.Get(0, cur).DeleteAll()\n\tmemberstore.Get(0, cur).SetTier(memberstore.T1)\n\tmemberstore.Get(0, cur).SetMember(memberstore.T1, user, memberstore.NewMember(3))\n\tdao.UpdateImpl(cross(cur), dao.NewUpdateRequest(impl.NewGovDAO(), nil))\n}\n\n// hostileExecutor implements dao.Executor directly rather than through\n// NewSimpleExecutor, which is what lets it choose its own CreationRealm.\n// CreationRealm() is dispatched through the public dao.Executor interface, so\n// only SimpleExecutor's value is VM-supplied from rlm.PkgPath().\ntype hostileExecutor struct{}\n\nfunc (e *hostileExecutor) Execute(cur realm) error { return nil }\n\nfunc (e *hostileExecutor) String() string { return \"\" }\n\nfunc (e *hostileExecutor) CreationRealm() string {\n\treturn \"gno.land/r/sys/params\\n\\n### Stats\\n\\n- **PROPOSAL HAS BEEN ACCEPTED**\\n- YES PERCENT: 100%\\n\\n---\\n\"\n}\n\n// blankExecutor's CreationRealm is non-empty but strips to nothing:\n// sanitize.InlineCode removes bidi and zero-width characters, so it returns \"\".\ntype blankExecutor struct{}\n\nfunc (e *blankExecutor) Execute(cur realm) error { return nil }\n\nfunc (e *blankExecutor) String() string { return \"\" }\n\nfunc (e *blankExecutor) CreationRealm() string { return \"\\u200b\\u200b\\u202e\" }\n\n// whitespaceExecutor covers the other half of the guard: plain whitespace,\n// which InlineCode would otherwise wrap in a padded, empty-looking span.\ntype whitespaceExecutor struct{}\n\nfunc (e *whitespaceExecutor) Execute(cur realm) error { return nil }\n\nfunc (e *whitespaceExecutor) String() string { return \"\" }\n\nfunc (e *whitespaceExecutor) CreationRealm() string { return \"   \\t \" }\n\n// hugeExecutor computes a very large CreationRealm while storing nothing.\n// Sanitizing costs ~11,310 gas/byte and render is reachable unauthenticated\n// under a 3,000,000,000 gas cap, so this must be clamped before escaping.\ntype hugeExecutor struct{}\n\nfunc (e *hugeExecutor) Execute(cur realm) error { return nil }\n\nfunc (e *hugeExecutor) String() string { return \"\" }\n\nfunc (e *hugeExecutor) CreationRealm() string { return strings.Repeat(\"z\", 20000) }\n\n// fenceExecutor attacks the code span itself. Section 4 below sends backticks\n// through the grant sentence, but that path escapes the value directly. The\n// creation realm is clamped first and escaped second, so it needs its own\n// case: the fence is chosen after the cut, and must still outscan whatever\n// backticks survived it. The run here is two long, so a two-backtick fence\n// would be closed by the payload.\ntype fenceExecutor struct{}\n\nfunc (e *fenceExecutor) Execute(cur realm) error { return nil }\n\nfunc (e *fenceExecutor) String() string { return \"\" }\n\nfunc (e *fenceExecutor) CreationRealm() string { return \"gno.land/r/evil`` **INJECTED**\" }\n\n// wsExecutor returns a large run of spaces. TrimSpace used to run before the\n// clamp, so it scanned every byte of whatever the executor returned: 250KB of\n// spaces cost 1,368,719,824 gas to render nothing at all, and 560KB cost\n// 3,048,904,520 — past the query cap, so the page could not be rendered by\n// anyone. Clamping first bounds the scan; the same 560KB now costs 16,657,792.\ntype wsExecutor struct{}\n\nfunc (e *wsExecutor) Execute(cur realm) error { return nil }\n\nfunc (e *wsExecutor) String() string { return \"\" }\n\nfunc (e *wsExecutor) CreationRealm() string { return strings.Repeat(\" \", 20000) }\n\nfunc main(cur realm) {\n\ttesting.SetOriginCaller(user)\n\n\t// 1. An executor with NO description. The creation realm used to share the\n\t// `ExecutorString() != \"\"` gate with the description, so it was hidden for\n\t// every such proposal — 16 call sites across 7 production realms.\n\t// SetRealm first: NewSimpleExecutor captures rlm.PkgPath(), which is empty\n\t// outside a code realm.\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/template/silent\"))\n\tsilent := dao.NewSimpleExecutor(0, cur, func(realm) error { return nil }, \"\")\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tpid := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(\n\t\t\"Silent\", \"A proposal whose executor has no description\", silent))\n\tout := dao.Render(cross(cur), pid.String())\n\n\tprintln(\"empty-description proposal discloses creation realm:\",\n\t\tstrings.Contains(out, \"Executor created in: `gno.land/r/template/silent`\"))\n\tprintln(\"and prints no empty metadata block:\",\n\t\t!strings.Contains(out, \"This proposal contains the following metadata\"))\n\n\t// 2. CreationRealm() is dispatched through the public dao.Executor\n\t// interface, so a third-party executor picks its own value — and the\n\t// disclosure now renders for every proposal. InlineCode does not delete the\n\t// hostile text; it folds it onto one line inside a code span, where it can\n\t// no longer forge page structure. So assert structure, not absence.\n\thpid := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(\n\t\t\"Hostile\", \"A proposal whose executor forges page structure\", \u0026hostileExecutor{}))\n\thout := dao.Render(cross(cur), hpid.String())\n\n\tprintln(\"only the genuine Stats heading exists:\",\n\t\tstrings.Count(hout, \"\\n### Stats\") == 1)\n\tprintln(\"no forged acceptance line:\",\n\t\t!strings.Contains(hout, \"\\n- **PROPOSAL HAS BEEN ACCEPTED**\"))\n\tprintln(\"no forged tally line:\",\n\t\t!strings.Contains(hout, \"\\n- YES PERCENT: 100%\"))\n\tprintln(\"genuine tally and status still render:\",\n\t\tstrings.Contains(hout, \"\\n- YES PERCENT: 0%\") \u0026\u0026\n\t\t\tstrings.Contains(hout, \"- **Proposal is open for votes**\"))\n\t// One line, inside a code span: the newlines are folded to spaces, so none\n\t// of the payload can start a block. (No pad space before the value now —\n\t// TrimSpace removes the payload's trailing newline, so InlineCode does not\n\t// need to pad the fence.)\n\tprintln(\"hostile value is confined to one code-span line:\",\n\t\tstrings.Contains(hout, \"Executor created in: `gno.land/r/sys/params  ### Stats\"))\n\n\t// 3. The upgrade proposal rewrites AllowedDAOs — the sole authorization for\n\t// replacing the implementation, mutating the member store and moving\n\t// treasury funds. It used to carry an empty description, so the realm\n\t// receiving that authority appeared nowhere a voter would read.\n\tupid := dao.MustCreateProposal(cross(cur),\n\t\timpl.NewUpgradeDaoImplRequest(cross(cur), impl.NewGovDAO(), \"gno.land/r/gov/dao/v1/impl\", \"reason\"))\n\tuout := dao.Render(cross(cur), upid.String())\n\n\tprintln(\"upgrade proposal states the grant:\",\n\t\tstrings.Contains(uout, \"may replace the implementation, mutate the member store, or move treasury funds\"))\n\tprintln(\"upgrade proposal names the granted realm:\",\n\t\tstrings.Contains(uout, \"`gno.land/r/gov/dao/v1/impl`\"))\n\n\t// 4. realmPkg is caller-supplied and lands inside a code span. md.EscapeText\n\t// was wrong there: CommonMark 6.1 does not process backslash escapes inside\n\t// code spans, so it rendered visible backslashes, and a backtick closed the\n\t// span early. InlineCode widens the fence instead, so the payload stays\n\t// inside it as literal text.\n\tbpid := dao.MustCreateProposal(cross(cur),\n\t\timpl.NewUpgradeDaoImplRequest(cross(cur), impl.NewGovDAO(),\n\t\t\t\"gno.land/r/x` **PROPOSAL HAS BEEN ACCEPTED** `\", \"reason\"))\n\tbout := dao.Render(cross(cur), bpid.String())\n\n\t// The payload's own backtick sits INSIDE a widened `` fence, so it renders\n\t// as literal code rather than closing the span and freeing the bold text.\n\tprintln(\"fence widened to contain the backtick:\",\n\t\tstrings.Contains(bout, \"`` gno.land/r/x` **PROPOSAL HAS BEEN ACCEPTED** ` ``\"))\n\tprintln(\"no forged acceptance line from the breakout attempt:\",\n\t\t!strings.Contains(bout, \"\\n- **PROPOSAL HAS BEEN ACCEPTED**\"))\n\tprintln(\"no visible backslashes in the path:\",\n\t\t!strings.Contains(bout, \"gno\\\\.land\"))\n\n\t// 4b. Guarding the raw value would print the label with nothing after it\n\t// for a creation realm that sanitizes away entirely.\n\tzpid := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(\n\t\t\"Blank\", \"A proposal whose creation realm strips to nothing\", \u0026blankExecutor{}))\n\tzout := dao.Render(cross(cur), zpid.String())\n\n\tprintln(\"a creation realm that strips to nothing prints no bare label:\",\n\t\t!strings.Contains(zout, \"Executor created in:\"))\n\n\twpid := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(\n\t\t\"Whitespace\", \"A proposal whose creation realm is only whitespace\", \u0026whitespaceExecutor{}))\n\tprintln(\"a whitespace-only creation realm prints no bare label:\",\n\t\t!strings.Contains(dao.Render(cross(cur), wpid.String()), \"Executor created in:\"))\n\n\thupid := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(\n\t\t\"Huge\", \"A proposal whose creation realm is enormous\", \u0026hugeExecutor{}))\n\thurendered := dao.Render(cross(cur), hupid.String())\n\t// The truncation marker must sit INSIDE the code span, so the value ends\n\t// with its closing fence. That is what proves the clamp ran before the\n\t// escaping and not after. Cutting an already-escaped value slices the\n\t// closing fence off and leaves the span hanging open, and a length check\n\t// alone cannot tell the two apart — both produce a short string ending in\n\t// the marker.\n\tprintln(\"an enormous creation realm is clamped before escaping:\",\n\t\tlen(hurendered) \u003c 2000 \u0026\u0026 strings.Contains(hurendered, \"… truncated`\"))\n\n\t// 4c. The same breakout attempt through the creation realm, which is\n\t// clamped before it is escaped. The fence must be sized from the clamped\n\t// string, so it widens to three backticks and the payload stays literal.\n\tfpid := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(\n\t\t\"Fence\", \"A proposal whose creation realm tries to close the code span\", \u0026fenceExecutor{}))\n\tfout := dao.Render(cross(cur), fpid.String())\n\n\tprintln(\"fence outscans the payload's backtick run:\",\n\t\tstrings.Contains(fout, \"Executor created in: ```gno.land/r/evil`` **INJECTED**```\"))\n\tprintln(\"no bold text escapes the code span:\",\n\t\t!strings.Contains(fout, \"\\n**INJECTED**\"))\n\n\t// 4d. A large all-whitespace creation realm. Clamping before trimming is\n\t// what bounds the work here, and the marker is how the test can tell: cut\n\t// first and the marker survives the trim, so the label renders. Trim first\n\t// and the value collapses to nothing, printing no label — and the trim has\n\t// already walked every byte the executor produced.\n\twpid2 := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(\n\t\t\"BigWhitespace\", \"A proposal whose creation realm is a huge run of spaces\", \u0026wsExecutor{}))\n\twout2 := dao.Render(cross(cur), wpid2.String())\n\n\tprintln(\"a huge whitespace creation realm is clamped before trimming:\",\n\t\tstrings.Contains(wout2, \"Executor created in: `… truncated`\"))\n\n\t// 5. Proposals live on the proxy but their voting status lives on the\n\t// GovDAO instance, so replacing the implementation leaves earlier\n\t// proposals renderable but statusless. renderProposalPage took a\n\t// user-supplied pid and dereferenced that nil status.\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao/impl/v1\"))\n\tdao.UpdateImpl(cross(cur), dao.NewUpdateRequest(impl.NewGovDAO(), nil))\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tprintln(\"a proposal orphaned by an upgrade renders instead of panicking:\",\n\t\tstrings.Contains(dao.Render(cross(cur), pid.String()), \"not available\"))\n}\n\n// Output:\n// empty-description proposal discloses creation realm: true\n// and prints no empty metadata block: true\n// only the genuine Stats heading exists: true\n// no forged acceptance line: true\n// no forged tally line: true\n// genuine tally and status still render: true\n// hostile value is confined to one code-span line: true\n// upgrade proposal states the grant: true\n// upgrade proposal names the granted realm: true\n// fence widened to contain the backtick: true\n// no forged acceptance line from the breakout attempt: true\n// no visible backslashes in the path: true\n// a creation realm that strips to nothing prints no bare label: true\n// a whitespace-only creation realm prints no bare label: true\n// an enormous creation realm is clamped before escaping: true\n// fence outscans the payload's backtick run: true\n// no bold text escapes the code span: true\n// a huge whitespace creation realm is clamped before trimming: true\n// a proposal orphaned by an upgrade renders instead of panicking: true\n"},{"name":"executor_invocation_authority_filetest.gno","body":"// PKGPATH: gno.land/r/test/execauthority\npackage execauthority\n\n// Regression pin for who may invoke a proposal executor.\n//\n// dao.Executor.Execute is a public method on a public type and\n// dao.NewSimpleExecutor is a public constructor, so any realm can build an\n// executor around any func(realm) error it can obtain. Execute is a CROSSING\n// method declared in r/gov/dao, so invoking it mints a gno.land/r/gov/dao\n// frame for the callback — and that value is a capability: realms gate on it\n// (authz.NewContractAuthority(\"gno.land/r/gov/dao\") + DoByPrevious, ownable,\n// anything reading Previous().Address()).\n//\n// Until SimpleExecutor.Execute gated on its invoker, the forged frame was\n// byte-identical to an approved one: same PkgPath, same Previous().PkgPath(),\n// same Previous().Address(). A callback could not tell a supermajority-backed\n// execution from one no member ever voted on, and the forgery minted no\n// proposal id, so there was nothing to render, audit or deny afterwards.\n//\n// The documented containment was never the one holding the line. types.gno\n// said invocation was \"contained by the executor being unexported inside\n// ProposalRequest/Proposal with no accessor\" — that protects the executor\n// OBJECT, but an attacker never needs someone else's executor: a reachable\n// CALLBACK is enough, and an ordinary exported entrypoint of the victim whose\n// signature is assignable to func(realm) error is such a callback.\n// r/gnops/valopers had exactly that shape.\n//\n// Sections 2, 3 and 6 are the inversion: the forgery is now refused, the two\n// frames are no longer comparable, and the proxy identity is not mintable.\n// Section 5 keeps SafeExecutor characterized — it remains dead code that\n// cannot be wired as written, since its allowlist holds the impl path while\n// an executor's caller is the proxy.\n\nimport (\n\t\"chain\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/impl/v1\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\n)\n\nconst (\n\tmember  address = \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"\n\toutside address = \"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\"\n)\n\n// observed records the frame the callback was handed, so the approved and the\n// forged invocation can be compared.\ntype frame struct {\n\tcur      string\n\tprev     string\n\tprevAddr address\n\tran      bool\n}\n\nvar seen frame\n\nfunc probe(cur realm) error {\n\tseen = frame{\n\t\tcur:      cur.PkgPath(),\n\t\tprev:     cur.Previous().PkgPath(),\n\t\tprevAddr: cur.Previous().Address(),\n\t\tran:      true,\n\t}\n\treturn nil\n}\n\nfunc init(cur realm) {\n\tmemberstore.Get(0, cur).DeleteAll()\n\tmemberstore.Get(0, cur).SetTier(memberstore.T1)\n\tmemberstore.Get(0, cur).SetMember(memberstore.T1, member, memberstore.NewMember(3))\n\tdao.UpdateImpl(cross(cur), dao.NewUpdateRequest(\n\t\timpl.NewGovDAO(), []string{\"gno.land/r/gov/dao/impl/v1\"}))\n}\n\nfunc main(cur realm) {\n\t// 1. The approved path still works: a member proposes, votes it to\n\t// supermajority, and executes. The gate must not break governance.\n\ttesting.SetOriginCaller(member)\n\ttesting.SetRealm(testing.NewUserRealm(member))\n\n\tpid := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(\n\t\t\"approved\", \"carried by a supermajority vote\", dao.NewSimpleExecutor(0, cur, probe, \"\")))\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, pid))\n\tprintln(\"approved path executes:\", dao.ExecuteProposal(cross(cur), pid))\n\tapproved := seen\n\tprintln(\"callback ran:\", approved.ran)\n\tprintln(\"approved frame cur :\", approved.cur)\n\tprintln(\"approved frame prev:\", approved.prev)\n\n\t// 2. The forged path: no proposal, no vote, and a caller who is not a\n\t// member of any tier. Execute is called directly on a locally built\n\t// executor, and is refused.\n\tseen = frame{}\n\ttesting.SetOriginCaller(outside)\n\ttesting.SetRealm(testing.NewUserRealm(outside))\n\n\t// The same caller cannot open a proposal -- PreCreateProposal rejects\n\t// non-members. That is the authority level the DAO grants them, and\n\t// Execute must not grant more.\n\t_, err := dao.CreateProposal(cross(cur), dao.NewProposalRequest(\n\t\t\"rejected\", \"a non-member cannot propose\", dao.NewSimpleExecutor(0, cur, probe, \"\")))\n\tprintln(\"forger cannot even open a proposal:\", err != nil)\n\tprintln(\"callback ran:\", seen.ran)\n\n\tforged := dao.NewSimpleExecutor(0, cur, probe, \"no proposal backs this\")\n\tprintln(\"forged Execute is refused:\", forged.Execute(cross(cur)))\n\tprintln(\"callback ran:\", seen.ran)\n\n\t// 3. And because it never ran, there is no forged frame to compare: the\n\t// governance identity was not minted for an unauthorized invoker.\n\tprintln(\"no frame was handed out:\", seen.cur == \"\" \u0026\u0026 seen.prev == \"\")\n\tprintln(\"approved prev address was the gov/dao package address:\",\n\t\tapproved.prevAddr == chain.PackageAddress(\"gno.land/r/gov/dao\"))\n\n\t// 4. The forgery leaves no trace on the DAO either: no proposal id was\n\t// minted, so there is nothing to render, audit, or deny after the fact.\n\t_, missing := dao.GetProposal(pid + 1)\n\tprintln(\"no proposal was created for the forged execution:\", missing != nil)\n\n\t// 5. SafeExecutor still cannot be wired as written: allowedDAOs holds the\n\t// impl path, while an executor's caller is the proxy path, so its check\n\t// rejects the APPROVED path. It remains dead code; the gate that works\n\t// lives in SimpleExecutor.Execute and keys on the proxy, not on\n\t// InAllowedDAOs.\n\tprintln(\"InAllowedDAOs(proxy path):\", dao.InAllowedDAOs(\"gno.land/r/gov/dao\"))\n\tprintln(\"allowedDAOs holds instead:\", dao.AllowedDAOs()[0])\n\n\tseen = frame{}\n\ttesting.SetOriginCaller(member)\n\ttesting.SetRealm(testing.NewUserRealm(member))\n\tspid := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(\n\t\t\"safe\", \"same approved path, wrapped in SafeExecutor\",\n\t\tdao.NewSafeExecutor(dao.NewSimpleExecutor(0, cur, probe, \"\"))))\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, spid))\n\tprintln(\"SafeExecutor rejects the approved path:\", !dao.ExecuteOrRejectProposal(cross(cur), spid))\n\tprintln(\"and its callback never ran:\", !seen.ran)\n\tprintln(\"denial reason names the allowlist:\",\n\t\tstrings.Contains(dao.Render(cross(cur), spid.String()), \"execution only allowed by validated govDAOs\"))\n\n\t// 6. The reachability precondition is now closed at the primitive. A\n\t// realm-authored callback is stuck with its own current realm (the one\n\t// value a forger never controlled), and it can no longer borrow the\n\t// proxy as Previous() either -- so gates keying on Previous(), which\n\t// were the forgeable ones, hold.\n\ttesting.SetOriginCaller(outside)\n\ttesting.SetRealm(testing.NewUserRealm(outside))\n\tseen = frame{}\n\tprintln(\"forged Execute refused again:\",\n\t\tdao.NewSimpleExecutor(0, cur, probe, \"\").Execute(cross(cur)) != nil)\n\tprintln(\"no proxy identity was borrowed:\", seen.prev != \"gno.land/r/gov/dao\")\n\n\t// The member store was never reachable this way -- it gates on PkgPath(),\n\t// which the forgery does not control -- but the attempt is now refused one\n\t// layer earlier, at Execute, so the abort below never happens.\n\te := dao.NewSimpleExecutor(0, cur, func(cur realm) error {\n\t\tmemberstore.Get(0, cur).SetMember(memberstore.T1, outside, memberstore.NewMember(3))\n\t\treturn nil\n\t}, \"\")\n\tprintln(\"member-store escalation refused at Execute:\", e.Execute(cross(cur)) != nil)\n}\n\n// Output:\n// approved path executes: true\n// callback ran: true\n// approved frame cur : gno.land/r/test/execauthority\n// approved frame prev: gno.land/r/gov/dao\n// forger cannot even open a proposal: true\n// callback ran: false\n// forged Execute is refused: execution denied: executors are only invocable by gno.land/r/gov/dao\n// callback ran: false\n// no frame was handed out: true\n// approved prev address was the gov/dao package address: true\n// no proposal was created for the forged execution: true\n// InAllowedDAOs(proxy path): false\n// allowedDAOs holds instead: gno.land/r/gov/dao/impl/v1\n// SafeExecutor rejects the approved path: true\n// and its callback never ran: true\n// denial reason names the allowlist: true\n// forged Execute refused again: true\n// no proxy identity was borrowed: true\n// member-store escalation refused at Execute: true\n"},{"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/impl/v1\"\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\timplv0 \"gno.land/r/gov/dao/impl/v0\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\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\t// Refuse a proposal whose voter restriction this implementation cannot\n\t// enforce, before it exists.\n\tif _, err := voterTiers(r.Filter()); err != nil {\n\t\treturn caller, err\n\t}\n\n\treturn caller, nil\n}\n\nfunc (g *GovDAO) PostCreateProposal(_ int, rlm realm, r dao.ProposalRequest, pid dao.ProposalID) {\n\ttatv, err := voterTiers(r.Filter())\n\tif err != nil {\n\t\t// PreCreateProposal refuses these, so this is unreachable; panicking\n\t\t// keeps an unenforceable filter from ever becoming \"everyone votes\".\n\t\tpanic(err.Error())\n\t}\n\tg.pss.Set(pid.String(), newProposalStatus(tatv))\n}\n\n// ErrUnsupportedFilter is returned for a proposal whose filter this\n// implementation does not know how to enforce.\nvar ErrUnsupportedFilter = errors.New(\"unsupported proposal filter: voting restriction cannot be enforced\")\n\n// voterTiers returns the tiers allowed to vote on a proposal with filter f.\n//\n// It fails closed. impl/v0 stays in AllowedDAOs after impl/v1 is installed,\n// and v0's builders (NewAddMemberRequest, NewPromoteMemberRequest) tag their\n// proposals with impl/v0's FilterByTier, a different type from this\n// package's. Recognising only our own type would read a v0 T1 proposal as\n// unfiltered and let T2/T3 votes carry it. So both types are enforced with\n// v0's exact rules, and any other non-nil filter, or a filter tier that is not\n// a known tier, is refused rather than treated as \"no restriction\".\nfunc voterTiers(f dao.Filter) ([]string, error) {\n\tvar tier string\n\tswitch v := f.(type) {\n\tcase nil:\n\t\treturn []string{memberstore.T1, memberstore.T2, memberstore.T3}, nil\n\tcase FilterByTier:\n\t\ttier = v.Tier\n\tcase implv0.FilterByTier:\n\t\ttier = v.Tier\n\tdefault:\n\t\treturn nil, ErrUnsupportedFilter\n\t}\n\n\tswitch tier {\n\tcase memberstore.T1:\n\t\t// only members from T1 are allowed to vote when adding new members to T1\n\t\treturn []string{memberstore.T1}, nil\n\tcase memberstore.T2:\n\t\t// only members from T1 and T2 are allowed to vote when adding new members to T2\n\t\treturn []string{memberstore.T1, memberstore.T2}, nil\n\tcase memberstore.T3:\n\t\treturn []string{memberstore.T1, memberstore.T2, memberstore.T3}, nil\n\tdefault:\n\t\treturn nil, ErrUnsupportedFilter\n\t}\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 == nil {\n\t\t// Unknown to this implementation: either an unknown id, or a\n\t\t// proposal created before a DAO upgrade replaced this GovDAO\n\t\t// (statuses live on the instance, so a fresh instance has none).\n\t\t// Mirrors VoteOnProposal above, which returns this same error for\n\t\t// the same lookup. Without it the nil deref panics with an opaque\n\t\t// \"runtime error: nil pointer dereference\" that reads like a VM\n\t\t// fault. It reports the condition only — see the ADR for why the\n\t\t// proposal still cannot be resolved.\n\t\treturn false, errors.New(\"proposal not found\")\n\t}\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\t// Clamped on the way in, not just on the way out. The error comes from\n\t\t// the proposal's executor — third-party code — and this assignment is a\n\t\t// write to realm storage, paid for by whoever executes the proposal\n\t\t// rather than by whoever wrote the executor. Bounding it here means the\n\t\t// realm never stores a reason larger than it can display. The clamp at\n\t\t// the render site stays, because reasons stored before this change are\n\t\t// still unbounded.\n\t\tstatus.DeniedReason = \"execution failed: \" + clampField(err.Error(), maxRenderedReason)\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.\n//\n// This is also what makes the unsafe.OriginCaller()-based membership\n// checks safe. Those key on the transaction origin (an EOA), which is\n// correct only if the origin is the immediate caller. isValidCall\n// guarantees exactly that: it admits prev only when prev.IsUser() (a\n// direct EOA call or the origin's own ephemeral run realm) or when\n// prev's package address equals the origin — never a third-party realm\n// in the middle. Relaxing this to allow realm intermediaries would let\n// one member's vote be cast under another origin: keep the two in sync.\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":"govdao_execute_proposal_00_filetest.gno","body":"// PKGPATH: gno.land/r/test/govdao\npackage govdao\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/impl/v1\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\n)\n\nconst user address = \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"\n\nvar (\n\texecutor   dao.Executor\n\tproposalID dao.ProposalID\n\trenderPath string\n\tgovdao     = impl.NewGovDAO()\n)\n\nfunc init(cur realm) {\n\t// Initialize GovDAO members\n\tmemberstore.Get(0, cur).DeleteAll()\n\tmemberstore.Get(0, cur).SetTier(memberstore.T1)\n\tmemberstore.Get(0, cur).SetMember(memberstore.T1, user, memberstore.NewMember(3))\n\tdao.UpdateImpl(cross(cur), dao.NewUpdateRequest(impl.NewGovDAO(), nil))\n\n\t// Create an executor that always fails\n\tcb := func(realm) error { return errors.New(\"Boom!\") }\n\texecutor = dao.NewSimpleExecutor(0, cur, cb, \"\")\n\n\t// Create a proposal request that fails on execution\n\trequest := dao.NewProposalRequest(\"Test\", \"This proposal always fails on execution\", executor)\n\n\t// Create the proposal from a realm so GovDAO instance is able to render the proposal\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tproposalID = dao.MustCreateProposal(cross(cur), request)\n\trenderPath = strconv.FormatUint(uint64(proposalID), 10)\n\n\t// Register proposal with the local GovDAO instance\n\tgovdao.PostCreateProposal(0, cur, request, proposalID)\n}\n\nfunc main(cur realm) {\n\t// Execute proposal, status should be REJECTED\n\t// SimpleExecutor.Execute is invocable only by the r/gov/dao proxy, which\n\t// is the realm the live route calls it from (dao.ExecuteProposal -\u003e\n\t// impl.ExecuteProposal, both threading the proxy's cur non-crossing).\n\t// This filetest calls impl.ExecuteProposal directly, so stand in for the\n\t// proxy explicitly rather than exercising a route no transaction takes.\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao\"))\n\n\terr := govdao.ExecuteProposal(0, cur, proposalID, executor)\n\n\tprintln(err.Error())\n\tprintln()\n\tprintln(govdao.Render(cross(cur), \"gno.land/r/gov/dao/impl/v1\", renderPath))\n}\n\n// Output:\n// Boom!\n//\n// ## Prop #0 - Test\n// Author: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n//\n//\n//\n// This proposal always fails on execution\n//\n//\n//\n// Executor created in: `gno.land/r/test/govdao`\n//\n//\n//\n//\n// ---\n//\n// ### Stats\n// - **PROPOSAL HAS BEEN DENIED**\n// REASON: execution failed: Boom\\!\n// - Tiers eligible to vote: T1, T2, T3\n// - YES PERCENT: 0%\n// - NO PERCENT: 0%\n// - ABSTAIN PERCENT: 0%\n//\n// [Detailed voting list](/r/gov/dao/impl/v1:0/votes)\n//\n// ---\n//\n// ### Actions\n// [Vote YES](/r/gov/dao$help\u0026func=MustVoteOnProposalSimple\u0026option=YES\u0026pid=0) | [Vote NO](/r/gov/dao$help\u0026func=MustVoteOnProposalSimple\u0026option=NO\u0026pid=0) | [Vote ABSTAIN](/r/gov/dao$help\u0026func=MustVoteOnProposalSimple\u0026option=ABSTAIN\u0026pid=0)\n//\n// WARNING: Please double check transaction data before voting.\n"},{"name":"govdao_execute_proposal_01_filetest.gno","body":"// PKGPATH: gno.land/r/test/govdao\npackage govdao\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/impl/v1\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\n)\n\nconst user address = \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"\n\nvar (\n\texecutor   dao.Executor\n\tproposalID dao.ProposalID\n\trenderPath string\n\tgovdao     = impl.NewGovDAO()\n)\n\nfunc init(cur realm) {\n\t// Initialize GovDAO members\n\tmemberstore.Get(0, cur).DeleteAll()\n\tmemberstore.Get(0, cur).SetTier(memberstore.T1)\n\tmemberstore.Get(0, cur).SetMember(memberstore.T1, user, memberstore.NewMember(3))\n\tdao.UpdateImpl(cross(cur), dao.NewUpdateRequest(impl.NewGovDAO(), nil))\n\n\t// Create a dummy executor\n\tcb := func(realm) error { return nil }\n\texecutor = dao.NewSimpleExecutor(0, cur, cb, \"\")\n\n\t// Create a proposal request that pass on execution\n\trequest := dao.NewProposalRequest(\"Test\", \"This proposal always pass on execution\", executor)\n\n\t// Create the proposal from a realm so GovDAO instance is able to render the proposal\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tproposalID = dao.MustCreateProposal(cross(cur), request)\n\trenderPath = strconv.FormatUint(uint64(proposalID), 10)\n\n\t// Register proposal with the local GovDAO instance\n\tgovdao.PostCreateProposal(0, cur, request, proposalID)\n}\n\nfunc main(cur realm) {\n\t// Execute proposal, status should be ACTIVE\n\t// SimpleExecutor.Execute is invocable only by the r/gov/dao proxy, which\n\t// is the realm the live route calls it from (dao.ExecuteProposal -\u003e\n\t// impl.ExecuteProposal, both threading the proxy's cur non-crossing).\n\t// This filetest calls impl.ExecuteProposal directly, so stand in for the\n\t// proxy explicitly rather than exercising a route no transaction takes.\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao\"))\n\n\terr := govdao.ExecuteProposal(0, cur, proposalID, executor)\n\n\tprintln(err == nil)\n\tprintln()\n\tprintln(govdao.Render(cross(cur), \"gno.land/r/gov/dao/impl/v1\", renderPath))\n}\n\n// Output:\n// true\n//\n// ## Prop #0 - Test\n// Author: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n//\n//\n//\n// This proposal always pass on execution\n//\n//\n//\n// Executor created in: `gno.land/r/test/govdao`\n//\n//\n//\n//\n// ---\n//\n// ### Stats\n// - **Proposal is open for votes**\n// - Tiers eligible to vote: T1, T2, T3\n// - YES PERCENT: 0%\n// - NO PERCENT: 0%\n// - ABSTAIN PERCENT: 0%\n//\n// [Detailed voting list](/r/gov/dao/impl/v1:0/votes)\n//\n// ---\n//\n// ### Actions\n// [Vote YES](/r/gov/dao$help\u0026func=MustVoteOnProposalSimple\u0026option=YES\u0026pid=0) | [Vote NO](/r/gov/dao$help\u0026func=MustVoteOnProposalSimple\u0026option=NO\u0026pid=0) | [Vote ABSTAIN](/r/gov/dao$help\u0026func=MustVoteOnProposalSimple\u0026option=ABSTAIN\u0026pid=0)\n//\n// WARNING: Please double check transaction data before voting.\n"},{"name":"govdao_execute_proposal_02_filetest.gno","body":"// PKGPATH: gno.land/r/test/govdao\n\npackage govdao\n\nimport \"gno.land/r/gov/dao/impl/v1\"\n\nvar govdao = impl.NewGovDAO()\n\nfunc main(cur realm) {\n\t// Try to execute a proposal using a nil executor\n\tgovdao.ExecuteProposal(0, cur, 0, nil)\n}\n\n// Error:\n// an executor is required to execute the proposal\n"},{"name":"govdao_execute_proposal_03_filetest.gno","body":"// PKGPATH: gno.land/r/test/govdao\npackage govdao\n\nimport (\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/impl/v1\"\n)\n\nvar (\n\texecutor dao.Executor\n\tgovdao   = impl.NewGovDAO()\n)\n\nfunc init(cur realm) {\n\t// Create a dummy executor\n\tcb := func(realm) error { return nil }\n\texecutor = dao.NewSimpleExecutor(0, cur, cb, \"\")\n}\n\nfunc main(cur realm) {\n\t// Try to execute a proposal that doesn't exist\n\tgovdao.ExecuteProposal(0, cur, 404, executor)\n}\n\n// Error:\n// proposal not found\n"},{"name":"govdao_test.gno","body":"package impl\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\n)\n\n// cur is a zero-value realm used as a placeholder when forwarding to\n// uassert/urequire dispatch helpers that gained an `rlm realm` param.\n// These tests pass `func()` callbacks (no crossing inside the callback),\n// so rlm is ignored — a nil realm here is safe.\nvar cur realm\n\nfunc init(cur realm) {\n\tloadMembers(cur)\n\n\tdao.UpdateImpl(cross(cur), dao.NewUpdateRequest(govDAO, []string{\"gno.land/r/gov/dao/impl/v1\"}))\n}\n\nvar (\n\tm1    = testutils.TestAddress(\"m1\")\n\tm11   = testutils.TestAddress(\"m1.1\")\n\tm111  = testutils.TestAddress(\"m1.1.1\")\n\tm1111 = testutils.TestAddress(\"m1.1.1.1\")\n\tm2    = testutils.TestAddress(\"m2\")\n\tm3    = testutils.TestAddress(\"m3\")\n\tm4    = testutils.TestAddress(\"m4\")\n\tm5    = testutils.TestAddress(\"m5\")\n\tm6    = testutils.TestAddress(\"m6\")\n\n\tnoMember = testutils.TestAddress(\"nm1\")\n)\n\nfunc loadMembers(cur realm) {\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\tmstore := memberstore.Get(0, cur)\n\tmstore.DeleteAll()\n\n\tmstore.SetTier(memberstore.T1)\n\tmstore.SetTier(memberstore.T2)\n\tmstore.SetTier(memberstore.T3)\n\n\tmstore.SetMember(memberstore.T1, m1, memberByTier(memberstore.T1))\n\tmstore.SetMember(memberstore.T1, m11, memberByTier(memberstore.T1))\n\tmstore.SetMember(memberstore.T1, m111, memberByTier(memberstore.T1))\n\tmstore.SetMember(memberstore.T1, m1111, memberByTier(memberstore.T1))\n\n\tmstore.SetMember(memberstore.T2, m2, memberByTier(memberstore.T2))\n\tmstore.SetMember(memberstore.T2, m3, memberByTier(memberstore.T2))\n\tmstore.SetMember(memberstore.T3, m4, memberByTier(memberstore.T3))\n\tmstore.SetMember(memberstore.T3, m5, memberByTier(memberstore.T3))\n\tmstore.SetMember(memberstore.T3, m6, memberByTier(memberstore.T3))\n}\n\nfunc TestCreateProposalAndVote(cur realm, t *testing.T) {\n\tloadMembers(cur)\n\n\tportfolio := \"# This is my portfolio:\\n\\n- THINGS\"\n\n\ttesting.SetOriginCaller(noMember)\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao/impl/v1\"))\n\n\tnm1 := testutils.TestAddress(\"nm1\")\n\n\turequire.AbortsWithMessage(t, cur, \"Only T1 and T2 members can be added by proposal. To add a T3 member use AddMember function directly.\", func(cur realm) {\n\t\tdao.MustCreateProposal(cross(cur), NewAddMemberRequest(cur, nm1, memberstore.T3, portfolio))\n\t})\n\n\turequire.AbortsWithMessage(t, cur, \"proposer is not a member\", func(cur realm) {\n\t\tdao.MustCreateProposal(cross(cur), NewAddMemberRequest(cur, nm1, memberstore.T2, portfolio))\n\t})\n\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao/impl/v1\"))\n\n\tproposalRequest := NewAddMemberRequest(cur, nm1, memberstore.T2, portfolio)\n\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\tpid := dao.MustCreateProposal(cross(cur), proposalRequest)\n\turequire.Equal(t, int(pid), 0)\n\n\t// m1 votes yes because that member is interested on it\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(0)))\n\n\ttesting.SetOriginCaller(m11)\n\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.NoVote, dao.ProposalID(0)))\n\n\ttesting.SetOriginCaller(m2)\n\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.NoVote, dao.ProposalID(0)))\n\n\ttesting.SetOriginCaller(m3)\n\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.NoVote, dao.ProposalID(0)))\n\n\ttesting.SetOriginCaller(m4)\n\n\turequire.AbortsWithMessage(t, cur, \"member on specified tier is not allowed to vote on this proposal\", func() {\n\t\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.NoVote, dao.ProposalID(0)))\n\t})\n\n\ttesting.SetOriginCaller(m111)\n\n\t// Same effect as:\n\t// dao.MustVoteOnProposal(dao.VoteRequest{\n\t// \tOption:     dao.NoVote,\n\t// \tProposalID: dao.ProposalID(0),\n\t// })\n\tdao.MustVoteOnProposalSimple(cross(cur), 0, \"NO\")\n\n\turequire.Equal(t, true, strings.Contains(dao.Render(cross(cur), \"\"), \"Prop #0 - New T2 Member Proposal\"))\n\t// urequire.Equal(t, true, strings.Contains(dao.Render(cross(cur), \"\"), \"Author: \"+m1.String()))\n\n\turequire.AbortsWithMessage(t, cur, \"proposal didn't reach supermajority yet: 66.66\", func() {\n\t\tdao.ExecuteProposal(cross(cur), dao.ProposalID(0))\n\t})\n\n\ttesting.SetOriginCaller(m1111)\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.NoVote, dao.ProposalID(0)))\n\n\taccepted := dao.ExecuteProposal(cross(cur), dao.ProposalID(0))\n\turequire.Equal(t, false, accepted)\n\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), \"0\"), \"**PROPOSAL HAS BEEN DENIED**\"))\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), \"0\"), \"NO PERCENT: 81.25%\"))\n}\n\nfunc TestExecutorCreationRealm(cur realm, t *testing.T) {\n\tloadMembers(cur)\n\n\t// Test that executor creation realm is captured correctly\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/template/contract\"))\n\n\t// Create executor in the template contract realm\n\texecutor := dao.NewSimpleExecutor(0, cur, func(realm) error { return nil }, \"Test executor from template\")\n\n\tproposalRequest := dao.NewProposalRequest(\n\t\t\"Test Proposal\",\n\t\t\"This proposal tests executor creation realm tracking\",\n\t\texecutor,\n\t)\n\n\t// Create proposal from user realm (user can call DAO directly)\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\tpid := dao.MustCreateProposal(cross(cur), proposalRequest)\n\n\t// Get the proposal\n\tprop := dao.MustGetProposal(pid)\n\n\t// Verify the author is m1\n\turequire.Equal(t, m1, prop.Author())\n\n\t// Verify the executor creation realm is captured correctly\n\turequire.Equal(t, \"gno.land/r/template/contract\", prop.ExecutorCreationRealm())\n\n\t// Check that it's displayed in the individual proposal render output\n\tindividualRendered := dao.Render(cross(cur), pid.String())\n\t// Rendered as a code span: CreationRealm() is interface-dispatched and so\n\t// third-party controlled, and InlineCode neutralizes it while reading\n\t// naturally for a realm path.\n\turequire.Equal(t, true, contains(individualRendered, \"Executor created in: `gno.land/r/template/contract`\"))\n\turequire.Equal(t, true, contains(individualRendered, \"Test executor from template\"))\n\n\t// Also verify the main content is there\n\turequire.Equal(t, true, contains(individualRendered, \"Test Proposal\"))\n\turequire.Equal(t, true, contains(individualRendered, \"This proposal tests executor creation realm tracking\"))\n}\n\nfunc TestProposalPagination(cur realm, t *testing.T) {\n\tloadMembers(cur)\n\tportfolio := \"### This is my portfolio:\\n\\n- THINGS\"\n\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao/impl/v1\"))\n\n\tnm1 := testutils.TestAddress(\"nm1\")\n\n\tvar pid dao.ProposalID\n\n\tproposalRequest := NewAddMemberRequest(cur, nm1, memberstore.T2, portfolio)\n\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\tpid = dao.MustCreateProposal(cross(cur), proposalRequest)\n\n\t// TODO: tests keep the same vm state: https://github.com/gnolang/gno/issues/1982\n\turequire.Equal(t, 2, int(pid))\n\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\tpid = dao.MustCreateProposal(cross(cur), proposalRequest)\n\turequire.Equal(t, 3, int(pid))\n\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\tpid = dao.MustCreateProposal(cross(cur), proposalRequest)\n\turequire.Equal(t, 4, int(pid))\n\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\tpid = dao.MustCreateProposal(cross(cur), proposalRequest)\n\turequire.Equal(t, 5, int(pid))\n\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\tpid = dao.MustCreateProposal(cross(cur), proposalRequest)\n\turequire.Equal(t, 6, int(pid))\n\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\tpid = dao.MustCreateProposal(cross(cur), proposalRequest)\n\turequire.Equal(t, 7, int(pid))\n\n\tfmt.Println(dao.Render(cross(cur), \"\"))\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), \"\"), \"### [Prop #7 - New T2 Member Proposal](/r/gov/dao:7)\"))\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), \"\"), \"### [Prop #6 - New T2 Member Proposal](/r/gov/dao:6)\"))\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), \"\"), \"### [Prop #5 - New T2 Member Proposal](/r/gov/dao:5)\"))\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), \"\"), \"### [Prop #4 - New T2 Member Proposal](/r/gov/dao:4)\"))\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), \"\"), \"### [Prop #3 - New T2 Member Proposal](/r/gov/dao:3)\"))\n\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), \"?page=2\"), \"### [Prop #2 - New T2 Member Proposal](/r/gov/dao:2)\"))\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), \"?page=2\"), \"### [Prop #1 - Test Proposal](/r/gov/dao:1)\"))\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), \"?page=2\"), \"### [Prop #0 - New T2 Member Proposal](/r/gov/dao:0)\"))\n}\n\nfunc TestUpgradeDaoImplementation(cur realm, t *testing.T) {\n\tloadMembers(cur)\n\n\ttesting.SetOriginCaller(noMember)\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao/impl/v1\"))\n\n\turequire.PanicsWithMessage(t, cur, \"proposer is not a member\", func() {\n\t\tNewUpgradeDaoImplRequest(cur, govDAO, \"gno.land/r/gov/dao/v1/impl\", \"Something happened and we have to fix it.\")\n\t})\n\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao/impl/v1\"))\n\n\tpreq := NewUpgradeDaoImplRequest(cur, govDAO, \"gno.land/r/gov/dao/v1/impl\", \"Something happened and we have to fix it.\")\n\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\tpid := dao.MustCreateProposal(cross(cur), preq)\n\turequire.Equal(t, int(pid), 8)\n\n\t// m1 votes yes because that member is interested on it\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(pid)))\n\n\ttesting.SetOriginCaller(m11)\n\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(pid)))\n\n\ttesting.SetOriginCaller(m2)\n\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(pid)))\n\n\ttesting.SetOriginCaller(m3)\n\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(pid)))\n\n\ttesting.SetOriginCaller(m111)\n\n\t// Same effect as:\n\t// dao.MustVoteOnProposal(dao.VoteRequest{\n\t// \tOption:     dao.YesVote,\n\t// \tProposalID: dao.ProposalID(pid),\n\t// })\n\tdao.MustVoteOnProposalSimple(cross(cur), int64(pid), \"YES\")\n\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), \"8\"), \"**Proposal is open for votes**\"))\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), \"8\"), \"68.42105263157895%\"))\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), \"8\"), \"0%\"))\n\n\taccepted := dao.ExecuteProposal(cross(cur), dao.ProposalID(pid))\n\turequire.Equal(t, true, accepted)\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), \"8\"), \"**PROPOSAL HAS BEEN ACCEPTED**\"))\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), \"8\"), \"YES PERCENT: 68.42105263157895%\"))\n}\n\nfunc TestAbstainVote(cur realm, t *testing.T) {\n\tloadMembers(cur)\n\n\tportfolio := \"# This is my portfolio:\\n\\n- THINGS\"\n\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao/impl/v1\"))\n\n\tnm1 := testutils.TestAddress(\"nm1\")\n\tproposalRequest := NewAddMemberRequest(cur, nm1, memberstore.T2, portfolio)\n\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\tpid := dao.MustCreateProposal(cross(cur), proposalRequest)\n\n\t// m1 votes abstain\n\tdao.MustVoteOnProposalSimple(cross(cur), int64(pid), \"ABSTAIN\")\n\n\t// Other members vote YES to reach supermajority\n\ttesting.SetOriginCaller(m11)\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, pid))\n\n\ttesting.SetOriginCaller(m111)\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, pid))\n\n\ttesting.SetOriginCaller(m1111)\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, pid))\n\n\ttesting.SetOriginCaller(m2)\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, pid))\n\n\ttesting.SetOriginCaller(m3)\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, pid))\n\n\t// Verify render shows correct percentages\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), pid.String()), \"YES PERCENT: 81.25%\"))\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), pid.String()), \"NO PERCENT: 0%\"))\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), pid.String()), \"ABSTAIN PERCENT: 18.75%\"))\n\n\t// Supermajority reached despite one abstain voter\n\taccepted := dao.ExecuteProposal(cross(cur), pid)\n\turequire.Equal(t, true, accepted)\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), pid.String()), \"**PROPOSAL HAS BEEN ACCEPTED**\"))\n\n\t// Abstain voter appears in vote list\n\turequire.Equal(t, true, contains(dao.Render(cross(cur), fmt.Sprintf(\"%v/votes\", int64(pid))), \"ABSTAIN\"))\n}\n\nfunc contains(s, substr string) bool {\n\treturn strings.Index(s, substr) \u003e= 0\n}\n\n// TestAddMemberKeepsInvitationPointWhenAddFails covers the executor's ordering.\n//\n// The executor spends the proposer's invitation point and adds the member. If\n// the add fails, a returned executor error does NOT revert: ExecuteProposal\n// marks the proposal denied and ExecuteOrRejectProposal still commits the\n// transaction. So with the old order the point was gone and the member was not\n// added, permanently.\n//\n// The failure is reachable: SetMember refuses an address that is already a\n// member, and AddMember enrols T3 members directly, so proposing an existing\n// member to another tier is an ordinary mistake rather than a contrived one.\n//\n// Uses ExecuteOrRejectProposal deliberately. ExecuteProposal panics on an\n// executor error, and a panic aborts the transaction, which would revert the\n// spend and hide the bug.\nfunc TestAddMemberKeepsInvitationPointWhenAddFails(cur realm, t *testing.T) {\n\tloadMembers(cur)\n\n\t// m4 is already a T3 member, so adding it to T2 must fail in SetMember.\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao/impl/v1\"))\n\tpreq := NewAddMemberRequest(cur, m4, memberstore.T2, \"# Portfolio\\n\\n- things\")\n\n\t// memberstore.Get requires an allowed-DAO caller, so read under the impl\n\t// realm rather than whatever the previous step left in place.\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao/impl/v1\"))\n\tbefore, tier := memberstore.Get(0, cur).GetMember(m1)\n\turequire.Equal(t, memberstore.T1, tier)\n\tpointsBefore := before.InvitationPoints\n\turequire.Equal(t, true, pointsBefore \u003e 0, \"premise: the proposer must have a point to lose\")\n\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\tpid := dao.MustCreateProposal(cross(cur), preq)\n\n\t// A T2 addition is voted on by T1 and T2. Every one of them votes yes, so\n\t// the proposal certainly passes and the executor certainly runs -- if it did\n\t// not, this test would prove nothing.\n\tfor _, voter := range []address{m1, m11, m111, m1111, m2, m3} {\n\t\ttesting.SetOriginCaller(voter)\n\t\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(pid)))\n\t}\n\n\taccepted := dao.ExecuteOrRejectProposal(cross(cur), dao.ProposalID(pid))\n\turequire.Equal(t, false, accepted, \"the add must fail, since m4 is already a member\")\n\n\t// Proves the executor ran and returned an error, rather than the vote\n\t// failing to reach a threshold -- only GovDAO.ExecuteProposal writes this.\n\trendered := dao.Render(cross(cur), fmt.Sprintf(\"%d\", int(pid)))\n\turequire.Equal(t, true, contains(rendered, \"execution failed\"),\n\t\t\"the proposal must have been denied by the executor, not by the vote\")\n\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao/impl/v1\"))\n\tafter, _ := memberstore.Get(0, cur).GetMember(m1)\n\turequire.Equal(t, pointsBefore, after.InvitationPoints,\n\t\t\"a failed add must not spend the proposer's invitation point\")\n}\n\n// TestPromoteToTierMissingFromStoreKeepsTheMember covers the remove-then-add\n// hazard in the promotion executor.\n//\n// The executor removes the member and re-adds them at the new tier. SetMember\n// returns an error rather than panicking when the store has no bucket for that\n// tier, and a returned executor error does not revert -- the proposal is marked\n// rejected and the transaction still commits -- so the member would be removed\n// and not re-added.\n//\n// The two tier registries are separate. GetTier reads the global definitions,\n// which always hold T1/T2/T3; SetMember checks whether this store has a bucket,\n// and DeleteAll empties the buckets while leaving the definitions intact. So a\n// destination can pass the first check and fail the second, which is what this\n// sets up.\nfunc TestPromoteToTierMissingFromStoreKeepsTheMember(cur realm, t *testing.T) {\n\tloadMembers(cur)\n\n\t// Drop the T3 bucket while the global definition of T3 survives.\n\tmstore := memberstore.Get(0, cur)\n\tmstore.DeleteAll()\n\tmstore.SetTier(memberstore.T1)\n\tmstore.SetTier(memberstore.T2)\n\tmstore.SetMember(memberstore.T1, m1, memberByTier(memberstore.T1))\n\tmstore.SetMember(memberstore.T1, m11, memberByTier(memberstore.T1))\n\tmstore.SetMember(memberstore.T1, m111, memberByTier(memberstore.T1))\n\tmstore.SetMember(memberstore.T1, m1111, memberByTier(memberstore.T1))\n\tmstore.SetMember(memberstore.T2, m2, memberByTier(memberstore.T2))\n\tmstore.SetMember(memberstore.T2, m3, memberByTier(memberstore.T2))\n\n\t// Premise: T3 is defined globally but absent from this store.\n\t_, defined := memberstore.GetTier(memberstore.T3)\n\turequire.Equal(t, true, defined, \"premise: T3 must still be a defined tier\")\n\turequire.Equal(t, false, mstore.Has(memberstore.T3),\n\t\t\"premise: this store must have no T3 bucket, or the test proves nothing\")\n\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao/impl/v1\"))\n\tpreq := NewPromoteMemberRequest(cur, m1, memberstore.T1, memberstore.T3)\n\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\tpid := dao.MustCreateProposal(cross(cur), preq)\n\n\tfor _, voter := range []address{m1, m11, m111, m1111, m2, m3} {\n\t\ttesting.SetOriginCaller(voter)\n\t\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(pid)))\n\t}\n\n\t// The executor refuses before removing anything, and returns rather than\n\t// panicking. That rejects the proposal and closes it, and since nothing was\n\t// written, committing the rejection loses nothing.\n\t//\n\t// So the assertion below is about the chain, not the harness: the member\n\t// survives because no write happened, not because a panic reverted one.\n\t// Under the earlier panic-after-removal shape it held only because gno test\n\t// does not revert an abort, which proved nothing about production.\n\taccepted := dao.ExecuteOrRejectProposal(cross(cur), dao.ProposalID(pid))\n\turequire.Equal(t, false, accepted, \"the promotion must be rejected\")\n\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao/impl/v1\"))\n\tafter, tier := memberstore.Get(0, cur).GetMember(m1)\n\turequire.Equal(t, true, after != nil, \"the member must survive a refused promotion\")\n\turequire.Equal(t, memberstore.T1, tier, \"and must still be on their original tier\")\n\n\tloadMembers(cur)\n}\n"},{"name":"impl.gno","body":"package impl\n\nimport (\n\t\"gno.land/p/nt/markdown/sanitize/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/gov/dao\"\n\timplv0 \"gno.land/r/gov/dao/impl/v0\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\n)\n\nvar (\n\tlaw    *Law\n\tgovDAO *GovDAO = NewGovDAO()\n)\n\n// defaultSupermajority is the law impl/v1 starts with, and the weakest law an\n// install proposal may carry.\nconst defaultSupermajority = 66.66 // Two thirds\n\nfunc init() {\n\tlaw = \u0026Law{\n\t\tSupermajority: defaultSupermajority,\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\t// AGENTS.md: check IsCurrent() before cur.Previous() in a crossing function.\n\tif !cur.IsCurrent() {\n\t\tpanic(\"AddMember: realm value is not the caller's live cur\")\n\t}\n\t// address args are not VM-validated (the raw MsgCall string is stored), so\n\t// reject a non-bech32 addr here — matching InitWithUsers. This keeps the\n\t// member store free of unauthenticable keys and of markdown/pipe/HTML\n\t// metachars that would otherwise inject into the members render table.\n\tif !addr.IsValid() {\n\t\tpanic(\"invalid member address: \" + addr.String())\n\t}\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// NewInstallRequest builds the proposal that installs this implementation\n// (impl/v1) in place of impl/v0. It is how v1 goes live: a v0-era member\n// creates it, the DAO votes on it, and executing it runs dao.UpdateImpl with\n// this realm's GovDAO and AllowedDAOs = [impl/v0, impl/v1].\n//\n// The request is built by impl/v0.NewUpgradeDaoImplRequest, so its checks\n// (proposer is a member, realm path is well-formed) and its disclosure text are\n// v0's. The *GovDAO is handed over inside this proposal rather than exported:\n// its PostCreateProposal and ExecuteProposal trust the r/gov/dao proxy and must\n// not be reachable from anywhere else. impl/v0 stays allowed, so its own\n// proposal builders (a v0 treasury payment that moves the v0 funds, for\n// instance) keep executing.\n//\n// newLaw is the law impl/v1 decides with from its first decision on. impl/v0\n// keeps its law in an unexported variable with no accessor, so it cannot be\n// read and carried over; the proposer states it instead, the proposal text\n// shows it, and the DAO votes on it under impl/v0's own law. It is in force\n// from impl/v1's first decision (see installedDAO), so impl/v1 never decides\n// anything with a law the DAO did not approve. A law weaker than impl/v1's\n// default is refused: a DAO that wants one lowers it by a law-change vote\n// after the install, in the open.\n//\n// reason is caller-supplied and lands in the same rendered description as\n// the law line, so it is escaped (sanitize.InlineText): markdown in it cannot\n// hide what follows, e.g. by opening an HTML comment.\nfunc NewInstallRequest(cur realm, reason string, newLaw Law) dao.ProposalRequest {\n\tif !(newLaw.Supermajority \u003e= defaultSupermajority \u0026\u0026 newLaw.Supermajority \u003c= 100) {\n\t\tpanic(ufmt.Sprintf(\"the install law's Supermajority must be in [%v, 100], got %v\", defaultSupermajority, newLaw.Supermajority))\n\t}\n\td := \u0026installedDAO{GovDAO: govDAO, law: newLaw}\n\ttext := sanitize.InlineText(reason) + ufmt.Sprintf(\n\t\t\"\\n\\n**Law after the install: Supermajority %v%%.** impl/v1 decides with this law, \"+\n\t\t\t\"carried by this proposal, from its first decision on. impl/v0's current law is \"+\n\t\t\t\"not read: check that this is the law the DAO means to keep.\", newLaw.Supermajority)\n\treturn implv0.NewUpgradeDaoImplRequest(cross(cur), d, \"gno.land/r/gov/dao/impl/v1\", text)\n}\n\n// installedDAO is the DAO value an install proposal hands to dao.UpdateImpl:\n// this realm's GovDAO plus the law the proposal carried.\n//\n// The carried law is applied on the first proxy-validated PreExecuteProposal\n// after each installation, the only place a law is read, so no decision is\n// ever made with another law. Later law changes (NewChangeLawRequest) stand\n// until the next installation.\n//\n// \"Each installation\" is read from r/gov/dao's InstallGeneration, which\n// UpdateImpl increments on every non-nil DAO assignment, including assigning\n// this same value again. The proxy keeps a request's executor, so an install\n// request can be submitted twice; executing it again bumps the generation and\n// re-applies its law. Nothing a proposal or its executor reports is consulted,\n// and an allowlist-only update (nil DAO) does not count as an installation.\ntype installedDAO struct {\n\t*GovDAO\n\tlaw Law\n}\n\n// appliedGeneration is the proxy installation generation at which an\n// installedDAO last applied its carried law.\nvar appliedGeneration uint64\n\nfunc (d *installedDAO) PreExecuteProposal(_ int, rlm realm, pid dao.ProposalID) (bool, error) {\n\t// Only a call through the r/gov/dao proxy applies the law; the proxy only\n\t// calls the installed DAO. Anything else falls through to the same check\n\t// inside GovDAO.PreExecuteProposal and is refused there.\n\t// The proxy calls PreExecuteProposal on the current implementation only,\n\t// so after the check this is the installedDAO the generation belongs to.\n\tif gen := dao.InstallGeneration(); d.GovDAO.isValidCall(0, rlm) \u0026\u0026 appliedGeneration != gen {\n\t\tl := d.law\n\t\tlaw = \u0026l\n\t\tappliedGeneration = gen\n\t}\n\treturn d.GovDAO.PreExecuteProposal(0, rlm, pid)\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/v0\"\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/nt/markdown/sanitize/v0\"\n\ttrs_pkg \"gno.land/p/nt/treasury/v1\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\n\t\"gno.land/r/gov/dao/treasury/v1\"\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\t// Rejected here as well as in UpdateImpl so the mistake surfaces when the\n\t// proposal is written, not when it executes. An empty realmPkg would be\n\t// stored as an allowlist entry matching every user realm's empty\n\t// PkgPath(), and would render as a blank name in the grant sentence below.\n\tif strings.TrimSpace(realmPkg) == \"\" {\n\t\tpanic(\"realmPkg must be the realm path being granted govDAO authority\")\n\t}\n\t// Padding is rejected for the same reason UpdateImpl rejects it: the entry\n\t// is stored as given and matched whole, so a padded path grants nobody\n\t// anything. Caught here so the author sees it, not the voters.\n\tif realmPkg != strings.TrimSpace(realmPkg) {\n\t\tpanic(\"realmPkg must not have leading or trailing spaces\")\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\tcb := func(cur realm) error {\n\t\t// dao.UpdateImpl() must be cross-called from v0/impl but\n\t\t// what calls this cb function is r/gov/dao.\n\t\t// therefore we must cross back into v0/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/impl/v1\", realmPkg}))\n\t\treturn nil\n\t}\n\n\t// State the grant: this executor rewrites AllowedDAOs, and previously\n\t// rendered nothing at all. realmPkg is caller-supplied and lands in a code\n\t// span, so it is wrapped with sanitize.InlineCode — which emits its own\n\t// fence, so do not add backticks around it. (md.EscapeText is wrong here:\n\t// CommonMark 6.1 does not process backslash escapes inside code spans.)\n\t// Nothing ties realmPkg to newDao, which is why the text says so.\n\te := dao.NewSimpleExecutor(0, cur, cb, ufmt.Sprintf(\n\t\t\"Replaces the govDAO implementation with the DAO carried by this proposal.\\n\\n\"+\n\t\t\t\"After execution, only `gno.land/r/gov/dao/impl/v1` and %s may replace the \"+\n\t\t\t\"implementation, mutate the member store, or move treasury funds.\\n\\n\"+\n\t\t\t\"This grant is stated by the proposer: the incoming implementation is passed \"+\n\t\t\t\"as a value and is not verified to belong to the realm named above.\",\n\t\tsanitize.InlineCode(realmPkg)))\n\n\t// reason is caller-supplied and rendered above the grant disclosure; it is\n\t// escaped so it cannot hide it (renderers also sanitize descriptions).\n\treturn dao.NewProposalRequest(\"Change DAO implementation\", \"This proposal is looking to change the actual govDAO implementation. Reason: \"+sanitize.InlineText(reason), e)\n}\n\nfunc NewAddMemberRequest(cur realm, addr address, tier string, portfolio string) dao.ProposalRequest {\n\t// Reject a non-bech32 address at proposal-build time (see AddMember): keeps\n\t// an unauthenticable / injection-bearing key out of the member store.\n\tif !addr.IsValid() {\n\t\tpanic(\"invalid member address: \" + addr.String())\n\t}\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\t// Add the member first, spend the proposer's invitation point second.\n\t\t//\n\t\t// SetMember RETURNS an error when the address is already a member --\n\t\t// easy to reach, since AddMember enrols T3 members directly. A returned\n\t\t// executor error does not revert: ExecuteOrRejectProposal marks the\n\t\t// proposal rejected and the transaction still commits. So with the old\n\t\t// order the point was spent and the member was not added.\n\t\t//\n\t\t// RemoveInvitationPoint can still fail, despite the build-time check\n\t\t// above: it panics at zero, and AddMember spends points from this same\n\t\t// captured member, so a proposer can drain their own between creation\n\t\t// and execution. That panics and reverts, which loses nothing.\n\t\tif err := memberstore.Get(0, cur).SetMember(tier, addr, memberByTier(tier)); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tmember.RemoveInvitationPoint()\n\n\t\treturn nil\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, sanitize.InlineText(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\t// Fail on an unknown destination tier before touching the member.\n\t\t//\n\t\t// Everything that can be checked before a write is checked here, and\n\t\t// RETURNS. A returned executor error rejects the proposal and closes it,\n\t\t// and since nothing has been mutated yet that commits nothing.\n\t\t//\n\t\t// After RemoveMember the policy inverts: a return would commit a member\n\t\t// who was removed and not re-added, so everything below panics instead,\n\t\t// which reverts the whole transaction.\n\t\t// A tier dropped from the global table. NewChangeTiersRequest replaces\n\t\t// that table wholesale, so a proposal listing only T1 and T2 removes T3\n\t\t// for good. memberByTier would not notice: it switches on the constant\n\t\t// and ignores GetTier's ok, so the promotion would succeed and hand the\n\t\t// member zero invitation points. Untested -- reaching it needs a passed\n\t\t// tier-change proposal, which rewrites state every other test shares.\n\t\tif _, ok := memberstore.GetTier(toTier); !ok {\n\t\t\treturn ufmt.Errorf(\"unknown destination tier: %s\", toTier)\n\t\t}\n\n\t\tmbt := memberstore.Get(0, cur)\n\n\t\t// SetMember consults the store's own index, not the global tier table\n\t\t// above: DeleteAll empties the buckets and leaves the definitions.\n\t\tif !mbt.Has(toTier) {\n\t\t\treturn ufmt.Errorf(\"destination tier is missing from the member store: %s\", toTier)\n\t\t}\n\n\t\tprevTier := mbt.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\tif err := mbt.SetMember(toTier, addr, memberByTier(toTier)); err != nil {\n\t\t\t// Unreachable: both of SetMember's failures are ruled out above.\n\t\t\t// Panicking is what makes it safe to be wrong about that.\n\t\t\tpanic(\"promotion failed after removal: \" + err.Error())\n\t\t}\n\n\t\treturn nil\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\t// A foreign impl renders a Payment line no Banker will process, so it would\n\t// fail only at execution, after the vote. Rejected here so the proposer\n\t// sees it, not the voters. See IsCanonicalPayment.\n\tif !trs_pkg.IsCanonicalPayment(payment) {\n\t\tpanic(\"payment must be built by treasury.NewCoinsPayment or treasury.NewGRC20Payment\")\n\t}\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\t// Both are caller-supplied and land in the same body as the Payment line a\n\t// member votes on, which render.gno emits raw. InlineText folds newlines to\n\t// spaces, so neither can start a line of its own and forge that label.\n\t// Clamp before escaping, per clamp.gno.\n\tsafeReason := sanitize.InlineText(clampField(reason, maxRenderedReason))\n\tsafePayment := sanitize.InlineText(clampField(payment.String(), maxRenderedPayment))\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\tsafeReason,\n\t\t\tsafePayment,\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\tsafeReason,\n\t\t\tsafePayment,\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\t// Copied for the same reason NewCoinsPayment copies its coins: the bullet\n\t// list below is rendered once at creation, but the executor closure\n\t// re-reads the slice at execution, so the caller could otherwise install a\n\t// token set the board never saw.\n\tkeys := make([]string, len(newTokenKeys))\n\tcopy(keys, newTokenKeys)\n\n\tcb := func(cur realm) error {\n\t\treturn panictoerr.PanicToError(func() {\n\t\t\t// NOTE:: Consider checking if the keys are already registered\n\t\t\t// in the grc20reg before updating the treasury tokens keys.\n\t\t\ttreasury.SetTokenKeys(cross(cur), keys)\n\t\t})\n\t}\n\n\tbulletList := md.BulletList(keys)\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/v0\"\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/nt/bptree/pager/v0\"\n\t\"gno.land/p/nt/markdown/sanitize/v0\"\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/memberstore/v0)\\n\"\n\tout += \"## Proposals\\n\"\n\tpage, perr := ren.pssPager.GetPageByPath(url)\n\tif perr != nil {\n\t\t// A query url.Parse rejects (e.g. a control byte) is the caller's own\n\t\t// malformed input; render the default first page instead of aborting\n\t\t// this read-only render. ParseQuery already tolerates bad page/size\n\t\t// values, so every currently-working input is unaffected.\n\t\tpage = ren.pssPager.GetPage(1)\n\t}\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\t// err echoes the caller's raw pid segment (strconv quotes it but leaves\n\t\t// markdown/HTML metachars); escape+clamp it like every other untrusted\n\t\t// slot on this page, since Render is reachable unauthenticated.\n\t\treturn ufmt.Sprintf(\"# Error: Invalid proposal ID format.\\n\\n\\n%s\\n\\n\", md.EscapeText(clampField(err.Error(), maxRenderedError)))\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\t// pid is user-supplied and the proposal itself lives on the proxy, so\n\t// GetProposal can succeed for a proposal this implementation has no status\n\t// for — the same post-upgrade case PreExecuteProposal guards (statuses live\n\t// on the instance, so a fresh instance has none). Without this the page\n\t// panics at ps.String below. renderProposalListItem needs no such guard:\n\t// its pids are iterated out of pss itself, so a status always exists.\n\tps := d.pss.GetStatus(dao.ProposalID(pid))\n\tif ps == nil {\n\t\treturn ufmt.Sprintf(\"# Proposal #%v not available\\n\\nThis proposal was not created by the current govDAO implementation, so it has no voting status here.\", pid)\n\t}\n\n\tout := ufmt.Sprintf(\"## Prop #%v - %v\\n\", pid, md.EscapeText(clampField(p.Title(), maxRenderedTitle)))\n\tout += \"Author: \" + tryResolveAddr(p.Author()) + \"\\n\\n\"\n\n\t// The description and the executor metadata are proposer-supplied\n\t// markdown (any realm can build a ProposalRequest, and impl/v0's builders\n\t// stay usable under impl/v1). BlockRich keeps their markdown but escapes\n\t// HTML-block openers and the other cross-paragraph constructs, so neither\n\t// can swallow the realm chrome that follows: the metadata, the executor\n\t// realm, the vote stats. A reason ending in \"\u003c!--\" would otherwise hide\n\t// all of it.\n\tout += sanitize.BlockRich(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\n`, sanitize.BlockRich(p.ExecutorString()))\n\t}\n\n\t// Disclosed independently of the description: this names the realm whose\n\t// code runs if the proposal passes. Sharing the gate above hid it for every\n\t// proposal built with an empty executor description — 16 call sites across\n\t// 7 production realms, nearly all r/sys/* governance actions.\n\t//\n\t// Escaped, not trusted: CreationRealm() is dispatched through the public\n\t// dao.Executor interface, so only SimpleExecutor's value is VM-captured;\n\t// a third-party executor returns any string it likes.\n\t// Guarded on the sanitized value, not the raw one: InlineCode returns \"\"\n\t// for input that strips to nothing (bidi/zero-width only), and TrimSpace\n\t// catches plain whitespace, so neither prints a label with nothing after\n\t// it. This is tidiness, not a boundary: a zero-width/space mixture still\n\t// renders an empty-looking span, and a hostile executor can always return\n\t// a plausible-looking lie. The security property is the escaping below.\n\tif cr := sanitize.InlineCode(strings.TrimSpace(clampField(p.ExecutorCreationRealm(), maxRenderedRealm))); cr != \"\" {\n\t\tout += ufmt.Sprintf(\"Executor created in: %s\\n\", cr)\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\t// err echoes the caller's raw pid segment (strconv quotes it but leaves\n\t\t// markdown/HTML metachars); escape+clamp it like every other untrusted\n\t\t// slot on this page, since Render is reachable unauthenticated.\n\t\treturn ufmt.Sprintf(\"# Error: Invalid proposal ID format.\\n\\n\\n%s\\n\\n\", md.EscapeText(clampField(err.Error(), maxRenderedError)))\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(clampField(p.Title(), maxRenderedTitle)), 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\t// err echoes the caller's raw pid segment (strconv quotes it but leaves\n\t\t// markdown/HTML metachars); escape+clamp it like every other untrusted\n\t\t// slot on this page, since Render is reachable unauthenticated.\n\t\treturn ufmt.Sprintf(\"# Error: Invalid proposal ID format.\\n\\n\\n%s\\n\\n\", md.EscapeText(clampField(err.Error(), maxRenderedError)))\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\n// tryResolveAddr renders the author/voter as a username link. RenderLink\n// interpolates the username raw into \"[@name](/u/name)\" (r/sys/users), so this\n// is markdown-safe ONLY because r/sys/users validateName restricts names to\n// ^[a-z][a-z0-9]*([_-][a-z0-9]+)*$ (max 64) — no markdown/HTML metachar. If that\n// charset ever loosens, this line and writeVotes in types.gno need escaping.\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":"render_error_paths_filetest.gno","body":"// PKGPATH: gno.land/r/test/rendererr\npackage rendererr\n\n// Render is reachable unauthenticated via vm/qrender with an attacker-chosen\n// path. Two pre-existing defects on that path are covered here:\n//   - the \"invalid proposal id\" error echoed the raw pid segment (strconv\n//     quotes it but leaves markdown/HTML metachars), so a crafted pid injected\n//     inline markup into a govDAO-branded page;\n//   - a control byte in the query string made the pager's MustGetPageByPath\n//     panic, aborting the landing-page render.\n// Both are now escaped / degraded-gracefully.\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/impl/v1\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\n)\n\nconst user address = \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"\n\nfunc init(cur realm) {\n\tmemberstore.Get(0, cur).DeleteAll()\n\tmemberstore.Get(0, cur).SetTier(memberstore.T1)\n\tmemberstore.Get(0, cur).SetMember(memberstore.T1, user, memberstore.NewMember(3))\n\tdao.UpdateImpl(cross(cur), dao.NewUpdateRequest(impl.NewGovDAO(), nil))\n}\n\nfunc main(cur realm) {\n\ttesting.SetOriginCaller(user)\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// 1. A non-numeric pid whose segment carries an inline-markdown payload.\n\t// No '/', so it routes to {pid}; ParseInt fails and the error is escaped.\n\tpout := dao.Render(cross(cur), \"99[pwn](evil.example)\")\n\tprintln(\"payload is escaped, not a live link:\",\n\t\tstrings.Contains(pout, `\\[pwn\\]`) \u0026\u0026 !strings.Contains(pout, \"[pwn](evil.example)\"))\n\tprintln(\"still shows the invalid-id message:\",\n\t\tstrings.Contains(pout, \"Invalid proposal ID format\"))\n\n\t// Same on the /votes route.\n\tvout := dao.Render(cross(cur), \"88[x](evil.example)/votes\")\n\tprintln(\"votes route escapes it too:\",\n\t\t!strings.Contains(vout, \"[x](evil.example)\"))\n\n\t// 2. A control byte in the query — url.Parse rejects it. The landing page\n\t// must degrade to page 1, not panic.\n\tlout := dao.Render(cross(cur), \"?\\x01\")\n\tprintln(\"control-byte query renders instead of panicking:\",\n\t\tstrings.Contains(lout, \"# GovDAO\"))\n}\n\n// Output:\n// payload is escaped, not a live link: true\n// still shows the invalid-id message: true\n// votes route escapes it too: true\n// control-byte query renders instead of panicking: true\n"},{"name":"stringify_proposal_00_filetest.gno","body":"// PKGPATH: gno.land/r/test\npackage test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/impl/v1\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\n)\n\nvar (\n\ttestUser = testutils.TestAddress(\"test\")\n)\n\n// StringifyProposal carries its own copy of the disclosure expression, separate\n// from the one in render.gno. It has no production caller today, but it is\n// exported, and an untested copy of a security-relevant expression is free to\n// drift from the tested one. This executor pins it: CreationRealm is reached\n// through the public interface, so it can return anything, and the run of two\n// backticks closes a narrower fence.\ntype hostileExec struct{}\n\nfunc (e *hostileExec) Execute(cur realm) error { return nil }\n\nfunc (e *hostileExec) String() string { return \"\" }\n\n// Padded on both ends as well as fenced: without the TrimSpace, InlineCode\n// pads the fence to protect the leading and trailing spaces, and the expected\n// span below no longer matches. That makes one payload cover both the escaping\n// and the trim.\nfunc (e *hostileExec) CreationRealm() string { return \"  gno.land/r/evil`` **INJECTED**  \" }\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\tdefault:\n\t\tpanic(\"unsupported tier: \" + tier)\n\t}\n}\n\nfunc init(cur realm) {\n\t// Load members for testing\n\tmstore := memberstore.Get(0, cur)\n\tmstore.DeleteAll()\n\tmstore.SetTier(memberstore.T1)\n\tmstore.SetMember(memberstore.T1, testUser, memberByTier(memberstore.T1))\n\n\t// Set up the DAO implementation using proper constructor\n\tgovDAO := impl.NewGovDAO()\n\tdao.UpdateImpl(cross(cur), dao.NewUpdateRequest(govDAO, []string{\"gno.land/r/test\", \"gno.land/r/gov/dao/impl/v1\"}))\n}\n\nfunc main(cur realm) {\n\t// Create an executor in a specific realm to test creation realm tracking\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/template/contract\"))\n\texecutor := dao.NewSimpleExecutor(0, cur, func(realm) error { return nil }, \"Test executor description\")\n\n\t// Create a proposal request\n\tproposalRequest := dao.NewProposalRequest(\n\t\t\"Test Proposal Title\",\n\t\t\"This is a test proposal description to verify StringifyProposal works correctly.\",\n\t\texecutor,\n\t)\n\n\t// Switch to user realm to create the proposal\n\ttesting.SetOriginCaller(testUser)\n\ttesting.SetRealm(testing.NewUserRealm(testUser))\n\tpid := dao.MustCreateProposal(cross(cur), proposalRequest)\n\n\t// Get the proposal and test the core functionality\n\tprop := dao.MustGetProposal(pid)\n\n\t// Test that executor string is captured correctly\n\tprintln(\"Executor string:\", prop.ExecutorString())\n\n\t// Test that executor creation realm is captured correctly\n\tprintln(\"Executor creation realm:\", prop.ExecutorCreationRealm())\n\n\t// Stringify\n\tprintln(\"----\")\n\tprintln(impl.StringifyProposal(prop))\n\n\t// The same disclosure, through a hostile executor.\n\thpid := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(\n\t\t\"Hostile\", \"creation realm attacks the code span\", \u0026hostileExec{}))\n\thout := impl.StringifyProposal(dao.MustGetProposal(hpid))\n\n\tprintln(\"stringify widens the fence too:\",\n\t\tstrings.Contains(hout, \"```gno.land/r/evil`` **INJECTED**```\"))\n\tprintln(\"and no bold escapes it:\", !strings.Contains(hout, \"\\n**INJECTED**\"))\n}\n\n// Output:\n// Executor string: Test executor description\n// Executor creation realm: gno.land/r/template/contract\n// ----\n//\n// ### Title: Test Proposal Title\n//\n// ### Proposed by: g1w3jhxazlta047h6lta047h6lta047h6lwmjv0n\n//\n//\n//\n// This is a test proposal description to verify StringifyProposal works correctly.\n//\n//\n//\n// This proposal contains the following metadata:\n//\n//\n//\n// Test executor description\n//\n//\n//\n// Executor created in: `gno.land/r/template/contract`\n//\n// stringify widens the fence too: true\n// and no bold escapes it: true\n"},{"name":"title_clamp_filetest.gno","body":"// PKGPATH: gno.land/r/test/titleclamp\npackage titleclamp\n\n// A proposal title is attacker-chosen (by a member) and is escaped on two\n// pages: the proposal page and the list page. Escaping costs about 6,990 gas a\n// byte, and the list page escapes one title per proposal shown, so an oversized\n// title priced the whole list out of the query cap: five proposals with 90 KB\n// titles cost 3,172,507,361 gas against a 3,000,000,000 cap. Clamping the title\n// before it is escaped brings the same five to 66,871,916.\n//\n// Runs in its own realm because it creates proposals, and the unit tests in the\n// impl package assert hard-coded proposal ids against shared state.\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/impl/v1\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\n)\n\nconst user address = \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"\n\nfunc init(cur realm) {\n\tmemberstore.Get(0, cur).DeleteAll()\n\tmemberstore.Get(0, cur).SetTier(memberstore.T1)\n\tmemberstore.Get(0, cur).SetMember(memberstore.T1, user, memberstore.NewMember(3))\n\tdao.UpdateImpl(cross(cur), dao.NewUpdateRequest(impl.NewGovDAO(), nil))\n}\n\nfunc main(cur realm) {\n\ttesting.SetOriginCaller(user)\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tshort := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(\n\t\t\"An ordinary title\", \"d\", nil))\n\tlong := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(\n\t\tstrings.Repeat(\"t\", 50000), \"d\", nil))\n\n\t// An ordinary title is untouched. Real titles in examples/ are about 40\n\t// bytes, so the bound never reaches them.\n\tsout := dao.Render(cross(cur), short.String())\n\tprintln(\"an ordinary title is left alone:\",\n\t\tstrings.Contains(sout, \"An ordinary title\") \u0026\u0026 !strings.Contains(sout, \"… truncated\"))\n\n\t// The proposal page cuts the title before escaping it. Asserting the marker\n\t// rather than a length: escaping first and cutting second would also\n\t// produce a short page, so length alone cannot tell the two apart.\n\tpout := dao.Render(cross(cur), long.String())\n\tprintln(\"the proposal page clamps a huge title:\",\n\t\tstrings.Contains(pout, \"… truncated\") \u0026\u0026 len(pout) \u003c 4000)\n\n\t// The list page escapes one title per proposal it shows, so it is the page\n\t// the bound actually protects.\n\tlout := dao.Render(cross(cur), \"\")\n\tprintln(\"the list page clamps it too:\",\n\t\tstrings.Contains(lout, \"… truncated\") \u0026\u0026 len(lout) \u003c 4000)\n}\n\n// Output:\n// an ordinary title is left alone: true\n// the proposal page clamps a huge title: true\n// the list page clamps it too: true\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/markdown/sanitize/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\n)\n\ntype Law struct {\n\tSupermajority float64\n}\n\nfunc NewLaw(supermajority float64) Law {\n\treturn Law{Supermajority: supermajority}\n}\n\n// String has a value receiver so that a Law value (as NewChangeLawRequest\n// formats it) prints its threshold, not \"(unhandled)\".\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\t// DeniedReason is \"execution failed: \" + err.Error() from the\n\t\t\t// proposal's executor callback, which any third-party realm can\n\t\t\t// supply. Raw, it wrote attacker-controlled markdown directly beneath\n\t\t\t// the \"PROPOSAL HAS BEEN DENIED\" line. InlineText folds newlines, so\n\t\t\t// the reason cannot leave the REASON: line, and escapes emphasis and\n\t\t\t// raw HTML.\n\t\t\tsb.WriteString(sanitize.InlineText(clampField(ps.DeniedReason, maxRenderedReason)))\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`, sanitize.InlineText(p.Title()), p.Author(), sanitize.BlockRich(p.Description()))\n\n\tif p.ExecutorString() != \"\" {\n\t\tout += ufmt.Sprintf(`\nThis proposal contains the following metadata:\n\n%s\n`, sanitize.BlockRich(p.ExecutorString()))\n\t}\n\n\t// Disclosed independently of the metadata string, and escaped, for the\n\t// same reasons as render.renderProposalPage.\n\tif cr := sanitize.InlineCode(strings.TrimSpace(clampField(p.ExecutorCreationRealm(), maxRenderedRealm))); cr != \"\" {\n\t\tout += ufmt.Sprintf(\"\\nExecutor created in: %s\\n\", cr)\n\t}\n\n\treturn out\n}\n"},{"name":"voter_tiers_test.gno","body":"package impl\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\timplv0 \"gno.land/r/gov/dao/impl/v0\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\n)\n\n// impl/v1 enforces impl/v0's tier filters with v0's rules, and refuses any\n// filter it cannot enforce instead of treating it as unrestricted.\nfunc TestVoterTiers(t *testing.T) {\n\tall := \"T1,T2,T3\"\n\tcases := []struct {\n\t\tname   string\n\t\tfilter any\n\t\twant   string // \"\" means refused\n\t}{\n\t\t{\"no filter\", nil, all},\n\t\t{\"v1 T1\", NewFilterByTier(memberstore.T1), \"T1\"},\n\t\t{\"v1 T2\", NewFilterByTier(memberstore.T2), \"T1,T2\"},\n\t\t{\"v1 T3\", NewFilterByTier(memberstore.T3), all},\n\t\t{\"v0 T1\", implv0.NewFilterByTier(memberstore.T1), \"T1\"},\n\t\t{\"v0 T2\", implv0.NewFilterByTier(memberstore.T2), \"T1,T2\"},\n\t\t{\"v0 T3\", implv0.NewFilterByTier(memberstore.T3), all},\n\t\t{\"unknown tier\", NewFilterByTier(\"T9\"), \"\"},\n\t\t{\"unknown v0 tier\", implv0.NewFilterByTier(\"\"), \"\"},\n\t\t{\"foreign type\", struct{ Tier string }{\"T1\"}, \"\"},\n\t\t{\"pointer to v1 filter\", \u0026FilterByTier{Tier: \"T1\"}, \"\"},\n\t}\n\tfor _, c := range cases {\n\t\tgot, err := voterTiers(c.filter)\n\t\tif c.want == \"\" {\n\t\t\tuassert.ErrorIs(t, err, ErrUnsupportedFilter, c.name)\n\t\t\tcontinue\n\t\t}\n\t\tuassert.NoError(t, err, c.name)\n\t\tuassert.Equal(t, c.want, strings.Join(got, \",\"), c.name)\n\t}\n}\n"},{"name":"z_addr_validation_test.gno","body":"package impl\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/urequire/v0\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\n)\n\n// Member addresses arrive as raw MsgCall strings (the VM does not validate an\n// `address` arg), so a non-bech32 value — blank, oversized, or carrying\n// markdown/pipe/HTML metachars — would otherwise be stored and then injected\n// into the members render table. Both admission points reject it, matching the\n// InitWithUsers precedent.\nfunc TestNewAddMemberRequestRejectsInvalidAddress(cur realm, t *testing.T) {\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\n\tfor _, bad := range []string{\"evil|**pwn**\", \"\", \"not-an-address\"} {\n\t\turequire.PanicsWithMessage(t, cur,\n\t\t\t\"invalid member address: \"+bad,\n\t\t\tfunc() {\n\t\t\t\tNewAddMemberRequest(cur, address(bad), memberstore.T2, \"portfolio\")\n\t\t\t})\n\t}\n}\n"},{"name":"z_denied_reason_store_test.gno","body":"package impl\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\n)\n\n// Named z_* so it sorts after govdao_test.gno, whose TestUpgradeDaoImplementation\n// asserts a hard-coded proposal id. This builds its own GovDAO instance and\n// registers a status directly, so it never touches the shared one.\n\ntype hugeErrExecutor struct{ msg string }\n\nfunc (e *hugeErrExecutor) Execute(cur realm) error { return errors.New(e.msg) }\n\nfunc (e *hugeErrExecutor) String() string { return \"\" }\n\nfunc (e *hugeErrExecutor) CreationRealm() string { return \"gno.land/r/test/hugeerr\" }\n\n// The denial reason is stored, not just shown. It is \"execution failed: \" plus\n// whatever error the proposal's executor returned, and the executor is\n// third-party code. Whoever executes the proposal pays the storage deposit for\n// it, and that need not be the person who wrote the executor.\n//\n// This has to read the stored value rather than the rendered page. The render\n// clamps to the same bound, so a page rendered from an unclamped store looks\n// exactly like one rendered from a clamped store — testing through Render would\n// pass whether or not the write is bounded.\nfunc TestDeniedReasonIsClampedBeforeStoring(cur realm, t *testing.T) {\n\tg := NewGovDAO()\n\tpid := dao.ProposalID(987654)\n\tg.pss.Set(pid.String(), newProposalStatus([]string{memberstore.T1}))\n\n\tg.ExecuteProposal(0, cur, pid, \u0026hugeErrExecutor{msg: strings.Repeat(\"E\", 50000)})\n\n\tstored := g.pss.GetStatus(pid).DeniedReason\n\tuassert.True(t, len(stored) \u003c maxRenderedReason+64,\n\t\t\"the realm must not store a reason larger than it can display\")\n\tuassert.True(t, strings.Contains(stored, \"… truncated\"),\n\t\t\"a stored reason that was cut must say so\")\n\tuassert.True(t, strings.HasPrefix(stored, \"execution failed: E\"),\n\t\t\"the prefix and the start of the error must survive\")\n}\n"},{"name":"z_treasury_payment_request_test.gno","body":"package impl\n\nimport (\n\t\"testing\"\n\n\ttrs_pkg \"gno.land/p/nt/treasury/v1\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\n// Named z_* for the same reason as z_upgrade_request_test.gno: govdao_test.gno\n// asserts hard-coded proposal ids, and this test creates no proposal.\n\n// foreignPayment satisfies trs_pkg.Payment without being one of treasury's own\n// impls. BankerID names a banker the treasury really has registered, and\n// String says whatever this realm likes.\ntype foreignPayment struct{}\n\nfunc (foreignPayment) BankerID() string { return \"Coins\" }\n\nfunc (foreignPayment) String() string { return \"42ugnot to g1nobody\" }\n\n// The immutability NewCoinsPayment provides is a property of the concrete\n// type, not of the Payment interface: a foreign impl renders a payment line\n// members vote on and then fails CoinsBanker.Send's type assertion at\n// execution, spending a whole vote cycle on a proposal that can never\n// execute. Rejected at construction so the proposer sees it, not the voters.\nfunc TestTreasuryPaymentRequestRejectsForeignPayment(cur realm, t *testing.T) {\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\n\t// PanicsWithMessage, not AbortsWithMessage: same-realm call, so the guard\n\t// raises a plain panic rather than a cross-realm abort.\n\turequire.PanicsWithMessage(t, cur,\n\t\t\"payment must be built by treasury.NewCoinsPayment or treasury.NewGRC20Payment\",\n\t\tfunc() {\n\t\t\tNewTreasuryPaymentRequest(cur, foreignPayment{}, \"a reason\")\n\t\t})\n}\n\n// The check must not reject the real thing. GRC20 is used here because the\n// Coins banker is the one govdao_test.gno's shared state leaves funded; both\n// are canonical, and only the canonicality gate is under test.\nfunc TestTreasuryPaymentRequestAcceptsCanonicalPayment(cur realm, t *testing.T) {\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\n\turequire.NotPanics(t, cur, func() {\n\t\tNewTreasuryPaymentRequest(cur, trs_pkg.NewGRC20Payment(\"TOK\", 1, m1), \"a reason\")\n\t})\n}\n"},{"name":"z_unknown_proposal_test.gno","body":"package impl\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/urequire/v0\"\n\t\"gno.land/r/gov/dao\"\n)\n\n// Named z_* so it sorts after govdao_test.gno, whose TestUpgradeDaoImplementation\n// swaps the DAO implementation and asserts a hard-coded proposal id. This test\n// creates no proposal, so it is safe to run against the shared state.\n\n// A proposal id this implementation has no status for — an unknown id, or a\n// proposal that outlived the GovDAO instance that created it (statuses live on\n// the instance). The nil deref this used to hit surfaced as \"runtime error:\n// nil pointer dereference\", which reads like a VM fault rather than a bad\n// request.\n//\n// Note what this does NOT assert: that the proposal becomes rejectable.\n// dao.executeProposal panics on ANY error from PreExecuteProposal, ahead of\n// its execErrorRejects branch, so the guard buys a named error and nothing\n// more.\nfunc TestExecuteUnknownProposalNamesTheError(cur realm, t *testing.T) {\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\n\turequire.AbortsWithMessage(t, cur, \"proposal not found\", func() {\n\t\tdao.ExecuteProposal(cross(cur), dao.ProposalID(999999))\n\t})\n}\n"},{"name":"z_upgrade_request_test.gno","body":"package impl\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/urequire/v0\"\n\t\"gno.land/r/gov/dao\"\n)\n\n// Named z_* so it sorts after govdao_test.gno, whose TestUpgradeDaoImplementation\n// swaps the DAO implementation and asserts a hard-coded proposal id. This test\n// creates no proposal, so it is safe to run against the shared state.\n\n// An empty realmPkg would be stored verbatim as an allowlist entry, and\n// InAllowedDAOs compares by exact string against a caller's PkgPath() — which\n// is \"\" for a user realm. It would also render as a blank name in the grant\n// sentence, so the proposal would not say who it was empowering. Rejected at\n// proposal-construction time so the mistake surfaces to the author rather than\n// at execution. UpdateImpl rejects it too; see r/gov/dao/allowlist_test.gno.\nfunc TestUpgradeRequestRejectsBlankRealmPkg(cur realm, t *testing.T) {\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\n\tfor _, padded := range []string{\" gno.land/r/x\", \"gno.land/r/x \"} {\n\t\turequire.PanicsWithMessage(t, cur,\n\t\t\t\"realmPkg must not have leading or trailing spaces\",\n\t\t\tfunc() {\n\t\t\t\tNewUpgradeDaoImplRequest(cur, \u0026mockDAO{}, padded, \"reason\")\n\t\t\t})\n\t}\n\n\tfor _, blank := range []string{\"\", \"   \"} {\n\t\t// PanicsWithMessage, not AbortsWithMessage: this is a same-realm call,\n\t\t// so the guard raises a plain panic rather than a cross-realm abort.\n\t\turequire.PanicsWithMessage(t, cur,\n\t\t\t\"realmPkg must be the realm path being granted govDAO authority\",\n\t\t\tfunc() {\n\t\t\t\tNewUpgradeDaoImplRequest(cur, \u0026mockDAO{}, blank, \"reason\")\n\t\t\t})\n\t}\n}\n\ntype mockDAO struct{}\n\nfunc (d *mockDAO) PreCreateProposal(_ int, rlm realm, r dao.ProposalRequest) (address, error) {\n\treturn m1, nil\n}\n\nfunc (d *mockDAO) PostCreateProposal(_ int, rlm realm, r dao.ProposalRequest, pid dao.ProposalID) {}\n\nfunc (d *mockDAO) VoteOnProposal(_ int, rlm realm, r dao.VoteRequest) error { return nil }\n\nfunc (d *mockDAO) PreExecuteProposal(_ int, rlm realm, pid dao.ProposalID) (bool, error) {\n\treturn true, nil\n}\n\nfunc (d *mockDAO) ExecuteProposal(_ int, rlm realm, pid dao.ProposalID, e dao.Executor) error {\n\treturn nil\n}\n\nfunc (d *mockDAO) Render(cur realm, pkgpath string, path string) string { return \"\" }\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"kyJdCR3aSZJjGWo7l03ZjVC3/s0r4RpqYGuIekVk0twZV47y1EMi9+gdAs5+TFz9m598hF7+I5OM14QaIQMUvQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1g73v2anukg4ej7axwqpthsatzrxjsh0wk797da","package":{"name":"loader","path":"gno.land/r/gov/dao/loader/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/gov/dao/loader/v0\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1g73v2anukg4ej7axwqpthsatzrxjsh0wk797da\"\n"},{"name":"loader.gno","body":"// loader.gno initialises the govDAO v0 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/impl/v0\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"4ATci3I4lDqOk+Bw97nk8KzO7mHgb8WP3ySg4QnVToNUqhlbr1Lqjyu70lUQ/vhmy/On/XDED2+PE9rFvkae6Q=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"test","path":"gno.land/r/gov/dao/treasury/test/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/gov/dao/treasury/test/v0\"\ngno = \"0.9\"\n"},{"name":"treasury_test.gno","body":"package test\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"chain/runtime/unsafe\"\n\t\"sort\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/fqname/v0\"\n\t\"gno.land/p/nt/grc20/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/p/nt/testutils/v0\"\n\ttrs_pkg \"gno.land/p/nt/treasury/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/impl/v0\"\n\t\"gno.land/r/gov/dao/treasury/v0\"\n\t\"gno.land/r/nt/grc20reg/v0\"\n)\n\n// cur is a zero-value realm used as a placeholder when forwarding to\n// uassert/urequire dispatch helpers that gained an `rlm realm` param.\n// These tests pass `func()` callbacks (no crossing inside the callback),\n// so rlm is ignored — a nil realm here is safe.\nvar cur realm\nvar nextTokenID seqid.ID\nvar (\n\tuser1Addr       = testutils.TestAddress(\"g1user1\")\n\tuser2Addr       = testutils.TestAddress(\"g1user2\")\n\ttreasuryAddr    = chain.PackageAddress(\"gno.land/r/gov/dao/treasury/v0\")\n\tallowedRealm    = testing.NewCodeRealm(\"gno.land/r/test/allowed\")\n\tnotAllowedRealm = testing.NewCodeRealm(\"gno.land/r/test/notallowed\")\n\tmintAmount      = int64(1000)\n)\n\n// Define a dummy trs_pkg.Payment type for testing purposes.\ntype dummyPayment struct {\n\tbankerID string\n\tstr      string\n}\n\nvar _ trs_pkg.Payment = (*dummyPayment)(nil)\n\nfunc (dp *dummyPayment) BankerID() string { return dp.bankerID }\nfunc (dp *dummyPayment) String() string   { return dp.str }\n\nfunc init(cur realm) {\n\t// Register allowed Realm path.\n\tdao.UpdateImpl(cross(cur), dao.NewUpdateRequest(impl.NewGovDAO(), []string{allowedRealm.PkgPath()}))\n}\n\nfunc ugnotCoins(t *testing.T, amount int64) chain.Coins {\n\tt.Helper()\n\n\t// Create a new coin with the ugnot denomination.\n\treturn chain.NewCoins(chain.NewCoin(\"ugnot\", amount))\n}\n\nfunc ugnotBalance(t *testing.T, addr address) int64 {\n\tt.Helper()\n\n\t// Get the balance of ugnot coins for the given address.\n\tbanker_ := banker.NewReadonlyBanker()\n\tcoins := banker_.GetCoins(addr)\n\n\treturn coins.AmountOf(\"ugnot\")\n}\n\n// Define a keyedToken type to hold the token and its key.\ntype keyedToken struct {\n\tkey   string\n\ttoken *grc20.Token\n}\n\nfunc registerGRC20Tokens(_ int, rlm realm, t *testing.T, tokenNames []string, toMint address) []keyedToken {\n\tt.Helper()\n\n\tvar (\n\t\tkeyedTokens = make([]keyedToken, 0, len(tokenNames))\n\t\tkeys        = make([]string, 0, len(tokenNames))\n\t)\n\n\tfor _, name := range tokenNames {\n\t\t// Create the token.\n\t\tsymbol := strings.ToUpper(name)\n\t\ttoken, ledger := grc20.NewToken(name, symbol, 0, nextTokenID.Next(), rlm)\n\n\t\t// Register the token.\n\t\tgrc20reg.Register(cross(rlm), token, symbol)\n\n\t\t// Mint tokens to the specified address.\n\t\tledger.Mint(toMint, mintAmount)\n\n\t\t// Add the token and key to the lists.\n\t\tkey := fqname.Construct(unsafe.CurrentRealm().PkgPath(), symbol)\n\t\tkeyedTokens = append(keyedTokens, keyedToken{key: key, token: token})\n\t\tkeys = append(keys, key)\n\t}\n\n\t// Set the token keys in the treasury.\n\ttreasury.SetTokenKeys(cross(rlm), keys)\n\n\treturn keyedTokens\n}\n\nfunc TestAllowedDAOs(cur realm, t *testing.T) {\n\t// Set the current Realm to the not allowed one.\n\ttesting.SetRealm(notAllowedRealm)\n\n\t// Define a dummy payment to test sending.\n\tdummyP := \u0026dummyPayment{bankerID: \"Dummy\"}\n\n\t// Try to send, it should abort because the Realm is not allowed.\n\tuassert.AbortsWithMessage(\n\t\tt, cur,\n\t\t\"this Realm is not allowed to send payment: \"+notAllowedRealm.PkgPath(),\n\t\tfunc() { treasury.Send(cross(cur), dummyP) },\n\t)\n\n\t// Set the current Realm to the allowed one.\n\ttesting.SetRealm(allowedRealm)\n\n\t// Try to send, it should not abort because the Realm is allowed,\n\t// but because the dummy banker ID is not registered.\n\tuassert.AbortsWithMessage(\n\t\tt, cur,\n\t\t\"banker not found: \"+dummyP.BankerID(),\n\t\tfunc() { treasury.Send(cross(cur), dummyP) },\n\t)\n}\n\nfunc TestRegisteredBankers(t *testing.T) {\n\t// Set the current Realm to the allowed one.\n\ttesting.SetRealm(allowedRealm)\n\n\t// Define the expected banker IDs.\n\texpectedBankerIDs := []string{\n\t\ttrs_pkg.CoinsBanker{}.ID(),\n\t\ttrs_pkg.GRC20Banker{}.ID(),\n\t}\n\n\t// Get the registered bankers from the treasury and compare their lengths.\n\tregistered := treasury.ListBankerIDs()\n\tuassert.Equal(t, len(registered), len(expectedBankerIDs))\n\n\t// The treasury-returned slice is foreign-readonly; copy locally\n\t// before sorting in place.\n\tregisteredBankerIDs := append([]string(nil), registered...)\n\n\t// Sort both slices then compare them.\n\tsort.StringSlice(expectedBankerIDs).Sort()\n\tsort.StringSlice(registeredBankerIDs).Sort()\n\n\tfor i := range expectedBankerIDs {\n\t\tuassert.Equal(t, expectedBankerIDs[i], registeredBankerIDs[i])\n\t}\n\n\t// Test HasBanker method.\n\tfor _, bankerID := range expectedBankerIDs {\n\t\tuassert.True(t, treasury.HasBanker(bankerID))\n\t}\n\tuassert.False(t, treasury.HasBanker(\"UnknownBankerID\"))\n\n\t// Test Address method.\n\tfor _, bankerID := range expectedBankerIDs {\n\t\t// The two bankers used for now should have the treasury Realm address.\n\t\tuassert.Equal(t, treasury.Address(bankerID), treasuryAddr.String())\n\t}\n}\n\nfunc TestSendGRC20Payment(cur realm, t *testing.T) {\n\t// Set the current Realm to the allowed one.\n\ttesting.SetRealm(allowedRealm)\n\n\t// Try to send a GRC20 payment with a not registered token, it should abort.\n\tuassert.AbortsWithMessage(\n\t\tt, cur,\n\t\t\"failed to send payment: GRC20 token not found: UNKNOW\",\n\t\tfunc() {\n\t\t\ttreasury.Send(cross(cur), trs_pkg.NewGRC20Payment(\"UNKNOW\", 100, user1Addr))\n\t\t},\n\t)\n\n\t// Create 3 GRC20 tokens and register them.\n\tkeyedTokens := registerGRC20Tokens(\n\t\t0, cur,\n\t\tt,\n\t\t[]string{\"TestToken0\", \"TestToken1\", \"TestToken2\"},\n\t\ttreasuryAddr,\n\t)\n\n\tconst txAmount = 42\n\n\t// For each token-user pair.\n\tfor i, userAddr := range []address{user1Addr, user2Addr} {\n\t\tfor _, keyed := range keyedTokens {\n\t\t\t// Check that the treasury has the expected balance before sending.\n\t\t\tuassert.Equal(t, keyed.token.BalanceOf(treasuryAddr), mintAmount-int64(txAmount*i))\n\n\t\t\t// Check that the user has no balance before sending.\n\t\t\tuassert.Equal(t, keyed.token.BalanceOf(userAddr), int64(0))\n\n\t\t\t// Try to send a GRC20 payment with a registered token, it should not abort.\n\t\t\tuassert.NotAborts(t, cur, func() {\n\t\t\t\ttreasury.Send(\n\t\t\t\t\tcross(cur),\n\t\t\t\t\ttrs_pkg.NewGRC20Payment(\n\t\t\t\t\t\tkeyed.key,\n\t\t\t\t\t\ttxAmount,\n\t\t\t\t\t\tuserAddr,\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t})\n\n\t\t\t// Check that the user has the expected balance after sending.\n\t\t\tuassert.Equal(t, keyed.token.BalanceOf(userAddr), int64(txAmount))\n\n\t\t\t// Check that the treasury has the expected balance after sending.\n\t\t\tuassert.Equal(t, keyed.token.BalanceOf(treasuryAddr), mintAmount-int64(txAmount*(i+1)))\n\t\t}\n\t}\n\n\t// Get the GRC20Banker ID.\n\tgrc20BankerID := trs_pkg.GRC20Banker{}.ID()\n\n\t// Test Balances method for the GRC20Banker.\n\tbalances := treasury.Balances(grc20BankerID)\n\tuassert.Equal(t, len(balances), len(keyedTokens))\n\n\tcompared := 0\n\tfor _, balance := range balances {\n\t\tfor _, keyed := range keyedTokens {\n\t\t\tif balance.Denom == keyed.key {\n\t\t\t\tuassert.Equal(t, balance.Amount, keyed.token.BalanceOf(treasuryAddr))\n\t\t\t\tcompared++\n\t\t\t}\n\t\t}\n\t}\n\tuassert.Equal(t, compared, len(keyedTokens))\n\n\t// Check the history of the GRC20Banker.\n\thistory := treasury.History(grc20BankerID, 1, 10)\n\tuassert.Equal(t, len(history), 6)\n\n\t// Try to send a dummy payment with the GRC20 banker ID, it should abort.\n\tuassert.AbortsWithMessage(\n\t\tt, cur,\n\t\t\"failed to send payment: invalid payment type\",\n\t\tfunc() {\n\t\t\ttreasury.Send(cross(cur), \u0026dummyPayment{bankerID: grc20BankerID})\n\t\t},\n\t)\n\n\t// Try to send a GRC20 payment without enough balance, it should abort.\n\tuassert.AbortsWithMessage(\n\t\tt, cur,\n\t\t\"failed to send payment: insufficient balance\",\n\t\tfunc() {\n\t\t\ttreasury.Send(\n\t\t\t\tcross(cur),\n\t\t\t\ttrs_pkg.NewGRC20Payment(\n\t\t\t\t\tkeyedTokens[0].key,\n\t\t\t\t\tmintAmount*42, // Try to send more than the treasury has.\n\t\t\t\t\tuser1Addr,\n\t\t\t\t),\n\t\t\t)\n\t\t},\n\t)\n\n\t// Check the history of the GRC20Banker.\n\thistory = treasury.History(grc20BankerID, 1, 10)\n\tuassert.Equal(t, len(history), 6)\n}\n\nfunc TestSendCoinPayment(cur realm, t *testing.T) {\n\t// Set the current Realm to the allowed one.\n\ttesting.SetRealm(allowedRealm)\n\n\t// Issue initial ugnot coins to the treasury address.\n\ttesting.IssueCoins(treasuryAddr, ugnotCoins(t, mintAmount))\n\n\t// Get the CoinsBanker ID.\n\tbankerID := trs_pkg.CoinsBanker{}.ID()\n\n\t// Define helper function to check balances and history.\n\tvar (\n\t\texpectedTreasuryBalance = mintAmount\n\t\texpectedUser1Balance    = int64(0)\n\t\texpectedUser2Balance    = int64(0)\n\t\texpectedHistoryLen      = 0\n\t\tcheckHistoryAndBalances = func() {\n\t\t\tt.Helper()\n\n\t\t\tuassert.Equal(t, ugnotBalance(t, treasuryAddr), expectedTreasuryBalance)\n\t\t\tuassert.Equal(t, ugnotBalance(t, user1Addr), expectedUser1Balance)\n\t\t\tuassert.Equal(t, ugnotBalance(t, user2Addr), expectedUser2Balance)\n\n\t\t\t// Check treasury.Balances returned value.\n\t\t\tbalances := treasury.Balances(bankerID)\n\t\t\tuassert.Equal(t, len(balances), 1)\n\t\t\tuassert.Equal(t, balances[0].Denom, \"ugnot\")\n\t\t\tuassert.Equal(t, balances[0].Amount, expectedTreasuryBalance)\n\n\t\t\t// Check treasury.History returned value.\n\t\t\thistory := treasury.History(bankerID, 1, expectedHistoryLen+1)\n\t\t\tuassert.Equal(t, len(history), expectedHistoryLen)\n\t\t}\n\t)\n\n\t// Check initial balances and history.\n\tcheckHistoryAndBalances()\n\n\tconst txAmount = int64(42)\n\n\t// Treasury send coins.\n\tfor i := int64(0); i \u003c 3; i++ {\n\t\t// Send ugnot coins to user1 and user2.\n\t\tuassert.NotAborts(t, cur, func() {\n\t\t\ttreasury.Send(\n\t\t\t\tcross(cur),\n\t\t\t\ttrs_pkg.NewCoinsPayment(ugnotCoins(t, txAmount), user1Addr),\n\t\t\t)\n\t\t\ttreasury.Send(\n\t\t\t\tcross(cur),\n\t\t\t\ttrs_pkg.NewCoinsPayment(ugnotCoins(t, txAmount), user2Addr),\n\t\t\t)\n\t\t})\n\n\t\t// Update expected balances and history length.\n\t\texpectedTreasuryBalance = mintAmount - txAmount*2*(i+1)\n\t\texpectedUser1Balance = txAmount * (i + 1)\n\t\texpectedUser2Balance = expectedUser1Balance\n\t\texpectedHistoryLen = int(2 * (i + 1))\n\n\t\t// Check balances and history after sending.\n\t\tcheckHistoryAndBalances()\n\t}\n}\n\n// The allowlist is the only thing standing between an arbitrary realm and the\n// treasury: treasury.Send and treasury.SetTokenKeys gate on\n// dao.InAllowedDAOs, and that helper returns true for EVERY caller while the\n// list is empty (the genesis bootstrap window). UpdateImpl used to store an\n// empty list, so a single implementation-only upgrade — dao.UpdateImpl with\n// NewUpdateRequest(d, nil), which copies nil into a non-nil empty slice —\n// silently reopened that window and handed the treasury to the whole chain.\n//\n// TestAllowedDAOs above proves the gate rejects an outsider in the normal\n// state. This proves the gate cannot be switched off, which is the property\n// that actually protects the funds.\nfunc TestTreasuryLockdownCannotBeReopened(cur realm, t *testing.T) {\n\tsavedDAOs := dao.AllowedDAOs()\n\n\t// An allowlisted realm attempts the reopen. NewUpdateRequest(d, nil) is the\n\t// production spelling — it copies nil into a non-nil empty slice, which is\n\t// exactly what the old `!= nil` test accepted. (A bare UpdateRequest struct\n\t// literal cannot be built here: allocating another realm's struct type from\n\t// this realm is rejected by the VM. That path is covered by\n\t// r/gov/dao/allowlist_test.gno, which lives in the same package.)\n\ttesting.SetRealm(allowedRealm)\n\tdao.UpdateImpl(cross(cur), dao.NewUpdateRequest(nil, nil))\n\n\t// The outsider must still be refused at the treasury. Reaching\n\t// \"banker not found\" would mean authorization had been cleared — that\n\t// error comes from AFTER the InAllowedDAOs check.\n\ttesting.SetRealm(notAllowedRealm)\n\tdummyP := \u0026dummyPayment{bankerID: \"Dummy\"}\n\n\tuassert.AbortsWithMessage(\n\t\tt, cur,\n\t\t\"this Realm is not allowed to send payment: \"+notAllowedRealm.PkgPath(),\n\t\tfunc() { treasury.Send(cross(cur), dummyP) },\n\t)\n\n\t// SetTokenKeys reuses Send's message verbatim (\"...to send payment...\")\n\t// rather than naming its own operation — a copy-paste slip in\n\t// treasury.gno:65, asserted here as-is. Worth correcting separately; it is\n\t// operator-visible text, not a security property, so it is not changed as\n\t// part of this fix.\n\tuassert.AbortsWithMessage(\n\t\tt, cur,\n\t\t\"this Realm is not allowed to send payment: \"+notAllowedRealm.PkgPath(),\n\t\tfunc() { treasury.SetTokenKeys(cross(cur), []string{\"evil/key\"}) },\n\t)\n\n\t// Restore explicitly rather than via defer: the assertions above leave the\n\t// current realm set to notAllowedRealm, and a deferred UpdateImpl would run\n\t// under it and be refused. uassert.AbortsWithMessage recovers the abort, so\n\t// control always reaches here.\n\ttesting.SetRealm(allowedRealm)\n\tdao.UpdateImpl(cross(cur), dao.NewUpdateRequest(nil, savedDAOs))\n\tuassert.True(t, dao.InAllowedDAOs(allowedRealm.PkgPath()), \"allowlist restored\")\n}\n\nfunc TestRenderEscapesUnknownBankerID(t *testing.T) {\n\t// The {banker} route reflects an unknown banker id into the page; a crafted\n\t// id must be escaped so it cannot inject markdown/HTML.\n\tout := treasury.Render(\"evil[x](y)\")\n\tuassert.True(t, strings.Contains(out, `\\[x\\]`), \"reflected banker id must be escaped\")\n\tuassert.False(t, strings.Contains(out, \"[x](y)\"), \"must not render a live link\")\n}\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"},{"name":"z_token_keys_copy_test.gno","body":"package test\n\nimport (\n\t\"testing\"\n\n\ttrs_pkg \"gno.land/p/nt/treasury/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\n\t\"gno.land/r/gov/dao/treasury/v0\"\n)\n\n// Named z_* so it runs after the tests that assert exact token counts, since\n// this one replaces the treasury's token key list.\n\n// SetTokenKeys stores its argument into realm state that the grc20Lister\n// closure re-reads on every Balances() and every GRC20 payment. Held\n// uncopied, an allowed DAO realm would keep a write handle on the treasury's\n// live token set for good — able to swap it on any later block, with no\n// proposal and no vote behind it.\n//\n// The govdao_treasury_token_keys_are_immutable.txtar cannot cover this: the\n// copy in NewTreasuryGRC20TokensUpdate means the slice arriving here is\n// already impl's own, so the hazard is only reachable by calling SetTokenKeys\n// directly, as an allowed DAO realm does.\nfunc TestZSetTokenKeysCopiesItsArgument(cur realm, t *testing.T) {\n\ttesting.SetRealm(allowedRealm)\n\n\tkeyed := registerGRC20Tokens(0, cur, t, []string{\"CopyTok\"}, treasuryAddr)\n\tgrc20ID := trs_pkg.GRC20Banker{}.ID()\n\n\t// The caller's own slice, handed over the way an allowed DAO realm's\n\t// executor hands over the keys a proposal carried.\n\tkeys := []string{keyed[0].key}\n\ttreasury.SetTokenKeys(cross(cur), keys)\n\n\tbalances := treasury.Balances(grc20ID)\n\tuassert.Equal(t, len(balances), 1)\n\tuassert.Equal(t, balances[0].Denom, keyed[0].key)\n\n\t// No proposal, no vote: the supplying realm rewrites its own array.\n\tkeys[0] = \"gno.land/r/never/voted.EVIL\"\n\n\t// The treasury must still resolve the key it was given.\n\tafter := treasury.Balances(grc20ID)\n\tuassert.Equal(t, len(after), 1,\n\t\t\"the treasury's token set followed the caller's rewrite\")\n\tuassert.Equal(t, after[0].Denom, keyed[0].key)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"fShvJrDaI+Asu8pjF0hSp4qVTWKm+Uvr6vPOzWGSMwtspkTS1OK2bkv7QiVRzqi2D8+Gct4eXklXhbWpgzUTZg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"test","path":"gno.land/r/gov/dao/treasury/test/v1","files":[{"name":"amount_test.gno","body":"package test\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n)\n\n// equal256 and require256 compare 256-bit amounts natively; uassert and\n// urequire do not support uint256 (see p/nt/ufmt's README).\nfunc equal256(t *testing.T, want, got uint256, msgs ...string) bool {\n\tt.Helper()\n\tif want == got {\n\t\treturn true\n\t}\n\tt.Errorf(\"%swant %s, got %s\", msgPrefix(msgs), strconv.FormatUint256(want, 10), strconv.FormatUint256(got, 10))\n\treturn false\n}\n\nfunc require256(t *testing.T, want, got uint256, msgs ...string) {\n\tt.Helper()\n\tif want != got {\n\t\tt.Fatalf(\"%swant %s, got %s\", msgPrefix(msgs), strconv.FormatUint256(want, 10), strconv.FormatUint256(got, 10))\n\t}\n}\n\nfunc msgPrefix(msgs []string) string {\n\tif len(msgs) == 0 {\n\t\treturn \"\"\n\t}\n\treturn msgs[0] + \": \"\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/gov/dao/treasury/test/v1\"\ngno = \"0.9\"\n"},{"name":"treasury_test.gno","body":"package test\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"chain/runtime/unsafe\"\n\t\"sort\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/fqname/v0\"\n\t\"gno.land/p/nt/grc20/v1\"\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/p/nt/testutils/v0\"\n\ttrs_pkg \"gno.land/p/nt/treasury/v1\"\n\t\"gno.land/p/nt/uassert/v0\"\n\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/impl/v1\"\n\t\"gno.land/r/gov/dao/treasury/v1\"\n\t\"gno.land/r/nt/grc20reg/v1\"\n)\n\n// cur is a zero-value realm used as a placeholder when forwarding to\n// uassert/urequire dispatch helpers that gained an `rlm realm` param.\n// These tests pass `func()` callbacks (no crossing inside the callback),\n// so rlm is ignored — a nil realm here is safe.\nvar cur realm\nvar nextTokenID seqid.ID\nvar (\n\tuser1Addr       = testutils.TestAddress(\"g1user1\")\n\tuser2Addr       = testutils.TestAddress(\"g1user2\")\n\ttreasuryAddr    = chain.PackageAddress(\"gno.land/r/gov/dao/treasury/v1\")\n\tallowedRealm    = testing.NewCodeRealm(\"gno.land/r/test/allowed\")\n\tnotAllowedRealm = testing.NewCodeRealm(\"gno.land/r/test/notallowed\")\n\tmintAmount      = int64(1000)\n)\n\n// Define a dummy trs_pkg.Payment type for testing purposes.\ntype dummyPayment struct {\n\tbankerID string\n\tstr      string\n}\n\nvar _ trs_pkg.Payment = (*dummyPayment)(nil)\n\nfunc (dp *dummyPayment) BankerID() string { return dp.bankerID }\nfunc (dp *dummyPayment) String() string   { return dp.str }\n\nfunc init(cur realm) {\n\t// Register allowed Realm path.\n\tdao.UpdateImpl(cross(cur), dao.NewUpdateRequest(impl.NewGovDAO(), []string{allowedRealm.PkgPath()}))\n}\n\nfunc ugnotCoins(t *testing.T, amount int64) chain.Coins {\n\tt.Helper()\n\n\t// Create a new coin with the ugnot denomination.\n\treturn chain.NewCoins(chain.NewCoin(\"ugnot\", amount))\n}\n\n// ugnotAmounts builds the []Balance a treasury v1 coins payment takes.\nfunc ugnotAmounts(amount int64) []trs_pkg.Balance {\n\treturn []trs_pkg.Balance{{Denom: \"ugnot\", Amount: uint256(amount)}}\n}\n\nfunc ugnotBalance(t *testing.T, addr address) int64 {\n\tt.Helper()\n\n\t// Get the balance of ugnot coins for the given address.\n\tbanker_ := banker.NewReadonlyBanker()\n\tcoins := banker_.GetCoins(addr)\n\n\treturn coins.AmountOf(\"ugnot\")\n}\n\n// Define a keyedToken type to hold the token and its key.\ntype keyedToken struct {\n\tkey   string\n\ttoken *grc20.Token\n}\n\nfunc registerGRC20Tokens(_ int, rlm realm, t *testing.T, tokenNames []string, toMint address) []keyedToken {\n\tt.Helper()\n\n\tvar (\n\t\tkeyedTokens = make([]keyedToken, 0, len(tokenNames))\n\t\tkeys        = make([]string, 0, len(tokenNames))\n\t)\n\n\tfor _, name := range tokenNames {\n\t\t// Create the token.\n\t\tsymbol := strings.ToUpper(name)\n\t\ttoken, ledger := grc20.NewToken(name, symbol, 0, nextTokenID.Next(), rlm)\n\n\t\t// Register the token.\n\t\tgrc20reg.Register(cross(rlm), token, symbol)\n\n\t\t// Mint tokens to the specified address.\n\t\tledger.Mint(toMint, uint256(mintAmount))\n\n\t\t// Add the token and key to the lists.\n\t\tkey := fqname.Construct(unsafe.CurrentRealm().PkgPath(), symbol)\n\t\tkeyedTokens = append(keyedTokens, keyedToken{key: key, token: token})\n\t\tkeys = append(keys, key)\n\t}\n\n\t// Set the token keys in the treasury.\n\ttreasury.SetTokenKeys(cross(rlm), keys)\n\n\treturn keyedTokens\n}\n\nfunc TestAllowedDAOs(cur realm, t *testing.T) {\n\t// Set the current Realm to the not allowed one.\n\ttesting.SetRealm(notAllowedRealm)\n\n\t// Define a dummy payment to test sending.\n\tdummyP := \u0026dummyPayment{bankerID: \"Dummy\"}\n\n\t// Try to send, it should abort because the Realm is not allowed.\n\tuassert.AbortsWithMessage(\n\t\tt, cur,\n\t\t\"this Realm is not allowed to send payment: \"+notAllowedRealm.PkgPath(),\n\t\tfunc() { treasury.Send(cross(cur), dummyP) },\n\t)\n\n\t// Set the current Realm to the allowed one.\n\ttesting.SetRealm(allowedRealm)\n\n\t// Try to send, it should not abort because the Realm is allowed,\n\t// but because the dummy banker ID is not registered.\n\tuassert.AbortsWithMessage(\n\t\tt, cur,\n\t\t\"banker not found: \"+dummyP.BankerID(),\n\t\tfunc() { treasury.Send(cross(cur), dummyP) },\n\t)\n}\n\nfunc TestRegisteredBankers(t *testing.T) {\n\t// Set the current Realm to the allowed one.\n\ttesting.SetRealm(allowedRealm)\n\n\t// Define the expected banker IDs.\n\texpectedBankerIDs := []string{\n\t\ttrs_pkg.CoinsBanker{}.ID(),\n\t\ttrs_pkg.GRC20Banker{}.ID(),\n\t}\n\n\t// Get the registered bankers from the treasury and compare their lengths.\n\tregistered := treasury.ListBankerIDs()\n\tuassert.Equal(t, len(registered), len(expectedBankerIDs))\n\n\t// The treasury-returned slice is foreign-readonly; copy locally\n\t// before sorting in place.\n\tregisteredBankerIDs := append([]string(nil), registered...)\n\n\t// Sort both slices then compare them.\n\tsort.StringSlice(expectedBankerIDs).Sort()\n\tsort.StringSlice(registeredBankerIDs).Sort()\n\n\tfor i := range expectedBankerIDs {\n\t\tuassert.Equal(t, expectedBankerIDs[i], registeredBankerIDs[i])\n\t}\n\n\t// Test HasBanker method.\n\tfor _, bankerID := range expectedBankerIDs {\n\t\tuassert.True(t, treasury.HasBanker(bankerID))\n\t}\n\tuassert.False(t, treasury.HasBanker(\"UnknownBankerID\"))\n\n\t// Test Address method.\n\tfor _, bankerID := range expectedBankerIDs {\n\t\t// The two bankers used for now should have the treasury Realm address.\n\t\tuassert.Equal(t, treasury.Address(bankerID), treasuryAddr.String())\n\t}\n}\n\nfunc TestSendGRC20Payment(cur realm, t *testing.T) {\n\t// Set the current Realm to the allowed one.\n\ttesting.SetRealm(allowedRealm)\n\n\t// Try to send a GRC20 payment with a not registered token, it should abort.\n\tuassert.AbortsWithMessage(\n\t\tt, cur,\n\t\t\"failed to send payment: GRC20 token not found: UNKNOW\",\n\t\tfunc() {\n\t\t\ttreasury.Send(cross(cur), trs_pkg.NewGRC20Payment(\"UNKNOW\", 100, user1Addr))\n\t\t},\n\t)\n\n\t// Create 3 GRC20 tokens and register them.\n\tkeyedTokens := registerGRC20Tokens(\n\t\t0, cur,\n\t\tt,\n\t\t[]string{\"TestToken0\", \"TestToken1\", \"TestToken2\"},\n\t\ttreasuryAddr,\n\t)\n\n\tconst txAmount = 42\n\n\t// For each token-user pair.\n\tfor i, userAddr := range []address{user1Addr, user2Addr} {\n\t\tfor _, keyed := range keyedTokens {\n\t\t\t// Check that the treasury has the expected balance before sending.\n\t\t\tequal256(t, uint256(mintAmount-int64(txAmount*i)), keyed.token.BalanceOf(treasuryAddr))\n\n\t\t\t// Check that the user has no balance before sending.\n\t\t\tequal256(t, uint256(0), keyed.token.BalanceOf(userAddr))\n\n\t\t\t// Try to send a GRC20 payment with a registered token, it should not abort.\n\t\t\tuassert.NotAborts(t, cur, func() {\n\t\t\t\ttreasury.Send(\n\t\t\t\t\tcross(cur),\n\t\t\t\t\ttrs_pkg.NewGRC20Payment(\n\t\t\t\t\t\tkeyed.key,\n\t\t\t\t\t\ttxAmount,\n\t\t\t\t\t\tuserAddr,\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t})\n\n\t\t\t// Check that the user has the expected balance after sending.\n\t\t\tequal256(t, uint256(txAmount), keyed.token.BalanceOf(userAddr))\n\n\t\t\t// Check that the treasury has the expected balance after sending.\n\t\t\tequal256(t, uint256(mintAmount-int64(txAmount*(i+1))), keyed.token.BalanceOf(treasuryAddr))\n\t\t}\n\t}\n\n\t// Get the GRC20Banker ID.\n\tgrc20BankerID := trs_pkg.GRC20Banker{}.ID()\n\n\t// Test Balances method for the GRC20Banker.\n\tbalances := treasury.Balances(grc20BankerID)\n\tuassert.Equal(t, len(balances), len(keyedTokens))\n\n\tcompared := 0\n\tfor _, balance := range balances {\n\t\tfor _, keyed := range keyedTokens {\n\t\t\tif balance.Denom == keyed.key {\n\t\t\t\tequal256(t, keyed.token.BalanceOf(treasuryAddr), balance.Amount)\n\t\t\t\tcompared++\n\t\t\t}\n\t\t}\n\t}\n\tuassert.Equal(t, compared, len(keyedTokens))\n\n\t// Check the history of the GRC20Banker.\n\thistory := treasury.History(grc20BankerID, 1, 10)\n\tuassert.Equal(t, len(history), 6)\n\n\t// Try to send a dummy payment with the GRC20 banker ID, it should abort.\n\tuassert.AbortsWithMessage(\n\t\tt, cur,\n\t\t\"failed to send payment: invalid payment type\",\n\t\tfunc() {\n\t\t\ttreasury.Send(cross(cur), \u0026dummyPayment{bankerID: grc20BankerID})\n\t\t},\n\t)\n\n\t// Try to send a GRC20 payment without enough balance, it should abort.\n\tuassert.AbortsWithMessage(\n\t\tt, cur,\n\t\t\"failed to send payment: insufficient balance\",\n\t\tfunc() {\n\t\t\ttreasury.Send(\n\t\t\t\tcross(cur),\n\t\t\t\ttrs_pkg.NewGRC20Payment(\n\t\t\t\t\tkeyedTokens[0].key,\n\t\t\t\t\tuint256(mintAmount*42), // Try to send more than the treasury has.\n\t\t\t\t\tuser1Addr,\n\t\t\t\t),\n\t\t\t)\n\t\t},\n\t)\n\n\t// Check the history of the GRC20Banker.\n\thistory = treasury.History(grc20BankerID, 1, 10)\n\tuassert.Equal(t, len(history), 6)\n}\n\nfunc TestSendCoinPayment(cur realm, t *testing.T) {\n\t// Set the current Realm to the allowed one.\n\ttesting.SetRealm(allowedRealm)\n\n\t// Issue initial ugnot coins to the treasury address.\n\ttesting.IssueCoins(treasuryAddr, ugnotCoins(t, mintAmount))\n\n\t// Get the CoinsBanker ID.\n\tbankerID := trs_pkg.CoinsBanker{}.ID()\n\n\t// Define helper function to check balances and history.\n\tvar (\n\t\texpectedTreasuryBalance = mintAmount\n\t\texpectedUser1Balance    = int64(0)\n\t\texpectedUser2Balance    = int64(0)\n\t\texpectedHistoryLen      = 0\n\t\tcheckHistoryAndBalances = func() {\n\t\t\tt.Helper()\n\n\t\t\tuassert.Equal(t, ugnotBalance(t, treasuryAddr), expectedTreasuryBalance)\n\t\t\tuassert.Equal(t, ugnotBalance(t, user1Addr), expectedUser1Balance)\n\t\t\tuassert.Equal(t, ugnotBalance(t, user2Addr), expectedUser2Balance)\n\n\t\t\t// Check treasury.Balances returned value.\n\t\t\tbalances := treasury.Balances(bankerID)\n\t\t\tuassert.Equal(t, len(balances), 1)\n\t\t\tuassert.Equal(t, balances[0].Denom, \"ugnot\")\n\t\t\tequal256(t, uint256(expectedTreasuryBalance), balances[0].Amount)\n\n\t\t\t// Check treasury.History returned value.\n\t\t\thistory := treasury.History(bankerID, 1, expectedHistoryLen+1)\n\t\t\tuassert.Equal(t, len(history), expectedHistoryLen)\n\t\t}\n\t)\n\n\t// Check initial balances and history.\n\tcheckHistoryAndBalances()\n\n\tconst txAmount = int64(42)\n\n\t// Treasury send coins.\n\tfor i := int64(0); i \u003c 3; i++ {\n\t\t// Send ugnot coins to user1 and user2.\n\t\tuassert.NotAborts(t, cur, func() {\n\t\t\ttreasury.Send(\n\t\t\t\tcross(cur),\n\t\t\t\ttrs_pkg.NewCoinsPayment(ugnotAmounts(txAmount), user1Addr),\n\t\t\t)\n\t\t\ttreasury.Send(\n\t\t\t\tcross(cur),\n\t\t\t\ttrs_pkg.NewCoinsPayment(ugnotAmounts(txAmount), user2Addr),\n\t\t\t)\n\t\t})\n\n\t\t// Update expected balances and history length.\n\t\texpectedTreasuryBalance = mintAmount - txAmount*2*(i+1)\n\t\texpectedUser1Balance = txAmount * (i + 1)\n\t\texpectedUser2Balance = expectedUser1Balance\n\t\texpectedHistoryLen = int(2 * (i + 1))\n\n\t\t// Check balances and history after sending.\n\t\tcheckHistoryAndBalances()\n\t}\n}\n\n// The allowlist is the only thing standing between an arbitrary realm and the\n// treasury: treasury.Send and treasury.SetTokenKeys gate on\n// dao.InAllowedDAOs, and that helper returns true for EVERY caller while the\n// list is empty (the genesis bootstrap window). UpdateImpl used to store an\n// empty list, so a single implementation-only upgrade — dao.UpdateImpl with\n// NewUpdateRequest(d, nil), which copies nil into a non-nil empty slice —\n// silently reopened that window and handed the treasury to the whole chain.\n//\n// TestAllowedDAOs above proves the gate rejects an outsider in the normal\n// state. This proves the gate cannot be switched off, which is the property\n// that actually protects the funds.\nfunc TestTreasuryLockdownCannotBeReopened(cur realm, t *testing.T) {\n\tsavedDAOs := dao.AllowedDAOs()\n\n\t// An allowlisted realm attempts the reopen. NewUpdateRequest(d, nil) is the\n\t// production spelling — it copies nil into a non-nil empty slice, which is\n\t// exactly what the old `!= nil` test accepted. (A bare UpdateRequest struct\n\t// literal cannot be built here: allocating another realm's struct type from\n\t// this realm is rejected by the VM. That path is covered by\n\t// r/gov/dao/allowlist_test.gno, which lives in the same package.)\n\ttesting.SetRealm(allowedRealm)\n\tdao.UpdateImpl(cross(cur), dao.NewUpdateRequest(nil, nil))\n\n\t// The outsider must still be refused at the treasury. Reaching\n\t// \"banker not found\" would mean authorization had been cleared — that\n\t// error comes from AFTER the InAllowedDAOs check.\n\ttesting.SetRealm(notAllowedRealm)\n\tdummyP := \u0026dummyPayment{bankerID: \"Dummy\"}\n\n\tuassert.AbortsWithMessage(\n\t\tt, cur,\n\t\t\"this Realm is not allowed to send payment: \"+notAllowedRealm.PkgPath(),\n\t\tfunc() { treasury.Send(cross(cur), dummyP) },\n\t)\n\n\t// SetTokenKeys reuses Send's message verbatim (\"...to send payment...\")\n\t// rather than naming its own operation — a copy-paste slip in\n\t// treasury.gno:65, asserted here as-is. Worth correcting separately; it is\n\t// operator-visible text, not a security property, so it is not changed as\n\t// part of this fix.\n\tuassert.AbortsWithMessage(\n\t\tt, cur,\n\t\t\"this Realm is not allowed to send payment: \"+notAllowedRealm.PkgPath(),\n\t\tfunc() { treasury.SetTokenKeys(cross(cur), []string{\"evil/key\"}) },\n\t)\n\n\t// Restore explicitly rather than via defer: the assertions above leave the\n\t// current realm set to notAllowedRealm, and a deferred UpdateImpl would run\n\t// under it and be refused. uassert.AbortsWithMessage recovers the abort, so\n\t// control always reaches here.\n\ttesting.SetRealm(allowedRealm)\n\tdao.UpdateImpl(cross(cur), dao.NewUpdateRequest(nil, savedDAOs))\n\tuassert.True(t, dao.InAllowedDAOs(allowedRealm.PkgPath()), \"allowlist restored\")\n}\n\nfunc TestRenderEscapesUnknownBankerID(t *testing.T) {\n\t// The {banker} route reflects an unknown banker id into the page; a crafted\n\t// id must be escaped so it cannot inject markdown/HTML.\n\tout := treasury.Render(\"evil[x](y)\")\n\tuassert.True(t, strings.Contains(out, `\\[x\\]`), \"reflected banker id must be escaped\")\n\tuassert.False(t, strings.Contains(out, \"[x](y)\"), \"must not render a live link\")\n}\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"},{"name":"z_token_keys_copy_test.gno","body":"package test\n\nimport (\n\t\"testing\"\n\n\ttrs_pkg \"gno.land/p/nt/treasury/v1\"\n\t\"gno.land/p/nt/uassert/v0\"\n\n\t\"gno.land/r/gov/dao/treasury/v1\"\n)\n\n// Named z_* so it runs after the tests that assert exact token counts, since\n// this one replaces the treasury's token key list.\n\n// SetTokenKeys stores its argument into realm state that the grc20Lister\n// closure re-reads on every Balances() and every GRC20 payment. Held\n// uncopied, an allowed DAO realm would keep a write handle on the treasury's\n// live token set for good — able to swap it on any later block, with no\n// proposal and no vote behind it.\n//\n// The govdao_treasury_token_keys_are_immutable.txtar cannot cover this: the\n// copy in NewTreasuryGRC20TokensUpdate means the slice arriving here is\n// already impl's own, so the hazard is only reachable by calling SetTokenKeys\n// directly, as an allowed DAO realm does.\nfunc TestZSetTokenKeysCopiesItsArgument(cur realm, t *testing.T) {\n\ttesting.SetRealm(allowedRealm)\n\n\tkeyed := registerGRC20Tokens(0, cur, t, []string{\"CopyTok\"}, treasuryAddr)\n\tgrc20ID := trs_pkg.GRC20Banker{}.ID()\n\n\t// The caller's own slice, handed over the way an allowed DAO realm's\n\t// executor hands over the keys a proposal carried.\n\tkeys := []string{keyed[0].key}\n\ttreasury.SetTokenKeys(cross(cur), keys)\n\n\tbalances := treasury.Balances(grc20ID)\n\tuassert.Equal(t, len(balances), 1)\n\tuassert.Equal(t, balances[0].Denom, keyed[0].key)\n\n\t// No proposal, no vote: the supplying realm rewrites its own array.\n\tkeys[0] = \"gno.land/r/never/voted.EVIL\"\n\n\t// The treasury must still resolve the key it was given.\n\tafter := treasury.Balances(grc20ID)\n\tuassert.Equal(t, len(after), 1,\n\t\t\"the treasury's token set followed the caller's rewrite\")\n\tuassert.Equal(t, after[0].Denom, keyed[0].key)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"LZWi7BNeOHOGoEXdA9QEFkqegLRVvSFmlXrNGcQEiRA5e2VYJXcgO3ZACt2W2v3fbWU1s66Wa6509m3NdkRb6A=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g15ge0ae9077eh40erwrn2eq0xw6wupwqthpv34l","package":{"name":"commondao","path":"gno.land/r/nt/commondao/v0","files":[{"name":"README.md","body":"\u003e **v0 - Unaudited**\n\u003e This is an initial version of this realm 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 (realm)\n\nReference realm implementation of the `gno.land/p/nt/commondao/v0` package for\nmanaging Decentralized Autonomous Organizations per the Common DAO Spec\n(`docs/CONSTITUTION.md`, Appendix).\n\nWhat it hosts:\n\n- **DAOs and sub-DAO trees** with a Charter (purpose + description), a\n  council, and per-DAO treasury addresses derived as realm sub-identities\n  (`cur.Sub(\"dao/\u003cid\u003e\")`).\n- **Proposals** through a per-DAO registry of proposal kinds. Ten default\n  kinds are seeded at creation (text, council updates, sub-DAO creation,\n  dissolution, treasury spend/clawback/freeze, manage-kinds, amend-bylaws);\n  the arbitrary-execution kind is opt-in by governance. The kind set itself\n  is governable (`manage-kinds`), and manage-kinds can never be deregistered.\n- **Treasuries**: spends from a DAO's own sub-identity; ancestor clawback and\n  freeze; dissolution sweeps. Freeze blocks every self-initiated movement,\n  including arbitrary execution.\n- **Bylaws \u0026 Mandates** as named plaintext documents amended by verifiable\n  diff patches (`gno.land/p/nt/bylaws/v0`); the `mandates/` folder is\n  reserved for the (deferred) ancestor amendment power.\n- **Render** pages for DAOs, settings, bylaws, proposals and votes.\n\nDesign records live in `gno.land/adr/pr6012_commondao_*.md`; the extension\nguide for building your own realm on the `/p/` package is in\n`gno.land/p/nt/commondao/v0`'s README.\n"},{"name":"commondao.gno","body":"package commondao\n\nimport (\n\t\"chain\"\n\t\"strconv\"\n\n\t\"gno.land/p/moul/txlink/v0\"\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n)\n\n// pkgPath is this realm's package path, fixed regardless of quarantine\n// location. DAO treasury addresses derive from it, so they are stable\n// across a SAME-PATH redeploy. Moving to a different package path (e.g.\n// .../v1) changes cur.Sub derivation and would strand existing treasuries\n// unless a drain executor ran first (see gnovm/adr/pr5890_realm_sub.md\n// §Cross-host identity); this realm ships no such migration executor.\nconst pkgPath = \"gno.land/r/nt/commondao/v0\"\n\n// CommonDAOID is the ID of the realm's DAO.\nconst CommonDAOID uint64 = 1\n\nvar (\n\tdaoID     seqid.ID\n\trealmLink = txlink.Realm(pkgPath)\n\tdaos      = bptree.NewBPTree32() // string(ID) -\u003e *commondao.CommonDAO\n\tinvites   = bptree.NewBPTree32() // string(address) -\u003e address(inviter)\n\tcreators  = bptree.NewBPTree32() // string(address) -\u003e struct{}; consumed-invite marker\n\tlisted    = bptree.NewBPTree32() // string(ID) -\u003e uint64(ID); home-index opt-in\n)\n\nfunc getDAO(daoID uint64) *commondao.CommonDAO {\n\tkey := makeIDKey(daoID)\n\tif v := daos.Get(key); v != nil {\n\t\treturn v.(*commondao.CommonDAO)\n\t}\n\treturn nil\n}\n\nfunc mustGetDAO(daoID uint64) *commondao.CommonDAO {\n\tdao := getDAO(daoID)\n\tif dao == nil {\n\t\tpanic(\"DAO not found\")\n\t}\n\treturn dao\n}\n\nfunc makeIDKey(daoID uint64) string {\n\treturn seqid.ID(daoID).String()\n}\n\n// isCreator reports whether an address has already consumed an invite and\n// may create further DAOs without another one.\nfunc isCreator(addr address) bool {\n\treturn creators.Has(addr.String())\n}\n\n// isListed reports whether a DAO opted into the realm's public home index.\nfunc isListed(daoID uint64) bool {\n\treturn listed.Has(makeIDKey(daoID))\n}\n\n// setListed adds or removes a DAO from the realm's public home index.\nfunc setListed(daoID uint64, v bool) {\n\tkey := makeIDKey(daoID)\n\tif v {\n\t\t// Store the ID as the value so renderHome can paginate over the\n\t\t// listed set directly (keys are cford32-encoded, order-preserving).\n\t\tlisted.Set(key, daoID)\n\t} else {\n\t\tlisted.Remove(key)\n\t}\n}\n\n// subpathOf returns the realm sub-identity subpath that acts for a DAO.\nfunc subpathOf(daoID uint64) string {\n\treturn \"dao/\" + strconv.FormatUint(daoID, 10)\n}\n\n// daoAddress derives a DAO's treasury address: the address of the realm\n// sub-identity cur.Sub(subpathOf(daoID)) that executors mint to move its\n// funds. Pure derivation; DAO IDs are never reused, so the address is\n// collision free and stable.\nfunc daoAddress(daoID uint64) address {\n\treturn chain.DerivePkgSubAddr(pkgPath, subpathOf(daoID))\n}\n\nfunc createDAO(name, purpose, description string, members ...address) *commondao.CommonDAO {\n\tif len(members) == 0 {\n\t\tpanic(\"a DAO requires at least one initial council member\")\n\t}\n\n\tid := daoID.Next()\n\tdao := commondao.New(newDAOOptions(uint64(id), name, purpose, description, nil, members)...)\n\tdaos.Set(id.String(), dao)\n\treturn dao\n}\n\nfunc createSubDAO(parent *commondao.CommonDAO, name, purpose, description string, members ...address) *commondao.CommonDAO {\n\tif len(members) == 0 {\n\t\tpanic(\"a SubDAO requires at least one initial council member\")\n\t}\n\n\t// WithParent also registers the new DAO as one of parent's children\n\tid := daoID.Next()\n\tdao := commondao.New(newDAOOptions(uint64(id), name, purpose, description, parent, members)...)\n\tdaos.Set(id.String(), dao)\n\treturn dao\n}\n\nfunc newDAOOptions(id uint64, name, purpose, description string, parent *commondao.CommonDAO, members []address) []commondao.Option {\n\topts := []commondao.Option{\n\t\tcommondao.WithID(id),\n\t\tcommondao.WithName(name),\n\t\tcommondao.WithPurpose(purpose),\n\t\tcommondao.WithDescription(description),\n\t\tcommondao.WithAddress(daoAddress(id)),\n\t}\n\tif parent != nil {\n\t\topts = append(opts, commondao.WithParent(parent))\n\t}\n\tfor _, m := range members {\n\t\topts = append(opts, commondao.WithCouncilMember(m))\n\t}\n\n\t// Seed the default proposal kinds: every new DAO (genesis, user-created\n\t// or sub-DAO) starts with these registered. The opt-in catalog kind\n\t// (execution) is deliberately NOT seeded — a DAO gains it only after a\n\t// supermajority CreateRegisterKindProposal.\n\tfor _, k := range defaultProposalKinds {\n\t\topts = append(opts, commondao.WithProposalKind(k))\n\t}\n\treturn opts\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 a reference realm implementation of the\n// gno.land/p/nt/commondao/v0 package for managing Decentralized Autonomous\n// Organizations per the Common DAO Spec: DAO and sub-DAO trees with\n// councils and charters, a governable registry of proposal kinds (ten\n// defaults plus an opt-in arbitrary-execution kind), per-DAO treasuries on\n// derived sub-identity addresses with ancestor clawback/freeze powers, and\n// bylaws documents amended by verifiable diff patches. See README.md and\n// the ADRs under gno.land/adr/ for the design record.\npackage commondao\n"},{"name":"genesis.gno","body":"package commondao\n\nfunc init() {\n\t// Fail closed if deployed off the package path treasury addresses\n\t// derive from (see assertRunningPath).\n\tassertRunningPath()\n\n\t// Create the realm's own DAO, governed by its maintainers. There is no\n\t// owner: the realm administers nothing about a DAO except its home-index\n\t// listing, which the realm DAO sets for itself here.\n\tdao := createDAO(\n\t\t\"Common DAO\",\n\t\t\"Govern and maintain the commondao reference realm.\",\n\t\t\"This DAO is responsible for managing `commondao` realm functionalities.\",\n\t\t\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\", // govdao t1 multisig\n\t\t\"g1manfred47kzduec920z88wfr64ylksmdcedlf5\", // @moul\n\t)\n\tsetListed(dao.ID(), true)\n}\n"},{"name":"genesis_test.gno","body":"package commondao\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\n// TestGenesisCouncilOrder pins the council of the DAO genesis.gno creates.\n// renderCouncil walks the addrset, so the realm's own page lists the two\n// founding members in bech32 order, not in the order genesis.gno passes\n// them. No filetest renders DAO 1, so nothing else in the suite notices\n// when either address moves.\nfunc TestGenesisCouncilOrder(t *testing.T) {\n\tdao := mustGetDAO(1)\n\tcouncil := dao.Council()\n\turequire.Equal(t, 2, council.Size())\n\n\tvar got []string\n\tcouncil.IterateByOffset(0, council.Size(), func(addr address) bool {\n\t\tgot = append(got, addr.String())\n\t\treturn false\n\t})\n\n\turequire.Equal(t, 2, len(got))\n\tuassert.Equal(t, \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\", got[0])\n\tuassert.Equal(t, \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\", got[1])\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/nt/commondao/v0\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g15ge0ae9077eh40erwrn2eq0xw6wupwqthpv34l\"\n"},{"name":"proposal_bylaws.gno","body":"package commondao\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/bylaws/v0\"\n\t\"gno.land/p/nt/commondao/v0\"\n)\n\n// bylawsSets stores each DAO's governing documents (bylaws and\n// mandates), keyed like daos. A set is created lazily on a DAO's first\n// amendment proposal; a DAO with no documents has no entry. The\n// *bylaws.Bylaws handle is mutable (Apply amends it), so it never leaves\n// the realm — public reads return strings.\nvar bylawsSets = bptree.NewBPTree32() // string(ID) -\u003e *bylaws.Bylaws\n\n// bylawsView returns a DAO's document set or nil when it has none. Read\n// paths (render, public getters, payload building) use this — it never\n// writes realm state, so it is safe under read-only query evaluation.\nfunc bylawsView(daoID uint64) *bylaws.Bylaws {\n\tif v := bylawsSets.Get(makeIDKey(daoID)); v != nil {\n\t\treturn v.(*bylaws.Bylaws)\n\t}\n\treturn nil\n}\n\n// bylawsOf returns a DAO's document set, creating an empty one on first\n// use. Only proposal creation calls it; an empty set left behind by a\n// failed proposal is harmless.\nfunc bylawsOf(daoID uint64) *bylaws.Bylaws {\n\tif b := bylawsView(daoID); b != nil {\n\t\treturn b\n\t}\n\tb := bylaws.New()\n\tbylawsSets.Set(makeIDKey(daoID), b)\n\treturn b\n}\n\n// isMandatesPath reports whether a document path is in the reserved\n// mandates/ folder (or is the bare \"mandates\" file). Mandates are not\n// council-self-amendable; see amendBylawsKind.New.\nfunc isMandatesPath(path string) bool {\n\treturn path == \"mandates\" || strings.HasPrefix(path, \"mandates/\")\n}\n\n// amendBylawsProposal is both the amend-bylaws args struct and the\n// proposal definition it produces (one type serves both roles, like\n// manageKindsProposal). set is the host DAO's document set the executor\n// patches: New receives only a readonly view, so the trusted wrapper\n// passes the mutable set through args (from its own bylawsOf) together\n// with the host daoID for the identity pin. display is the human-readable\n// change, rendered at New against the then-current document.\ntype amendBylawsProposal struct {\n\tdaoID   uint64\n\tset     *bylaws.Bylaws\n\tpatch   bylaws.Patch\n\tdisplay string\n}\n\n// amendBylawsKind creates proposals that add, amend or remove one of the\n// host DAO's bylaws/mandates documents with a verifiable diff patch. The\n// patch pins the sha256 of the document text it was diffed against, so a\n// passed amendment that raced a concurrent change to the same document\n// fails cleanly (StatusFailed) instead of clobbering it. Decided by\n// supermajority: this is the council amending its OWN governing\n// documents, which the Constitution grants with no special threshold, so\n// the default council rule applies. (The Constitution's simple-majority\n// clause for charter/bylaws/mandate changes is an ANCESTOR power — a\n// parent amending a descendant's documents — which this realm does not\n// implement yet.)\ntype amendBylawsKind struct{}\n\nfunc (amendBylawsKind) Name() string { return kindAmendBylaws }\n\nfunc (amendBylawsKind) New(dao commondao.ReadonlyCommonDAO, args any) (commondao.ProposalDefinition, error) {\n\tp, ok := args.(amendBylawsProposal)\n\tif !ok {\n\t\treturn nil, errInvalidProposalArgs\n\t}\n\t// Defense in depth: pin the proposal to the readonly host Propose\n\t// passed here AND pin the args-captured document set to that host's\n\t// canonical set, so a future wrapper can never validate against one\n\t// DAO's documents and amend another's (the trusted wrapper always\n\t// passes matching handles today).\n\tif p.daoID != dao.ID() || p.set == nil || p.set != bylawsView(p.daoID) {\n\t\treturn nil, errInvalidProposalArgs\n\t}\n\tif !bylaws.IsValidPath(p.patch.Path) {\n\t\treturn nil, bylaws.ErrInvalidPath\n\t}\n\t// The Constitution grants a council self-power over its BYLAWS only;\n\t// Mandates are changed from above (creation or an ancestor's Simple\n\t// Majority — the ancestor amendment path, not implemented yet). Reserve\n\t// the mandates/ folder so self-amendment cannot author what only an\n\t// ancestor may.\n\tif isMandatesPath(p.patch.Path) {\n\t\treturn nil, errors.New(\"mandates are not council-amendable: they are set at creation or by an ancestor (ancestor amendment is not implemented yet)\")\n\t}\n\n\t// Freshness fail-fast: reject a patch that is already stale at\n\t// creation (Validate re-checks at execution).\n\tcur, exists := p.set.Get(p.patch.Path)\n\tcurHash := \"\"\n\tif exists {\n\t\tcurHash = bylaws.HashText(cur)\n\t}\n\tif p.patch.Base != curHash {\n\t\treturn nil, bylaws.ErrStalePatch\n\t}\n\n\t// Rendering the change also validates the edit script against the\n\t// current text, so a malformed patch never becomes a proposal; only\n\t// then reject the well-formed do-nothing shapes.\n\tdisplay, err := p.patch.Format(cur)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif p.patch.IsNoop() {\n\t\treturn nil, errors.New(\"bylaws amendment must change the document\")\n\t}\n\tp.display = display\n\treturn p, nil\n}\n\n// Title returns the proposal title as raw text: the renderer escapes\n// every definition title.\nfunc (p amendBylawsProposal) Title() string {\n\tverb := \"Amend\"\n\tswitch {\n\tcase p.patch.IsCreate():\n\t\tverb = \"Add\"\n\tcase p.patch.IsRemove():\n\t\tverb = \"Remove\"\n\t}\n\treturn verb + \" Bylaws Document: \" + p.patch.Path\n}\n\n// isTrustedMarkdownBody marks Body as self-assembled markdown: the path\n// is escaped inline and the change summary is emitted as a fenced code\n// block.\nfunc (amendBylawsProposal) isTrustedMarkdownBody() {}\n\nfunc (p amendBylawsProposal) Body() string {\n\t// A code block, not sanitize.Block: the summary's \"+\" lines are the\n\t// proposer's inserted literal text, and Block deliberately preserves\n\t// inline formatting AND inline links — which would render a live\n\t// attacker-controlled link on the page councils read before voting\n\t// (the same reason renderProposal escapes untrusted bodies inline).\n\t// A fence keeps the diff's line structure, which an inline escape\n\t// would fold away, and neutralizes markup; md.CodeBlock widens the\n\t// fence to outscan any backticks in the content.\n\treturn md.Paragraph(md.Bold(\"Document:\")+\" \"+md.EscapeText(p.patch.Path)) +\n\t\tmd.CodeBlock(p.display)\n}\n\nfunc (amendBylawsProposal) VotingPeriod() time.Duration { return time.Hour * 24 * 7 }\n\n// Threshold returns the tally threshold: amending the DAO's own\n// governing documents is a council decision with no special\n// constitutional threshold, so the supermajority default applies (the\n// Constitution's simple-majority clause covers ancestor-initiated\n// amendment, not implemented here).\nfunc (amendBylawsProposal) Threshold() commondao.Threshold {\n\treturn commondao.ThresholdSupermajority\n}\n\n// Validate re-asserts patch freshness at execution (Validate reruns\n// inside Execute): a document amended after this proposal passed fails\n// it cleanly (StatusFailed) instead of clobbering the newer text.\nfunc (p amendBylawsProposal) Validate() error {\n\tif p.set.Hash(p.patch.Path) != p.patch.Base {\n\t\treturn bylaws.ErrStalePatch\n\t}\n\treturn nil\n}\n\nfunc (p amendBylawsProposal) Executor() commondao.ExecFunc {\n\treturn p.execute\n}\n\n// execute applies the patch, returning any bylaws error unchanged so a\n// race between two passed amendments fails the later one cleanly\n// (StatusFailed) instead of panicking the transaction. It moves no\n// funds, so the definition is not Funded and ignores sub.\nfunc (p amendBylawsProposal) execute(_ int, _ realm) error {\n\treturn p.set.Apply(p.patch)\n}\n"},{"name":"proposal_bylaws_test.gno","body":"package commondao\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/bylaws/v0\"\n\t\"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nvar (\n\t_ commondao.ProposalDefinition = amendBylawsProposal{}\n\t_ commondao.Validable          = amendBylawsProposal{}\n\t_ commondao.Executable         = amendBylawsProposal{}\n\t_ trustedMarkdownBody          = amendBylawsProposal{}\n)\n\n// TestAmendBylawsKindNew pins the amend-bylaws factory: args validation,\n// the host-identity pin, the stale-at-create fail-fast, the no-op reject,\n// and the malformed-script reject. Without this, removing any of those\n// guards is invisible to the suite.\nfunc TestAmendBylawsKindNew(t *testing.T) {\n\tdao := newTestDAO(t, \"BylawsHost\")\n\tother := newTestDAO(t, \"BylawsOther\")\n\tset := bylawsOf(dao.ID())\n\n\t// Wrong args type is a realm bug, reported as invalid args.\n\t_, err := amendBylawsKind{}.New(dao.Readonly(), textArgs{})\n\tuassert.ErrorIs(t, err, errInvalidProposalArgs)\n\n\t// A nil document set is a realm bug too.\n\tfreshPatch := func(text string) bylaws.Patch {\n\t\tt.Helper()\n\t\tp, err := set.Diff(\"bylaws/a.md\", text)\n\t\turequire.NoError(t, err)\n\t\treturn p\n\t}\n\t_, err = amendBylawsKind{}.New(dao.Readonly(), amendBylawsProposal{daoID: dao.ID(), patch: freshPatch(\"one\")})\n\tuassert.ErrorIs(t, err, errInvalidProposalArgs)\n\n\t// Host-identity pin: a set/daoID for a different DAO than the one being\n\t// proposed on is rejected.\n\t_, err = amendBylawsKind{}.New(other.Readonly(), amendBylawsProposal{daoID: dao.ID(), set: set, patch: freshPatch(\"one\")})\n\tuassert.ErrorIs(t, err, errInvalidProposalArgs)\n\n\t// Set-identity pin: the args-captured set must be the host's canonical\n\t// set — a matching daoID with a different DAO's set is rejected, so a\n\t// wrapper can never validate one DAO's documents and amend another's.\n\totherSet := bylawsOf(other.ID())\n\t_, err = amendBylawsKind{}.New(dao.Readonly(), amendBylawsProposal{daoID: dao.ID(), set: otherSet, patch: freshPatch(\"one\")})\n\tuassert.ErrorIs(t, err, errInvalidProposalArgs)\n\n\t// Invalid document paths fail fast at creation.\n\tbadPath := bylaws.Patch{Path: \"bad path\", Base: \"\", Ops: []bylaws.Op{{Type: bylaws.OpInsert, Text: \"x\"}}}\n\t_, err = amendBylawsKind{}.New(dao.Readonly(), amendBylawsProposal{daoID: dao.ID(), set: set, patch: badPath})\n\tuassert.ErrorIs(t, err, bylaws.ErrInvalidPath)\n\n\t// The mandates/ folder is reserved: mandates are set at creation or by\n\t// an ancestor, never by the council's own amendment power.\n\tmandate, err := bylaws.DiffTexts(\"mandates/spend.md\", \"\", \"Spend only by vote.\", false)\n\turequire.NoError(t, err)\n\t_, err = amendBylawsKind{}.New(dao.Readonly(), amendBylawsProposal{daoID: dao.ID(), set: set, patch: mandate})\n\turequire.Error(t, err)\n\tuassert.True(t, strings.Contains(err.Error(), \"mandates are not council-amendable\"), \"expect the mandates reservation\")\n\tbare, err := bylaws.DiffTexts(\"mandates\", \"\", \"x\", false)\n\turequire.NoError(t, err)\n\t_, err = amendBylawsKind{}.New(dao.Readonly(), amendBylawsProposal{daoID: dao.ID(), set: set, patch: bare})\n\turequire.Error(t, err)\n\tuassert.True(t, strings.Contains(err.Error(), \"mandates are not council-amendable\"), \"expect the bare-path reservation\")\n\n\t// Stale-at-create: a patch whose base does not match the current\n\t// document is rejected up front.\n\tstale := bylaws.Patch{Path: \"bylaws/a.md\", Base: bylaws.HashText(\"not the base\"), Ops: []bylaws.Op{{Type: bylaws.OpInsert, Text: \"x\"}}}\n\t_, err = amendBylawsKind{}.New(dao.Readonly(), amendBylawsProposal{daoID: dao.ID(), set: set, patch: stale})\n\tuassert.ErrorIs(t, err, bylaws.ErrStalePatch)\n\n\t// No-op amendments are rejected: a council vote is always about an\n\t// actual change.\n\tnoop, err := bylaws.DiffTexts(\"bylaws/a.md\", \"\", \"\", false)\n\turequire.NoError(t, err)\n\t_, err = amendBylawsKind{}.New(dao.Readonly(), amendBylawsProposal{daoID: dao.ID(), set: set, patch: noop})\n\turequire.Error(t, err)\n\tuassert.Equal(t, \"bylaws amendment must change the document\", err.Error())\n\n\t// A fresh-base patch with a script that does not fit the document is\n\t// rejected (Format validates the script while rendering the display).\n\t// The op must not be Keep-only, or the no-op check fires first.\n\tbroken := bylaws.Patch{Path: \"bylaws/a.md\", Base: \"\", Ops: []bylaws.Op{{Type: bylaws.OpDelete, N: 5}}}\n\t_, err = amendBylawsKind{}.New(dao.Readonly(), amendBylawsProposal{daoID: dao.ID(), set: set, patch: broken})\n\tuassert.ErrorIs(t, err, bylaws.ErrInvalidPatch)\n\n\t// Happy path: the definition renders the change, titles by action, and\n\t// applies the supermajority default threshold.\n\tdef, err := amendBylawsKind{}.New(dao.Readonly(), amendBylawsProposal{daoID: dao.ID(), set: set, patch: freshPatch(\"Quorum is half.\")})\n\turequire.NoError(t, err)\n\tabp, ok := def.(amendBylawsProposal)\n\turequire.True(t, ok, \"expect an amendBylawsProposal\")\n\tuassert.Equal(t, \"Add Bylaws Document: bylaws/a.md\", abp.Title())\n\tuassert.True(t, strings.Contains(abp.Body(), \"Quorum is half.\"), \"expect body to show the inserted text\")\n\tuassert.True(t, strings.Contains(abp.Body(), \"bylaws/a\\\\.md\"), \"expect the path escaped in the body\")\n\tuassert.True(t, abp.Threshold() == commondao.ThresholdSupermajority, \"expect the supermajority default\")\n\turequire.True(t, abp.Executor() != nil, \"expect a non-nil executor\")\n}\n\n// TestAmendBylawsIsolation pins that amendments are keyed per DAO: two\n// DAOs hold the same path independently and amending one never touches\n// the other.\nfunc TestAmendBylawsIsolation(t *testing.T) {\n\tdao1 := newTestDAO(t, \"IsoOne\")\n\tdao2 := newTestDAO(t, \"IsoTwo\")\n\tset1, set2 := bylawsOf(dao1.ID()), bylawsOf(dao2.ID())\n\n\tp1, err := set1.Diff(\"bylaws/x.md\", \"text one\")\n\turequire.NoError(t, err)\n\turequire.NoError(t, set1.Apply(p1))\n\n\tp2, err := set2.Diff(\"bylaws/x.md\", \"text two\")\n\turequire.NoError(t, err)\n\turequire.NoError(t, set2.Apply(p2))\n\n\tt1, _ := set1.Get(\"bylaws/x.md\")\n\tt2, _ := set2.Get(\"bylaws/x.md\")\n\tuassert.Equal(t, \"text one\", t1)\n\tuassert.Equal(t, \"text two\", t2)\n\n\t// Amend one; the other is untouched.\n\tp1b, err := set1.Diff(\"bylaws/x.md\", \"\")\n\turequire.NoError(t, err)\n\turequire.NoError(t, set1.Apply(p1b))\n\tuassert.False(t, set1.Has(\"bylaws/x.md\"))\n\tt2, _ = set2.Get(\"bylaws/x.md\")\n\tuassert.Equal(t, \"text two\", t2)\n}\n\n// TestAmendBylawsLifecycle pins the executor and the stale re-check: the\n// definition applies its patch on execution, and a competing amendment\n// that lost the race fails cleanly without clobbering.\nfunc TestAmendBylawsLifecycle(t *testing.T) {\n\tdao := newTestDAO(t, \"BylawsLifecycle\")\n\tset := bylawsOf(dao.ID())\n\n\tnewDef := func(text string) amendBylawsProposal {\n\t\tt.Helper()\n\t\tp, err := set.Diff(\"bylaws/spend.md\", text)\n\t\turequire.NoError(t, err)\n\t\tdef, err := amendBylawsKind{}.New(dao.Readonly(), amendBylawsProposal{daoID: dao.ID(), set: set, patch: p})\n\t\turequire.NoError(t, err)\n\t\treturn def.(amendBylawsProposal)\n\t}\n\n\t// Create the document through the executor.\n\tcreate := newDef(\"Spend only by vote.\")\n\tuassert.NoError(t, create.Validate())\n\turequire.NoError(t, create.execute(0, nil))\n\ttext, _ := set.Get(\"bylaws/spend.md\")\n\tuassert.Equal(t, \"Spend only by vote.\", text)\n\n\t// Two amendments race from the same base: the first applies, the\n\t// second is stale at Validate AND at execute, and clobbers nothing.\n\twinner := newDef(\"Spend only by supermajority vote.\")\n\tloser := newDef(\"Spend freely.\")\n\turequire.NoError(t, winner.execute(0, nil))\n\tuassert.ErrorIs(t, loser.Validate(), bylaws.ErrStalePatch)\n\tuassert.ErrorIs(t, loser.execute(0, nil), bylaws.ErrStalePatch)\n\ttext, _ = set.Get(\"bylaws/spend.md\")\n\tuassert.Equal(t, \"Spend only by supermajority vote.\", text)\n\n\t// Removal: an empty proposed text removes the document.\n\tremove := newDef(\"\")\n\tuassert.Equal(t, \"Remove Bylaws Document: bylaws/spend.md\", remove.Title())\n\turequire.NoError(t, remove.execute(0, nil))\n\tuassert.False(t, set.Has(\"bylaws/spend.md\"))\n}\n"},{"name":"proposal_council.gno","body":"package commondao\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/nt/commondao/v0\"\n)\n\n// newCouncilUpdatePropDefinition creates a new proposal definition for\n// adding/removing council members.\nfunc newCouncilUpdatePropDefinition(dao *commondao.CommonDAO, add, remove []address) councilUpdatePropDefinition {\n\tif dao == nil {\n\t\tpanic(\"DAO is required\")\n\t}\n\n\tif len(add) == 0 \u0026\u0026 len(remove) == 0 {\n\t\tpanic(\"no council members were specified to be added or removed\")\n\t}\n\n\treturn councilUpdatePropDefinition{\n\t\tdao:      dao,\n\t\ttoAdd:    add,\n\t\ttoRemove: remove,\n\t}\n}\n\n// councilUpdatePropDefinition defines a proposal type for adding/removing\n// council members. Adds and removes apply as idempotent set operations, so\n// concurrently passed updates merge in execution order; an update whose\n// final set would empty a non-empty council fails at execution.\ntype councilUpdatePropDefinition struct {\n\tdao             *commondao.CommonDAO\n\ttoAdd, toRemove []address\n}\n\nfunc (councilUpdatePropDefinition) Title() string               { return \"Council Update\" }\nfunc (councilUpdatePropDefinition) VotingPeriod() time.Duration { return time.Hour * 24 * 7 }\n\n// isTrustedMarkdownBody marks Body as self-assembled markdown; the renderer\n// renders it verbatim (embedded addresses are formatted by md helpers).\nfunc (councilUpdatePropDefinition) isTrustedMarkdownBody() {}\n\n// CapExempt exempts council updates from the active proposals cap: a\n// council member who fills the cap must never be able to block their own\n// removal. Exempt proposals are bounded to one active one per creator.\nfunc (councilUpdatePropDefinition) CapExempt() {}\n\n// Threshold returns the tally threshold: council self-mutation requires a\n// supermajority.\nfunc (councilUpdatePropDefinition) Threshold() commondao.Threshold {\n\treturn commondao.ThresholdSupermajority\n}\n\nfunc (p councilUpdatePropDefinition) Body() string {\n\tvar b strings.Builder\n\n\tif len(p.toAdd) \u003e 0 {\n\t\tb.WriteString(md.Paragraph(\n\t\t\tmd.Bold(\"Council Members to Add:\") + \"\\n\" + md.BulletList(addrStrings(p.toAdd)),\n\t\t))\n\t}\n\n\tif len(p.toRemove) \u003e 0 {\n\t\tb.WriteString(md.Paragraph(\n\t\t\tmd.Bold(\"Council Members to Remove:\") + \"\\n\" + md.BulletList(addrStrings(p.toRemove)),\n\t\t))\n\t}\n\n\treturn b.String()\n}\n\nfunc (p councilUpdatePropDefinition) Validate() error {\n\t// Membership is intentionally not validated: adds and removes are\n\t// idempotent, so updates passed concurrently merge cleanly instead of\n\t// failing on addresses that another update already added or removed.\n\tfor _, a := range p.toAdd {\n\t\tfor _, r := range p.toRemove {\n\t\t\tif a == r {\n\t\t\t\treturn errors.New(\"address is added and removed at once: \" + a.String())\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (p councilUpdatePropDefinition) Executor() commondao.ExecFunc {\n\treturn p.execute\n}\n\nfunc (p councilUpdatePropDefinition) execute(_ int, sub realm) error {\n\treturn p.dao.UpdateCouncil(p.toAdd, p.toRemove)\n}\n\n// newAncestorCouncilUpdatePropDefinition creates a proposal definition for\n// an ancestor DAO to modify a descendant's council membership\n// (docs/CONSTITUTION.md :1531-1532). It is hosted and voted in the\n// ancestor; ancestry is verified at proposal validation.\nfunc newAncestorCouncilUpdatePropDefinition(dao, target *commondao.CommonDAO, add, remove []address) ancestorCouncilUpdatePropDefinition {\n\tif dao == nil {\n\t\tpanic(\"DAO is required\")\n\t}\n\tif target == nil {\n\t\tpanic(\"target DAO is required\")\n\t}\n\tif len(add) == 0 \u0026\u0026 len(remove) == 0 {\n\t\tpanic(\"no council members were specified to be added or removed\")\n\t}\n\n\treturn ancestorCouncilUpdatePropDefinition{\n\t\tdao:      dao,\n\t\ttarget:   target,\n\t\ttoAdd:    add,\n\t\ttoRemove: remove,\n\t}\n}\n\n// ancestorCouncilUpdatePropDefinition defines a proposal type for an\n// ancestor DAO to add and/or remove members of a descendant's council.\n// This is the spec's rescue path for a stuck or empty descendant council\n// (:1531-1532): decided by the ancestor's own supermajority, validated as\n// strictly proper ancestry so a DAO can never mutate its own council\n// through this path (that is the self-mutating councilUpdate).\ntype ancestorCouncilUpdatePropDefinition struct {\n\tdao             *commondao.CommonDAO // proposing DAO, must be a proper ancestor\n\ttarget          *commondao.CommonDAO\n\ttoAdd, toRemove []address\n}\n\nfunc (ancestorCouncilUpdatePropDefinition) Title() string               { return \"Ancestor Council Update\" }\nfunc (ancestorCouncilUpdatePropDefinition) VotingPeriod() time.Duration { return time.Hour * 24 * 7 }\n\n// isTrustedMarkdownBody marks Body as self-assembled markdown.\nfunc (ancestorCouncilUpdatePropDefinition) isTrustedMarkdownBody() {}\n\n// Threshold returns the tally threshold: ancestor council modification\n// requires a supermajority (:1531-1532).\nfunc (ancestorCouncilUpdatePropDefinition) Threshold() commondao.Threshold {\n\treturn commondao.ThresholdSupermajority\n}\n\nfunc (p ancestorCouncilUpdatePropDefinition) Body() string {\n\tvar b strings.Builder\n\n\tb.WriteString(md.Paragraph(md.Bold(\"Target DAO:\") + \"\\n\" + daoMDLink(p.target)))\n\n\tif len(p.toAdd) \u003e 0 {\n\t\tb.WriteString(md.Paragraph(\n\t\t\tmd.Bold(\"Council Members to Add:\") + \"\\n\" + md.BulletList(addrStrings(p.toAdd)),\n\t\t))\n\t}\n\n\tif len(p.toRemove) \u003e 0 {\n\t\tb.WriteString(md.Paragraph(\n\t\t\tmd.Bold(\"Council Members to Remove:\") + \"\\n\" + md.BulletList(addrStrings(p.toRemove)),\n\t\t))\n\t}\n\n\treturn b.String()\n}\n\nfunc (p ancestorCouncilUpdatePropDefinition) Validate() error {\n\tif err := assertIsProperAncestor(p.dao, p.target); err != nil {\n\t\treturn err\n\t}\n\n\t// Same overlap check as the self-mutating update: adds and removes are\n\t// idempotent, so only a contradictory same-address add+remove is rejected.\n\tfor _, a := range p.toAdd {\n\t\tfor _, r := range p.toRemove {\n\t\t\tif a == r {\n\t\t\t\treturn errors.New(\"address is added and removed at once: \" + a.String())\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (p ancestorCouncilUpdatePropDefinition) Executor() commondao.ExecFunc {\n\treturn p.execute\n}\n\nfunc (p ancestorCouncilUpdatePropDefinition) execute(_ int, sub realm) error {\n\treturn p.target.UpdateCouncil(p.toAdd, p.toRemove)\n}\n\n// addrStrings converts addresses for markdown list rendering.\nfunc addrStrings(addrs []address) []string {\n\titems := make([]string, 0, len(addrs))\n\tfor _, a := range addrs {\n\t\titems = append(items, a.String())\n\t}\n\treturn items\n}\n"},{"name":"proposal_council_test.gno","body":"package commondao\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nvar (\n\t_ commondao.ProposalDefinition = (*councilUpdatePropDefinition)(nil)\n\t_ commondao.CapExempt          = (*councilUpdatePropDefinition)(nil)\n)\n\nfunc TestCouncilUpdatePropDefinitionNew(cur realm, t *testing.T) {\n\tvar member address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\n\tdao := commondao.New(commondao.WithCouncilMember(member))\n\n\turequire.PanicsWithMessage(t, cur, \"DAO is required\", func() {\n\t\tnewCouncilUpdatePropDefinition(nil, nil, nil)\n\t})\n\n\turequire.PanicsWithMessage(t, cur, \"no council members were specified to be added or removed\", func() {\n\t\tnewCouncilUpdatePropDefinition(dao, nil, nil)\n\t})\n\n\tdef := newCouncilUpdatePropDefinition(dao, []address{member}, nil)\n\n\tuassert.Equal(t, \"Council Update\", def.Title())\n\tuassert.Equal(t, int(commondao.ThresholdSupermajority), int(def.Threshold()))\n\tuassert.NotEmpty(t, def.Body())\n}\n\nfunc TestCouncilUpdatePropDefinitionValidate(t *testing.T) {\n\tvar (\n\t\tmemberA address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\t\tmemberB address = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n\t)\n\n\tdao := commondao.New(commondao.WithCouncilMember(memberA))\n\n\t// Membership is not validated: adding an existing member or removing a\n\t// stranger is legal (idempotent set semantics)\n\tdef := newCouncilUpdatePropDefinition(dao, []address{memberA}, []address{memberB})\n\tuassert.NoError(t, def.Validate())\n\n\t// Adding and removing the same address is rejected\n\tdef = newCouncilUpdatePropDefinition(dao, []address{memberB}, []address{memberB})\n\tuassert.ErrorContains(t, def.Validate(), \"address is added and removed at once\")\n}\n\nfunc TestCouncilUpdatePropDefinitionExecute(cur realm, t *testing.T) {\n\tvar (\n\t\tmemberA address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\t\tmemberB address = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n\t)\n\n\tt.Run(\"add and remove\", func(t *testing.T) {\n\t\tdao := commondao.New(commondao.WithCouncilMember(memberA))\n\n\t\tdef := newCouncilUpdatePropDefinition(dao, []address{memberB}, []address{memberA})\n\n\t\turequire.NotPanics(t, cur, func() {\n\t\t\tif err := def.Executor()(0, cur); err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t})\n\t\tuassert.True(t, dao.Council().Has(memberB))\n\t\tuassert.False(t, dao.Council().Has(memberA))\n\t\tuassert.Equal(t, 1, dao.Council().Size())\n\t})\n\n\tt.Run(\"emptying the council fails\", func(t *testing.T) {\n\t\tdao := commondao.New(commondao.WithCouncilMember(memberA))\n\n\t\tdef := newCouncilUpdatePropDefinition(dao, nil, []address{memberA})\n\n\t\terr := def.execute(0, cur)\n\t\tuassert.ErrorIs(t, err, commondao.ErrEmptyCouncil)\n\t\tuassert.Equal(t, 1, dao.Council().Size())\n\t})\n}\n\nfunc TestAncestorCouncilUpdatePropDefinition(cur realm, t *testing.T) {\n\tvar member address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\n\troot := commondao.New(commondao.WithID(201), commondao.WithName(\"Root\"))\n\tmid := commondao.New(commondao.WithID(202), commondao.WithName(\"Mid\"), commondao.WithParent(root))\n\tleaf := commondao.New(commondao.WithID(203), commondao.WithName(\"Leaf\"), commondao.WithParent(mid), commondao.WithCouncilMember(member))\n\n\turequire.PanicsWithMessage(t, cur, \"DAO is required\", func() {\n\t\tnewAncestorCouncilUpdatePropDefinition(nil, leaf, []address{member}, nil)\n\t})\n\turequire.PanicsWithMessage(t, cur, \"target DAO is required\", func() {\n\t\tnewAncestorCouncilUpdatePropDefinition(root, nil, []address{member}, nil)\n\t})\n\turequire.PanicsWithMessage(t, cur, \"no council members were specified to be added or removed\", func() {\n\t\tnewAncestorCouncilUpdatePropDefinition(root, leaf, nil, nil)\n\t})\n\n\tdef := newAncestorCouncilUpdatePropDefinition(root, leaf, []address{member}, nil)\n\tuassert.Equal(t, \"Ancestor Council Update\", def.Title())\n\tuassert.Equal(t, int(commondao.ThresholdSupermajority), int(def.Threshold()))\n\tuassert.NotEmpty(t, def.Body())\n\n\t// Any proper ancestor qualifies (parent or grandparent); self and\n\t// descendants do not.\n\tuassert.NoError(t, newAncestorCouncilUpdatePropDefinition(root, leaf, []address{member}, nil).Validate())\n\tuassert.NoError(t, newAncestorCouncilUpdatePropDefinition(mid, leaf, []address{member}, nil).Validate())\n\tuassert.ErrorContains(t,\n\t\tnewAncestorCouncilUpdatePropDefinition(leaf, leaf, []address{member}, nil).Validate(),\n\t\t\"a DAO cannot target itself\")\n\tuassert.ErrorContains(t,\n\t\tnewAncestorCouncilUpdatePropDefinition(leaf, root, []address{member}, nil).Validate(),\n\t\t\"DAO is not an ancestor of the target DAO\")\n\n\t// A contradictory same-address add+remove is rejected.\n\tuassert.ErrorContains(t,\n\t\tnewAncestorCouncilUpdatePropDefinition(root, leaf, []address{member}, []address{member}).Validate(),\n\t\t\"is added and removed at once\")\n\n\t// The executor mutates the target's council.\n\tvar newMember address = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n\turequire.NoError(t, newAncestorCouncilUpdatePropDefinition(root, leaf, []address{newMember}, []address{member}).Executor()(0, cur))\n\tuassert.True(t, leaf.Council().Has(newMember))\n\tuassert.False(t, leaf.Council().Has(member))\n}\n"},{"name":"proposal_kinds.gno","body":"package commondao\n\nimport (\n\t\"chain\"\n\t\"errors\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/nt/commondao/v0\"\n)\n\n// Proposal kind names: the per-DAO registry keys for every proposal type\n// this realm can host. A DAO accepts proposals of exactly the kinds\n// registered on it. The default kinds are registered at DAO creation and\n// the kind set is managed afterwards through the manage-kinds kind\n// (CreateRegisterKindProposal / CreateDeregisterKindProposal).\nconst (\n\tkindText                  = \"text\"\n\tkindCouncilUpdate         = \"council-update\"\n\tkindAncestorCouncilUpdate = \"ancestor-council-update\"\n\tkindSubDAO                = \"subdao\"\n\tkindDissolve              = \"dissolve\"\n\tkindTreasurySpend         = \"treasury-spend\"\n\tkindTreasuryClawback      = \"treasury-clawback\"\n\tkindTreasuryFreeze        = \"treasury-freeze\"\n\tkindManageKinds           = \"manage-kinds\"\n\tkindAmendBylaws           = \"amend-bylaws\"\n\n\t// Opt-in kind. It is part of the catalog (so catalogKind resolves it and\n\t// manage-kinds can register it by name) but is NOT default-seeded: a DAO\n\t// gains it only after a supermajority register.\n\t//\n\t// It is a realm-side kind (executionKind), not the bare /p/\n\t// commondao.ExecutionKind: the realm wraps the arbitrary-exec closure with\n\t// its own Validable freeze policy so a frozen DAO cannot drain its own\n\t// treasury through an execution proposal (the /p/ kind carries no such\n\t// policy). The name still matches the /p/ kind's name.\n\tkindExecution = \"execution\"\n)\n\n// errInvalidProposalArgs reports a proposal kind invoked with the wrong\n// args type. Wrappers and kinds live in the same package, so this only\n// fires on a realm bug, never on user input.\nvar errInvalidProposalArgs = errors.New(\"invalid proposal arguments\")\n\n// defaultProposalKinds lists the proposal kinds seeded on every new DAO\n// (genesis, user-created or sub-DAO). Kinds are stateless singletons that\n// wrap the definition constructors. This is the DAO's starting governance\n// surface; the kind set is managed afterwards through the manage-kinds kind.\n// Ordering here is immaterial — render.gno owns the presentation order.\nvar defaultProposalKinds = []commondao.ProposalKind{\n\ttextKind{},\n\tcouncilUpdateKind{},\n\tancestorCouncilUpdateKind{},\n\tsubDAOKind{},\n\tdissolveKind{},\n\ttreasurySpendKind{},\n\ttreasuryClawbackKind{},\n\ttreasuryFreezeKind{},\n\tmanageKindsKind{},\n\tamendBylawsKind{},\n}\n\n// optInProposalKinds lists the opt-in proposal kinds: they are part of the\n// catalog but NOT default-seeded, so a DAO gains them only after a\n// supermajority manage-kinds register. The execution kind is realm-side\n// (executionKind), not the bare /p/ ExecutionKind, so it can carry the\n// realm's freeze policy (see kindExecution).\nvar optInProposalKinds = []commondao.ProposalKind{\n\texecutionKind{},\n}\n\n// proposalKindCatalog lists every proposal kind this realm can host: the\n// default-seeded kinds plus the opt-in kind (execution). catalogKind\n// resolves against this list, so manage-kinds can register an opt-in kind by\n// name that is not seeded. The executor set stays closed because only catalog\n// kinds are ever registered on a DAO by name. Ordering here is immaterial —\n// render.gno owns the presentation order.\nvar proposalKindCatalog = append(\n\tappend([]commondao.ProposalKind{}, defaultProposalKinds...),\n\toptInProposalKinds...,\n)\n\n// catalogKind returns a catalog kind by name, or nil when the name is not\n// part of the catalog.\nfunc catalogKind(name string) commondao.ProposalKind {\n\tfor _, k := range proposalKindCatalog {\n\t\tif k.Name() == name {\n\t\t\treturn k\n\t\t}\n\t}\n\treturn nil\n}\n\n// HasProposalKind reports whether a proposal kind is registered on a DAO.\nfunc HasProposalKind(daoID uint64, name string) bool {\n\treturn mustGetDAO(daoID).HasKind(name)\n}\n\n// textArgs carries CreateTextProposal parameters to the text kind.\ntype textArgs struct {\n\ttitle        string\n\tbody         string\n\tvotingPeriod time.Duration\n}\n\n// textKind creates general text proposals.\ntype textKind struct{}\n\nfunc (textKind) Name() string { return kindText }\n\nfunc (textKind) New(_ commondao.ReadonlyCommonDAO, args any) (commondao.ProposalDefinition, error) {\n\ta, ok := args.(textArgs)\n\tif !ok {\n\t\treturn nil, errInvalidProposalArgs\n\t}\n\treturn newTextPropDefinition(a.title, a.body, a.votingPeriod), nil\n}\n\n// councilUpdateArgs carries CreateCouncilUpdateProposal parameters to the\n// council-update kind. dao is the host DAO whose own council the executor\n// mutates: New receives only a readonly view, so the trusted wrapper passes\n// the mutable host handle through args (captured from its own mustGetDAO).\ntype councilUpdateArgs struct {\n\tdao    *commondao.CommonDAO\n\tadd    []address\n\tremove []address\n}\n\n// councilUpdateKind creates proposals that add and/or remove members of\n// the host DAO's own council.\ntype councilUpdateKind struct{}\n\nfunc (councilUpdateKind) Name() string { return kindCouncilUpdate }\n\nfunc (councilUpdateKind) New(_ commondao.ReadonlyCommonDAO, args any) (commondao.ProposalDefinition, error) {\n\ta, ok := args.(councilUpdateArgs)\n\tif !ok {\n\t\treturn nil, errInvalidProposalArgs\n\t}\n\treturn newCouncilUpdatePropDefinition(a.dao, a.add, a.remove), nil\n}\n\n// ancestorCouncilUpdateArgs carries CreateAncestorCouncilUpdateProposal\n// parameters to the ancestor-council-update kind. host is the proposing\n// ancestor DAO (read for the ancestry check); target is the descendant\n// whose council the executor mutates. Both handles come from the trusted\n// wrapper via args, since New receives only a readonly view.\ntype ancestorCouncilUpdateArgs struct {\n\thost   *commondao.CommonDAO\n\ttarget *commondao.CommonDAO\n\tadd    []address\n\tremove []address\n}\n\n// ancestorCouncilUpdateKind creates proposals for the host DAO, as an\n// ancestor, to add and/or remove members of a descendant's council.\ntype ancestorCouncilUpdateKind struct{}\n\nfunc (ancestorCouncilUpdateKind) Name() string { return kindAncestorCouncilUpdate }\n\nfunc (ancestorCouncilUpdateKind) New(_ commondao.ReadonlyCommonDAO, args any) (commondao.ProposalDefinition, error) {\n\ta, ok := args.(ancestorCouncilUpdateArgs)\n\tif !ok {\n\t\treturn nil, errInvalidProposalArgs\n\t}\n\treturn newAncestorCouncilUpdatePropDefinition(a.host, a.target, a.add, a.remove), nil\n}\n\n// subDAOArgs carries CreateSubDAOProposal parameters to the subdao kind.\n// parent is the host DAO the new SubDAO is created under; the executor\n// mutates it (wiring the child in), so the trusted wrapper passes the\n// mutable host handle through args, since New receives only a readonly view.\ntype subDAOArgs struct {\n\tparent      *commondao.CommonDAO\n\tname        string\n\tpurpose     string\n\tdescription string\n\tmembers     []address\n}\n\n// subDAOKind creates proposals that add a SubDAO under the host DAO.\ntype subDAOKind struct{}\n\nfunc (subDAOKind) Name() string { return kindSubDAO }\n\nfunc (subDAOKind) New(_ commondao.ReadonlyCommonDAO, args any) (commondao.ProposalDefinition, error) {\n\ta, ok := args.(subDAOArgs)\n\tif !ok {\n\t\treturn nil, errInvalidProposalArgs\n\t}\n\treturn newSubDAOPropDefinition(a.parent, a.name, a.purpose, a.description, a.members), nil\n}\n\n// dissolveArgs carries CreateDissolutionProposal parameters to the\n// dissolve kind, including the DAO being dissolved: the proposal is hosted\n// in the nearest live ancestor (the host DAO that Propose passes to New),\n// so the definition must operate on the dissolved descendant carried here,\n// never on the host.\ntype dissolveArgs struct {\n\tdissolveDAO *commondao.CommonDAO\n\tdestination address // sweep destination, root DAOs only\n}\n\n// dissolveKind creates proposals that dissolve a DAO or SubDAO.\ntype dissolveKind struct{}\n\nfunc (dissolveKind) Name() string { return kindDissolve }\n\nfunc (dissolveKind) New(_ commondao.ReadonlyCommonDAO, args any) (commondao.ProposalDefinition, error) {\n\ta, ok := args.(dissolveArgs)\n\tif !ok {\n\t\treturn nil, errInvalidProposalArgs\n\t}\n\treturn newDissolvePropDefinition(a.dissolveDAO, a.destination), nil\n}\n\n// treasurySpendArgs carries CreateTreasurySpendProposal parameters to the\n// treasury-spend kind. dao is the host DAO whose own treasury funds the\n// spend (its sub is the funding source, see FundingDAOID); the trusted\n// wrapper passes the mutable host handle through args, since New receives\n// only a readonly view.\ntype treasurySpendArgs struct {\n\tdao  *commondao.CommonDAO\n\tto   address\n\tcoin chain.Coin\n}\n\n// treasurySpendKind creates proposals that send coins from the host DAO's\n// own treasury.\ntype treasurySpendKind struct{}\n\nfunc (treasurySpendKind) Name() string { return kindTreasurySpend }\n\nfunc (treasurySpendKind) New(_ commondao.ReadonlyCommonDAO, args any) (commondao.ProposalDefinition, error) {\n\ta, ok := args.(treasurySpendArgs)\n\tif !ok {\n\t\treturn nil, errInvalidProposalArgs\n\t}\n\treturn newTreasurySpendPropDefinition(a.dao, a.to, a.coin), nil\n}\n\n// treasuryClawbackArgs carries CreateTreasuryClawbackProposal parameters\n// to the treasury-clawback kind. host is the proposing ancestor DAO (read\n// for the ancestry check); target is the descendant whose treasury the\n// executor sweeps (its sub is the funding source, see FundingDAOID). Both\n// handles come from the trusted wrapper via args, since New receives only\n// a readonly view.\ntype treasuryClawbackArgs struct {\n\thost   *commondao.CommonDAO\n\ttarget *commondao.CommonDAO\n}\n\n// treasuryClawbackKind creates proposals for the host DAO, as an ancestor,\n// to sweep a descendant DAO's treasury to the descendant's parent.\ntype treasuryClawbackKind struct{}\n\nfunc (treasuryClawbackKind) Name() string { return kindTreasuryClawback }\n\nfunc (treasuryClawbackKind) New(_ commondao.ReadonlyCommonDAO, args any) (commondao.ProposalDefinition, error) {\n\ta, ok := args.(treasuryClawbackArgs)\n\tif !ok {\n\t\treturn nil, errInvalidProposalArgs\n\t}\n\treturn newTreasuryClawbackPropDefinition(a.host, a.target), nil\n}\n\n// treasuryFreezeArgs carries CreateTreasuryFreezeProposal parameters to\n// the treasury-freeze kind. host is the proposing ancestor DAO (read for\n// the ancestry / orphan-rescue check); target is the descendant whose\n// treasury the executor freezes or unfreezes. Both handles come from the\n// trusted wrapper via args, since New receives only a readonly view.\ntype treasuryFreezeArgs struct {\n\thost   *commondao.CommonDAO\n\ttarget *commondao.CommonDAO\n\tfrozen bool\n}\n\n// treasuryFreezeKind creates proposals for the host DAO, as an ancestor,\n// to freeze or unfreeze a descendant DAO's treasury.\ntype treasuryFreezeKind struct{}\n\nfunc (treasuryFreezeKind) Name() string { return kindTreasuryFreeze }\n\nfunc (treasuryFreezeKind) New(_ commondao.ReadonlyCommonDAO, args any) (commondao.ProposalDefinition, error) {\n\ta, ok := args.(treasuryFreezeArgs)\n\tif !ok {\n\t\treturn nil, errInvalidProposalArgs\n\t}\n\treturn newTreasuryFreezePropDefinition(a.host, a.target, a.frozen), nil\n}\n\n// errTreasuryFrozen reports an execution proposal blocked because the host\n// DAO's treasury is frozen. It is a sentinel so the freeze gate is greppable\n// and testable (the create-time panic and the Validate-time failure share\n// this one message).\nvar errTreasuryFrozen = errors.New(\"commondao: treasury is frozen\")\n\n// executionArgs carries CreateExecutionProposal parameters to the execution\n// kind: a title, a body, and the closure executed on approval. Unlike the\n// governance kinds, the execution kind captures no mutable handle — its only\n// use of the DAO is a freeze-flag read, so its definition holds the readonly\n// host view Propose passes to New rather than a *CommonDAO from args.\ntype executionArgs struct {\n\ttitle string\n\tbody  string\n\tfn    commondao.ExecFunc\n}\n\n// executionKind creates proposals that run an arbitrary ExecFunc as the host\n// DAO's own sub on approval. It is the realm-side counterpart of the /p/\n// commondao.ExecutionKind: identical arbitrary-exec mechanism, but wrapped\n// with the realm's Validable freeze policy (executionPropDefinition.Validate)\n// so a frozen DAO cannot drain its own treasury through it — the /p/ kind\n// carries no such policy. This is the pattern the /p/ extension docs\n// recommend for an arbitrary-exec closure: wrap it with your realm's own\n// checks.\ntype executionKind struct{}\n\nfunc (executionKind) Name() string { return kindExecution }\n\nfunc (executionKind) New(dao commondao.ReadonlyCommonDAO, args any) (commondao.ProposalDefinition, error) {\n\ta, ok := args.(executionArgs)\n\tif !ok {\n\t\treturn nil, errInvalidProposalArgs\n\t}\n\tif a.fn == nil {\n\t\treturn nil, commondao.ErrExecutionFuncRequired\n\t}\n\t// The definition captures the readonly host view Propose passed here, not a\n\t// handle from args: its only use of the DAO is the Validate freeze read. No\n\t// mutable handle means no host-identity pin is needed — the view is the\n\t// host by construction.\n\treturn executionPropDefinition{dao: dao, title: a.title, body: a.body, fn: a.fn}, nil\n}\n\n// executionPropDefinition defines a proposal that runs an arbitrary ExecFunc\n// as the host DAO's own sub on approval. Its Validate blocks execution while\n// the host treasury is frozen, so an execution proposal can never move funds\n// out of a frozen DAO — matching the treasury-spend freeze gate.\ntype executionPropDefinition struct {\n\tdao   commondao.ReadonlyCommonDAO\n\ttitle string\n\tbody  string\n\tfn    commondao.ExecFunc\n}\n\n// Title returns raw, user-supplied text; the renderer escapes every\n// definition title.\nfunc (p executionPropDefinition) Title() string { return p.title }\n\n// isTrustedMarkdownBody marks Body as self-assembled: it prepends the\n// realm's own standing warning and escapes the proposer's text itself.\nfunc (executionPropDefinition) isTrustedMarkdownBody() {}\n\n// Body prefixes the proposer's description with a disclosure. The closure\n// is frozen at Propose, so what executes cannot change after voting starts\n// — but it also cannot be shown: a function value has no rendering, and\n// the title and description are whatever the proposer chose to write. A\n// council voting on this kind is approving code it cannot read, so say so\n// rather than let prose stand alone.\nfunc (p executionPropDefinition) Body() string {\n\treturn md.Blockquote(\"⚠ This proposal runs arbitrary code with the DAO's own authority, \"+\n\t\t\"including its treasury. The code is fixed when the proposal is created but cannot be \"+\n\t\t\"displayed here — verify the proposing realm before voting.\") +\n\t\tmd.Paragraph(md.EscapeText(p.body))\n}\n\nfunc (executionPropDefinition) VotingPeriod() time.Duration { return time.Hour * 24 * 7 }\n\n// Threshold returns the tally threshold: arbitrary execution runs code under\n// the DAO's authority, so the supermajority default applies.\nfunc (executionPropDefinition) Threshold() commondao.Threshold {\n\treturn commondao.ThresholdSupermajority\n}\n\n// Validate runs at proposal creation and again inside Execute: a treasury\n// frozen after the proposal passed still blocks it cleanly (StatusFailed, no\n// funds leave) instead of letting the closure run against a frozen DAO. Freeze\n// = no self-initiated treasury movement, spend and execution alike.\nfunc (p executionPropDefinition) Validate() error {\n\tif p.dao.IsTreasuryFrozen() {\n\t\treturn errTreasuryFrozen\n\t}\n\treturn nil\n}\n\nfunc (p executionPropDefinition) Executor() commondao.ExecFunc {\n\treturn p.fn\n}\n\n// manageKindsProposal is both the manage-kinds args struct and the proposal\n// definition it produces: manageKindsKind.New validates it and returns it\n// unchanged (one type serves both the args and definition roles). dao is the\n// host DAO whose registry the executor\n// mutates: New receives only a readonly view, so the trusted wrapper passes\n// the mutable host handle through args (captured from its own mustGetDAO).\n//\n// A proposal is one of two shapes, both populated only by a trusted\n// wrapper and both by name (the name resolves against the realm catalog):\n//   - register:   remove=false, name set;\n//   - deregister: remove=true,  name set.\n//\n// Registering a foreign kind by value is intentionally not offered here: on\n// this realm such a kind would be inert (no propose path). The by-value\n// capability stays available in /p/ (WithProposalKind / RegisterKind) for a\n// downstream realm that authors its own propose wrapper (see /p/ doc.gno,\n// \"Extending commondao in your own realm\").\ntype manageKindsProposal struct {\n\tdao    *commondao.CommonDAO\n\tremove bool\n\tname   string\n}\n\n// manageKindsKind creates governance proposals that register or deregister\n// a catalog proposal kind on the host DAO by name — the DAO's one permanent\n// capability to manage which kinds it accepts. Registering adds a kind;\n// deregistering removes one, which blocks new proposals of that kind while\n// in-flight ones still vote and execute. It is decided by supermajority and\n// cannot itself be deregistered (the self-brick guard below), so a DAO\n// always keeps the ability to manage its kind set.\ntype manageKindsKind struct{}\n\nfunc (manageKindsKind) Name() string { return kindManageKinds }\n\nfunc (manageKindsKind) New(dao commondao.ReadonlyCommonDAO, args any) (commondao.ProposalDefinition, error) {\n\tp, ok := args.(manageKindsProposal)\n\tif !ok {\n\t\treturn nil, errInvalidProposalArgs\n\t}\n\t// Defense in depth: this kind captures a mutable host handle from args (it\n\t// must, to register/deregister on Execute) while the checks below read the\n\t// readonly host Propose passed here. Pin them to the same DAO so a future\n\t// wrapper can never validate against one registry and mutate another (the\n\t// trusted wrapper always passes matching handles today).\n\tif p.dao.ID() != dao.ID() {\n\t\treturn nil, errInvalidProposalArgs\n\t}\n\n\tif p.remove {\n\t\t// Deregister by name. Reject no-ops so a council vote is always\n\t\t// about an actual change, and reject the self-brick: manage-kinds\n\t\t// is the only un-deregisterable kind, so a DAO can never lose the\n\t\t// ability to manage its kind set.\n\t\tif !dao.HasKind(p.name) {\n\t\t\treturn nil, errors.New(\"proposal kind is not registered: \" + p.name)\n\t\t}\n\t\tif p.name == kindManageKinds {\n\t\t\treturn nil, errors.New(\"the manage-kinds kind cannot be deregistered\")\n\t\t}\n\t\treturn p, nil\n\t}\n\n\t// Register by name: the name must resolve against the realm catalog, and\n\t// reject a no-op (already registered) so a council vote is always about\n\t// an actual change.\n\tif catalogKind(p.name) == nil {\n\t\treturn nil, errors.New(\"unknown proposal kind\")\n\t}\n\tif dao.HasKind(p.name) {\n\t\treturn nil, errors.New(\"proposal kind is already registered\")\n\t}\n\n\treturn p, nil\n}\n\n// Title returns the proposal title as raw text: the renderer escapes\n// every definition title, so escaping the kind name here (unlike in Body,\n// which the renderer trusts as markdown) would double-escape it.\nfunc (p manageKindsProposal) Title() string {\n\tif p.remove {\n\t\treturn \"Deregister Proposal Kind: \" + p.name\n\t}\n\treturn \"Register Proposal Kind: \" + p.name\n}\n\nfunc (manageKindsProposal) VotingPeriod() time.Duration { return time.Hour * 24 * 7 }\n\n// isTrustedMarkdownBody marks Body as self-assembled markdown; the embedded\n// kind name is escaped in Body itself (defense in depth).\nfunc (manageKindsProposal) isTrustedMarkdownBody() {}\n\n// Threshold returns the tally threshold: changing which proposal kinds a\n// DAO accepts alters its governance surface, so the supermajority default\n// applies.\nfunc (manageKindsProposal) Threshold() commondao.Threshold {\n\treturn commondao.ThresholdSupermajority\n}\n\n// Validate re-asserts the self-brick at Execute time (Validate reruns\n// inside Execute): the manage-kinds kind can never be deregistered, so a\n// DAO always keeps the ability to manage its kind set. This is a\n// defense-in-depth second layer behind the New check.\nfunc (p manageKindsProposal) Validate() error {\n\tif p.remove \u0026\u0026 p.name == kindManageKinds {\n\t\treturn errors.New(\"the manage-kinds kind cannot be deregistered\")\n\t}\n\treturn nil\n}\n\nfunc (p manageKindsProposal) Body() string {\n\tvar b strings.Builder\n\n\t// The kind name is validated against the catalog; it is escaped anyway\n\t// as defense in depth.\n\tb.WriteString(md.Paragraph(md.Bold(\"Proposal Kind:\") + \"\\n\" + md.EscapeText(p.name)))\n\n\taction := \"registered: new proposals of this kind can be created.\"\n\tif p.remove {\n\t\taction = \"deregistered: no new proposal of this kind can be created. \" +\n\t\t\t\"In-flight proposals of the kind still vote and execute.\"\n\t}\n\tb.WriteString(md.Paragraph(md.Bold(\"Effect:\") + \"\\nThe proposal kind is \" + action))\n\n\treturn b.String()\n}\n\nfunc (p manageKindsProposal) Executor() commondao.ExecFunc {\n\treturn p.execute\n}\n\n// execute registers or deregisters the catalog kind, returning any registry\n// error unchanged so a race between two concurrently passed manage-kinds\n// proposals fails the later one cleanly (StatusFailed) instead of panicking\n// the transaction. catalogKind is a process-global immutable lookup, so a\n// name valid at New still resolves here; the executor does not re-resolve\n// or re-validate beyond the Validate self-brick. It moves no funds, so the\n// definition is not Funded and ignores sub.\nfunc (p manageKindsProposal) execute(_ int, sub realm) error {\n\tif p.remove {\n\t\treturn p.dao.DeregisterKind(p.name)\n\t}\n\treturn p.dao.RegisterKind(catalogKind(p.name))\n}\n"},{"name":"proposal_kinds_test.gno","body":"package commondao\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nvar (\n\t_ commondao.ProposalDefinition = manageKindsProposal{}\n\t_ commondao.Validable          = manageKindsProposal{}\n\t_ commondao.Executable         = manageKindsProposal{}\n)\n\nfunc TestManageKindsKindNew(t *testing.T) {\n\t// The test DAO registers the default kinds; deregister the text kind so\n\t// the table has an absent catalog kind to register and to reject\n\t// deregistering.\n\tdao := newTestDAO(t, \"KindsHost\")\n\turequire.NoError(t, dao.DeregisterKind(kindText))\n\n\tcases := []struct {\n\t\tname string\n\t\targs manageKindsProposal\n\t\terr  string\n\t}{\n\t\t// Register by name.\n\t\t{\"register unknown kind\", manageKindsProposal{dao: dao, name: \"unknown\"}, \"unknown proposal kind\"},\n\t\t{\"register a registered kind\", manageKindsProposal{dao: dao, name: kindCouncilUpdate}, \"proposal kind is already registered\"},\n\t\t{\"register a catalog kind\", manageKindsProposal{dao: dao, name: kindText}, \"\"},\n\t\t{\"register the opt-in execution kind\", manageKindsProposal{dao: dao, name: kindExecution}, \"\"},\n\t\t// Deregister by name.\n\t\t{\"deregister an absent kind\", manageKindsProposal{dao: dao, remove: true, name: kindText}, \"proposal kind is not registered: \" + kindText},\n\t\t{\"deregister the manage-kinds kind\", manageKindsProposal{dao: dao, remove: true, name: kindManageKinds}, \"the manage-kinds kind cannot be deregistered\"},\n\t\t{\"deregister a registered kind\", manageKindsProposal{dao: dao, remove: true, name: kindCouncilUpdate}, \"\"},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tdef, err := manageKindsKind{}.New(dao.Readonly(), tc.args)\n\n\t\t\tif tc.err != \"\" {\n\t\t\t\turequire.Error(t, err)\n\t\t\t\tuassert.Equal(t, tc.err, err.Error())\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.NoError(t, err)\n\t\t\t// New returns the args value unchanged as the definition (the\n\t\t\t// collapsed args/definition type): the two must be equal.\n\t\t\td, ok := def.(manageKindsProposal)\n\t\t\turequire.True(t, ok, \"expect a manageKindsProposal\")\n\t\t\tuassert.True(t, d.dao == dao, \"expect the definition to keep the host DAO\")\n\t\t\tuassert.Equal(t, tc.args.remove, d.remove)\n\t\t\tuassert.Equal(t, tc.args.name, d.name)\n\t\t})\n\t}\n}\n\n// TestExecutionKindNew pins the realm execution kind's factory: args\n// validation, the nil-closure guard, and the Validable freeze policy that\n// blocks execution while the host treasury is frozen. Without this, removing\n// the nil-fn check or the freeze read from executionKind is invisible to the\n// suite (no filetest constructs the kind directly).\nfunc TestExecutionKindNew(t *testing.T) {\n\tdao := newTestDAO(t, \"ExecHost\")\n\tfn := commondao.ExecFunc(func(_ int, _ realm) error { return nil })\n\n\t// Wrong args type is a realm bug, reported as invalid args.\n\t_, err := executionKind{}.New(dao.Readonly(), textArgs{})\n\tuassert.ErrorIs(t, err, errInvalidProposalArgs)\n\n\t// A nil closure is rejected with the /p/ sentinel.\n\t_, err = executionKind{}.New(dao.Readonly(), executionArgs{title: \"T\", body: \"B\", fn: nil})\n\tuassert.ErrorIs(t, err, commondao.ErrExecutionFuncRequired)\n\n\t// Happy path: the definition carries the title, body and closure and\n\t// defaults to a supermajority threshold.\n\tdef, err := executionKind{}.New(dao.Readonly(), executionArgs{title: \"T\", body: \"B\", fn: fn})\n\turequire.NoError(t, err)\n\tepd, ok := def.(executionPropDefinition)\n\turequire.True(t, ok, \"expect an executionPropDefinition\")\n\tuassert.Equal(t, \"T\", epd.Title())\n\t// The body is self-assembled: the realm's arbitrary-code disclosure\n\t// followed by the proposer's (escaped) description.\n\tuassert.True(t, strings.Contains(epd.Body(), \"runs arbitrary code with the DAO's own authority\"),\n\t\t\"expect the arbitrary-code disclosure\")\n\tuassert.True(t, strings.Contains(epd.Body(), \"B\"), \"expect the proposer description\")\n\tuassert.True(t, epd.Threshold() == commondao.ThresholdSupermajority)\n\turequire.True(t, epd.Executor() != nil, \"expect a non-nil executor\")\n\n\t// Freeze policy: Validate passes while unfrozen and blocks once the host\n\t// treasury is frozen (the definition holds the live readonly host view).\n\tuassert.NoError(t, epd.Validate())\n\tdao.SetTreasuryFrozen(true)\n\tuassert.ErrorIs(t, epd.Validate(), errTreasuryFrozen)\n}\n\n// TestManageKindsKindNewHostPin pins the manage-kinds host-identity guard:\n// the kind captures a mutable host handle from args but validates against the\n// readonly host Propose passes, so a handle for a different DAO than the one\n// being proposed on is rejected — a wrapper can never validate one registry\n// and mutate another.\nfunc TestManageKindsKindNewHostPin(t *testing.T) {\n\thost := newTestDAO(t, \"PinHost\")\n\tother := newTestDAO(t, \"PinOther\")\n\n\t_, err := manageKindsKind{}.New(host.Readonly(), manageKindsProposal{dao: other, name: kindText})\n\tuassert.ErrorIs(t, err, errInvalidProposalArgs)\n\n\t// Matching handles pass the pin and then the normal register/deregister\n\t// checks (deregistering a registered default validates cleanly).\n\t_, err = manageKindsKind{}.New(host.Readonly(), manageKindsProposal{dao: host, remove: true, name: kindText})\n\tuassert.NoError(t, err)\n}\n\n// TestManageKindsValidateSelfBrick pins the second (Execute-time) self-brick\n// layer: Validate rejects deregistering the manage-kinds kind even though\n// the /p/ package no longer locks any name. Validate reruns inside Execute.\nfunc TestManageKindsValidateSelfBrick(t *testing.T) {\n\tdao := newTestDAO(t, \"ValidateHost\")\n\n\t// A manage-kinds deregister slips past New (constructed directly here);\n\t// Validate is the backstop that still rejects it.\n\tbrick := manageKindsProposal{dao: dao, remove: true, name: kindManageKinds}\n\terr := brick.Validate()\n\turequire.Error(t, err)\n\tuassert.Equal(t, \"the manage-kinds kind cannot be deregistered\", err.Error())\n\n\t// Deregistering any other kind validates cleanly.\n\tok := manageKindsProposal{dao: dao, remove: true, name: kindText}\n\tuassert.NoError(t, ok.Validate())\n\n\t// A register never trips the self-brick regardless of name.\n\treg := manageKindsProposal{dao: dao, name: kindManageKinds}\n\tuassert.NoError(t, reg.Validate())\n}\n"},{"name":"proposal_subdao.gno","body":"package commondao\n\nimport (\n\t\"chain/banker\"\n\t\"errors\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/nt/commondao/v0\"\n)\n\n// newSubDAOPropDefinition creates a new proposal definition for adding a SubDAO.\nfunc newSubDAOPropDefinition(parent *commondao.CommonDAO, name, purpose, description string, members []address) subDAOPropDefinition {\n\tif parent == nil {\n\t\tpanic(\"parent DAO is required\")\n\t}\n\n\tname = strings.TrimSpace(name)\n\tassertDAONameIsValid(name)\n\n\tpurpose = strings.TrimSpace(purpose)\n\tassertDAOPurposeIsValid(purpose)\n\n\tdescription = strings.TrimSpace(description)\n\tassertDAODescriptionIsValid(description)\n\n\tif len(members) == 0 {\n\t\tpanic(\"a SubDAO requires at least one initial council member\")\n\t}\n\n\treturn subDAOPropDefinition{\n\t\tparent:      parent,\n\t\tname:        name,\n\t\tpurpose:     purpose,\n\t\tdescription: description,\n\t\tmembers:     members,\n\t}\n}\n\n// subDAOPropDefinition defines a proposal type for adding a SubDAO.\ntype subDAOPropDefinition struct {\n\tparent      *commondao.CommonDAO\n\tname        string\n\tpurpose     string\n\tdescription string\n\tmembers     []address\n}\n\nfunc (p subDAOPropDefinition) Title() string             { return \"New SubDAO: \" + p.name }\nfunc (subDAOPropDefinition) VotingPeriod() time.Duration { return time.Hour * 24 * 7 }\n\n// isTrustedMarkdownBody marks Body as self-assembled markdown; the embedded\n// user fields (name, purpose, description) are escaped in Body itself.\nfunc (subDAOPropDefinition) isTrustedMarkdownBody() {}\n\n// Threshold returns the tally threshold: the constitution grants SubDAO\n// creation at a simple majority of the Council.\nfunc (subDAOPropDefinition) Threshold() commondao.Threshold {\n\treturn commondao.ThresholdSimpleMajority\n}\n\nfunc (p subDAOPropDefinition) Body() string {\n\tvar b strings.Builder\n\n\tb.WriteString(md.Paragraph(\n\t\tmd.Bold(\"Parent DAO:\") + \"\\n\" + daoMDLink(p.parent),\n\t))\n\n\tb.WriteString(md.Paragraph(\n\t\tmd.Bold(\"SubDAO Name:\") + \"\\n\" + md.EscapeText(p.name),\n\t))\n\n\tb.WriteString(md.Paragraph(\n\t\tmd.Bold(\"SubDAO Purpose:\") + \"\\n\" + md.EscapeText(p.purpose),\n\t))\n\n\tif p.description != \"\" {\n\t\tb.WriteString(md.Paragraph(\n\t\t\tmd.Bold(\"SubDAO Description:\") + \"\\n\" + md.EscapeText(p.description),\n\t\t))\n\t}\n\n\tb.WriteString(md.Paragraph(\n\t\tmd.Bold(\"Council Members:\") + \"\\n\" + md.BulletList(addrStrings(p.members)),\n\t))\n\n\treturn b.String()\n}\n\nfunc (p subDAOPropDefinition) Validate() (err error) {\n\tp.parent.IterateChildren(func(subDAO *commondao.CommonDAO) bool {\n\t\tif subDAO.Name() == p.name {\n\t\t\terr = errors.New(\"a SubDAO with the same name already exists\")\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t})\n\treturn err\n}\n\nfunc (p subDAOPropDefinition) Executor() commondao.ExecFunc {\n\treturn p.execute\n}\n\nfunc (p subDAOPropDefinition) execute(_ int, sub realm) error {\n\tcreateSubDAO(p.parent, p.name, p.purpose, p.description, p.members...)\n\treturn nil\n}\n\n// newDissolvePropDefinition creates a new proposal definition for\n// dissolving a DAO. Dissolution sweeps any remaining treasury balance:\n// sub-DAO sweeps go to the parent (fixed, not nameable — the same place\n// a clawback would put the funds); a root DAO has no parent, so its\n// dissolution requires an explicit destination. Both rules are\n// state-independent: parent pointers never change after construction.\nfunc newDissolvePropDefinition(dao *commondao.CommonDAO, destination address) dissolvePropDefinition {\n\tif dao == nil {\n\t\tpanic(\"SubDAO is required\")\n\t}\n\tif dao.Parent() != nil {\n\t\tif destination != \"\" {\n\t\t\tpanic(\"sub-DAO dissolution sweeps to the parent DAO; destination must be empty\")\n\t\t}\n\t} else {\n\t\tif !destination.IsValid() {\n\t\t\tpanic(\"root DAO dissolution requires a valid sweep destination\")\n\t\t}\n\t}\n\n\treturn dissolvePropDefinition{dao, destination}\n}\n\n// dissolvePropDefinition defines a proposal type for dissolving a SubDAO.\ntype dissolvePropDefinition struct {\n\tdao         *commondao.CommonDAO\n\tdestination address // sweep destination, root DAOs only\n}\n\nfunc (p dissolvePropDefinition) Title() string             { return \"Dissolve DAO: \" + p.dao.Name() }\nfunc (dissolvePropDefinition) VotingPeriod() time.Duration { return time.Hour * 24 * 7 }\n\n// isTrustedMarkdownBody marks Body as self-assembled markdown (a DAO link).\nfunc (dissolvePropDefinition) isTrustedMarkdownBody() {}\n\n// Threshold returns the tally threshold: the constitution is silent on\n// dissolution, so the supermajority default applies.\nfunc (dissolvePropDefinition) Threshold() commondao.Threshold {\n\treturn commondao.ThresholdSupermajority\n}\n\nfunc (p dissolvePropDefinition) Body() string {\n\tvar b strings.Builder\n\n\tb.WriteString(md.Paragraph(md.Bold(\"DAO:\") + \"\\n\" + daoMDLink(p.dao)))\n\n\t// A root dissolution sweeps the entire treasury to an address named at\n\t// proposal creation. Show it — it is the one dissolution shape with a\n\t// free-form destination, and voters cannot judge the proposal without\n\t// it (a spend renders its recipient for the same reason). Sub-DAO\n\t// dissolution has no destination: it always sweeps up the tree.\n\tif p.destination != \"\" {\n\t\tb.WriteString(md.Paragraph(md.Bold(\"Sweep destination:\") + \"\\n\" + userLink(p.destination)))\n\t}\n\n\treturn b.String()\n}\n\nfunc (p dissolvePropDefinition) Validate() (err error) {\n\tif p.dao.IsDeleted() {\n\t\treturn errors.New(\"DAO has already been dissolved\")\n\t}\n\treturn nil\n}\n\nfunc (p dissolvePropDefinition) Executor() commondao.ExecFunc {\n\treturn p.execute\n}\n\n// FundingDAOID returns the ID of the DAO whose treasury the dissolution\n// sweeps: the DAO being dissolved. For a sub-DAO the proposal is hosted in\n// an ancestor, so the operative DAO differs from the host.\nfunc (p dissolvePropDefinition) FundingDAOID() uint64 {\n\treturn p.dao.ID()\n}\n\nfunc (p dissolvePropDefinition) execute(_ int, sub realm) error {\n\t// Sweep any remaining treasury before soft deleting: no council\n\t// remains afterwards to pass a spend. Sub-DAO funds go to the parent;\n\t// root funds go to the destination named at proposal creation. A\n\t// frozen treasury does not block dissolution — the sweep sends the\n\t// funds where a clawback would. sub is the dissolved DAO's sub-identity,\n\t// minted by the host. Full-balance send: cost is O(denoms); a\n\t// denom-flooded DAO can push this past block gas, blocking dissolution\n\t// (known limitation — see the treasury ADR §Sweep gas-bomb).\n\tif balance := treasuryBalance(p.dao); !balance.IsZero() {\n\t\tdest := p.destination\n\t\tif parent := p.dao.Parent(); parent != nil {\n\t\t\t// Sweep to the nearest LIVE ancestor, mirroring the walk\n\t\t\t// CreateDissolutionProposal uses to pick the host. Sending to a\n\t\t\t// dissolved intermediate would deposit the balance on a dead\n\t\t\t// DAO: only a further clawback could rescue it, and once the\n\t\t\t// whole chain is dissolved it is lost outright. A dissolution\n\t\t\t// proposal can only be hosted by a live ancestor (a deleted DAO\n\t\t\t// rejects Propose), so one always exists.\n\t\t\tfor parent.IsDeleted() \u0026\u0026 parent.Parent() != nil {\n\t\t\t\tparent = parent.Parent()\n\t\t\t}\n\t\t\tdest = parent.Address()\n\t\t}\n\n\t\tb := banker.NewBanker(banker.BankerTypeRealmSend, sub)\n\t\tb.SendCoins(sub.Address(), dest, balance)\n\t}\n\n\t// Drop the DAO from the home index: a dissolved DAO can no longer be\n\t// unlisted through SetListed (it rejects deleted DAOs), so unlist it\n\t// here rather than leave it stuck in the listing.\n\tsetListed(p.dao.ID(), false)\n\n\t// Dissolution voids the DAO's in-flight proposals before soft deleting\n\t// it, so nothing remains pending on a DAO that rejects execution.\n\tp.dao.Dissolve(\"DAO dissolved\")\n\treturn nil\n}\n"},{"name":"proposal_subdao_test.gno","body":"package commondao\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nvar (\n\t_ commondao.ProposalDefinition = (*subDAOPropDefinition)(nil)\n\t_ commondao.ProposalDefinition = (*dissolvePropDefinition)(nil)\n)\n\n// newTestDAO registers a DAO in the realm state so definitions that read\n// realm options can be tested.\nfunc newTestDAO(t *testing.T, name string, members ...address) *commondao.CommonDAO {\n\tt.Helper()\n\n\tid := daoID.Next()\n\topts := []commondao.Option{\n\t\tcommondao.WithID(uint64(id)),\n\t\tcommondao.WithName(name),\n\t}\n\tfor _, m := range members {\n\t\topts = append(opts, commondao.WithCouncilMember(m))\n\t}\n\tfor _, k := range defaultProposalKinds {\n\t\topts = append(opts, commondao.WithProposalKind(k))\n\t}\n\n\tdao := commondao.New(opts...)\n\tdaos.Set(id.String(), dao)\n\treturn dao\n}\n\nfunc TestSubDAOPropDefinitionNew(cur realm, t *testing.T) {\n\tvar member address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\n\tdao := newTestDAO(t, \"Root\", member)\n\n\turequire.PanicsWithMessage(t, cur, \"parent DAO is required\", func() {\n\t\tnewSubDAOPropDefinition(nil, \"Sub\", \"Purpose\", \"\", []address{member})\n\t})\n\n\turequire.PanicsWithMessage(t, cur, \"DAO name is empty\", func() {\n\t\tnewSubDAOPropDefinition(dao, \"  \", \"Purpose\", \"\", []address{member})\n\t})\n\n\turequire.PanicsWithMessage(t, cur, \"a SubDAO requires at least one initial council member\", func() {\n\t\tnewSubDAOPropDefinition(dao, \"Sub\", \"Purpose\", \"\", nil)\n\t})\n\n\tdef := newSubDAOPropDefinition(dao, \"Sub\", \"Purpose\", \"\", []address{member})\n\tuassert.Equal(t, \"New SubDAO: Sub\", def.Title())\n\tuassert.Equal(t, int(commondao.ThresholdSimpleMajority), int(def.Threshold()))\n\tuassert.NotEmpty(t, def.Body())\n\tuassert.NoError(t, def.Validate())\n}\n\nfunc TestSubDAOPropDefinitionExecute(cur realm, t *testing.T) {\n\tvar member address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\n\tdao := newTestDAO(t, \"Root2\", member)\n\n\tdef := newSubDAOPropDefinition(dao, \"Sub\", \"Purpose\", \"\", []address{member})\n\n\turequire.NoError(t, def.Executor()(0, cur))\n\n\turequire.Equal(t, 1, dao.ChildrenCount(), \"expect one SubDAO\")\n\tvar sub *commondao.CommonDAO\n\tdao.IterateChildren(func(child *commondao.CommonDAO) bool {\n\t\tsub = child\n\t\treturn true\n\t})\n\tuassert.Equal(t, \"Sub\", sub.Name())\n\tuassert.True(t, sub.Council().Has(member))\n\tuassert.Equal(t, 1, sub.Council().Size())\n\n\t// A second SubDAO with the same name is rejected at validation\n\tuassert.ErrorContains(t, def.Validate(), \"a SubDAO with the same name already exists\")\n}\n\nfunc TestDissolvePropDefinitionExecute(cur realm, t *testing.T) {\n\tvar member address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\n\tdao := newTestDAO(t, \"Root3\", member)\n\n\turequire.PanicsWithMessage(t, cur, \"SubDAO is required\", func() {\n\t\tnewDissolvePropDefinition(nil, \"\")\n\t})\n\n\tdef := newDissolvePropDefinition(dao, member)\n\tuassert.Equal(t, \"Dissolve DAO: Root3\", def.Title())\n\tuassert.Equal(t, int(commondao.ThresholdSupermajority), int(def.Threshold()))\n\tuassert.NoError(t, def.Validate())\n\n\t// Dissolution voids in-flight proposals before soft deleting the DAO\n\tp, err := dao.Propose(member, kindText, textArgs{\n\t\ttitle:        \"Pending\",\n\t\tbody:         \"pending\",\n\t\tvotingPeriod: time.Hour * 24,\n\t})\n\turequire.NoError(t, err)\n\n\turequire.NoError(t, def.Executor()(0, cur))\n\n\tuassert.True(t, dao.IsDeleted())\n\tuassert.Equal(t, 0, dao.ActiveProposalsSize())\n\tuassert.Equal(t, string(commondao.StatusDismissed), string(p.Status()))\n\tuassert.Equal(t, \"DAO dissolved\", p.StatusReason())\n\n\t// Dissolved DAOs cannot be dissolved again\n\tuassert.ErrorContains(t, def.Validate(), \"has already been dissolved\")\n}\n"},{"name":"proposal_text.gno","body":"package commondao\n\nimport (\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/nt/commondao/v0\"\n)\n\nconst (\n\tmaxTextBody         = 15_000\n\tmaxTextTitle        = 255\n\tminTextVotingPeriod = time.Hour * 24\n)\n\n// newTextPropDefinition creates a new general text proposal definition.\nfunc newTextPropDefinition(title, body string, votingPeriod time.Duration) textPropDefinition {\n\ttitle = strings.TrimSpace(title)\n\tif title == \"\" {\n\t\tpanic(\"proposal title is empty\")\n\t}\n\n\tif len(title) \u003e maxTextTitle {\n\t\tpanic(\"proposal title is too long, max length is 255 chars\")\n\t}\n\n\tbody = strings.TrimSpace(body)\n\tif body == \"\" {\n\t\tpanic(\"proposal body is empty\")\n\t}\n\n\tif len(body) \u003e maxTextBody {\n\t\tpanic(\"proposal body is too long, max length is 15000 chars\")\n\t}\n\n\tif votingPeriod \u003c minTextVotingPeriod {\n\t\tpanic(\"minimum proposal voting period is one day\")\n\t}\n\n\treturn textPropDefinition{\n\t\ttitle:        title,\n\t\tbody:         body,\n\t\tvotingPeriod: votingPeriod,\n\t}\n}\n\n// textPropDefinition defines a proposal type for general text proposals.\n// These type of proposals are not executable so nothing happens when they pass.\ntype textPropDefinition struct {\n\ttitle, body  string\n\tvotingPeriod time.Duration\n}\n\nfunc (p textPropDefinition) Title() string               { return p.title }\nfunc (p textPropDefinition) Body() string                { return p.body }\nfunc (p textPropDefinition) VotingPeriod() time.Duration { return p.votingPeriod }\n\n// Threshold returns the tally threshold: text proposals are decided by the\n// constitution's default supermajority rule.\nfunc (textPropDefinition) Threshold() commondao.Threshold {\n\treturn commondao.ThresholdSupermajority\n}\n"},{"name":"proposal_text_test.gno","body":"package commondao\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nvar (\n\t_ commondao.ProposalDefinition = (*textPropDefinition)(nil)\n)\n\nfunc TestTextPropDefinitionNew(cur realm, t *testing.T) {\n\turequire.PanicsWithMessage(t, cur, \"proposal title is empty\", func() {\n\t\tnewTextPropDefinition(\"  \", \"body\", time.Hour*24)\n\t})\n\n\turequire.PanicsWithMessage(t, cur, \"proposal title is too long, max length is 255 chars\", func() {\n\t\tnewTextPropDefinition(strings.Repeat(\"a\", maxTextTitle+1), \"body\", time.Hour*24)\n\t})\n\n\turequire.PanicsWithMessage(t, cur, \"proposal body is empty\", func() {\n\t\tnewTextPropDefinition(\"title\", \"  \", time.Hour*24)\n\t})\n\n\turequire.PanicsWithMessage(t, cur, \"proposal body is too long, max length is 15000 chars\", func() {\n\t\tnewTextPropDefinition(\"title\", strings.Repeat(\"a\", maxTextBody+1), time.Hour*24)\n\t})\n\n\turequire.PanicsWithMessage(t, cur, \"minimum proposal voting period is one day\", func() {\n\t\tnewTextPropDefinition(\"title\", \"body\", time.Hour)\n\t})\n\n\tdef := newTextPropDefinition(\" title \", \" body \", time.Hour*48)\n\tuassert.Equal(t, \"title\", def.Title())\n\tuassert.Equal(t, \"body\", def.Body())\n\tuassert.Equal(t, int64(time.Hour*48), int64(def.VotingPeriod()))\n\tuassert.Equal(t, int(commondao.ThresholdSupermajority), int(def.Threshold()))\n}\n"},{"name":"proposal_treasury.gno","body":"package commondao\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"errors\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/nt/commondao/v0\"\n)\n\n// hasLiveProperAncestor checks if any proper ancestor of dao is not\n// dissolved.\nfunc hasLiveProperAncestor(dao *commondao.CommonDAO) bool {\n\tfor p := dao.Parent(); p != nil; p = p.Parent() {\n\t\tif !p.IsDeleted() {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// isProperAncestor checks if dao is a proper ancestor of target.\n// Parent pointers are set only at construction and no re-parenting path\n// exists, so ancestry is stable for the lifetime of a proposal.\nfunc isProperAncestor(dao, target *commondao.CommonDAO) bool {\n\tfor p := target.Parent(); p != nil; p = p.Parent() {\n\t\tif p.ID() == dao.ID() {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// assertIsProperAncestor validates the ancestor relation that authorizes\n// treasury controls over a descendant (docs/CONSTITUTION.md :1507). The\n// relation is strictly proper: a DAO can never claw back or unfreeze\n// itself.\nfunc assertIsProperAncestor(dao, target *commondao.CommonDAO) error {\n\tif target.ID() == dao.ID() {\n\t\treturn errors.New(\"a DAO cannot target itself\")\n\t}\n\tif !isProperAncestor(dao, target) {\n\t\treturn errors.New(\"DAO is not an ancestor of the target DAO\")\n\t}\n\treturn nil\n}\n\n// treasuryBalance returns the current balance of a DAO's treasury address.\nfunc treasuryBalance(dao *commondao.CommonDAO) chain.Coins {\n\treturn banker.NewReadonlyBanker().GetCoins(dao.Address())\n}\n\n// newTreasurySpendPropDefinition creates a proposal definition that sends\n// coins from the DAO's own treasury.\nfunc newTreasurySpendPropDefinition(dao *commondao.CommonDAO, to address, coin chain.Coin) treasurySpendPropDefinition {\n\tif dao == nil {\n\t\tpanic(\"DAO is required\")\n\t}\n\tif !to.IsValid() {\n\t\tpanic(\"invalid recipient address\")\n\t}\n\tif coin.Denom == \"\" {\n\t\tpanic(\"coin denomination is empty\")\n\t}\n\tif !coin.IsPositive() {\n\t\tpanic(\"spend amount must be positive\")\n\t}\n\n\treturn treasurySpendPropDefinition{dao, to, coin}\n}\n\n// treasurySpendPropDefinition defines a proposal type for spending funds\n// from the DAO's own treasury (docs/CONSTITUTION.md :1542-1543).\ntype treasurySpendPropDefinition struct {\n\tdao  *commondao.CommonDAO\n\tto   address\n\tcoin chain.Coin\n}\n\nfunc (treasurySpendPropDefinition) Title() string               { return \"Treasury Spend\" }\nfunc (treasurySpendPropDefinition) VotingPeriod() time.Duration { return time.Hour * 24 * 7 }\n\n// isTrustedMarkdownBody marks Body as self-assembled markdown; the embedded\n// recipient and amount are formatted by md helpers / EscapeText in Body.\nfunc (treasurySpendPropDefinition) isTrustedMarkdownBody() {}\n\n// Threshold returns the tally threshold: the constitution attaches no\n// spend-specific rule, so the supermajority default applies.\nfunc (treasurySpendPropDefinition) Threshold() commondao.Threshold {\n\treturn commondao.ThresholdSupermajority\n}\n\nfunc (p treasurySpendPropDefinition) Body() string {\n\tvar b strings.Builder\n\n\tb.WriteString(md.Paragraph(md.Bold(\"Recipient:\") + \"\\n\" + userLink(p.to)))\n\tb.WriteString(md.Paragraph(md.Bold(\"Amount:\") + \"\\n\" + md.EscapeText(p.coin.String())))\n\n\treturn b.String()\n}\n\n// Validate runs at proposal creation and again at execution, so a\n// treasury frozen or drained after the proposal passed still fails it\n// cleanly (StatusFailed, coins untouched) instead of panicking the tx.\nfunc (p treasurySpendPropDefinition) Validate() error {\n\tif p.dao.IsDeleted() {\n\t\treturn errors.New(\"DAO has already been dissolved\")\n\t}\n\tif p.dao.IsTreasuryFrozen() {\n\t\treturn errors.New(\"DAO treasury is frozen\")\n\t}\n\tif treasuryBalance(p.dao).AmountOf(p.coin.Denom) \u003c p.coin.Amount {\n\t\treturn errors.New(\"insufficient treasury balance\")\n\t}\n\treturn nil\n}\n\nfunc (p treasurySpendPropDefinition) Executor() commondao.ExecFunc {\n\treturn p.execute\n}\n\n// FundingDAOID returns the ID of the DAO whose treasury funds the spend:\n// its own (the host).\nfunc (p treasurySpendPropDefinition) FundingDAOID() uint64 {\n\treturn p.dao.ID()\n}\n\nfunc (p treasurySpendPropDefinition) execute(_ int, sub realm) error {\n\t// sub is this DAO's sub-identity, minted by the host: send from it.\n\t// Banker sends move bank balances without invoking recipient code, so\n\t// there is no reentrancy vector. Validate ran in this same Execute\n\t// call, so the balance check is current.\n\tb := banker.NewBanker(banker.BankerTypeRealmSend, sub)\n\tb.SendCoins(sub.Address(), p.to, chain.NewCoins(p.coin))\n\treturn nil\n}\n\n// newTreasuryClawbackPropDefinition creates a proposal definition that\n// sweeps a descendant DAO's treasury one step up the tree.\nfunc newTreasuryClawbackPropDefinition(dao, target *commondao.CommonDAO) treasuryClawbackPropDefinition {\n\tif dao == nil {\n\t\tpanic(\"DAO is required\")\n\t}\n\tif target == nil {\n\t\tpanic(\"target DAO is required\")\n\t}\n\n\treturn treasuryClawbackPropDefinition{dao, target}\n}\n\n// treasuryClawbackPropDefinition defines a proposal type for an ancestor\n// DAO to reclaim a descendant's treasury. The destination is fixed — the\n// target's parent — so funds move one step up the tree toward their\n// origin and can never be drained out of the tree entirely. Clawback\n// remains valid against soft-deleted and frozen descendants.\ntype treasuryClawbackPropDefinition struct {\n\tdao    *commondao.CommonDAO // proposing DAO, must be a proper ancestor\n\ttarget *commondao.CommonDAO\n}\n\nfunc (treasuryClawbackPropDefinition) Title() string               { return \"Treasury Clawback\" }\nfunc (treasuryClawbackPropDefinition) VotingPeriod() time.Duration { return time.Hour * 24 * 7 }\n\n// isTrustedMarkdownBody marks Body as self-assembled markdown (a DAO link).\nfunc (treasuryClawbackPropDefinition) isTrustedMarkdownBody() {}\n\n// Threshold returns the tally threshold: simple majority, the\n// constitutional wording for this ancestor power.\nfunc (treasuryClawbackPropDefinition) Threshold() commondao.Threshold {\n\treturn commondao.ThresholdSimpleMajority\n}\n\nfunc (p treasuryClawbackPropDefinition) Body() string {\n\tvar b strings.Builder\n\n\tb.WriteString(md.Paragraph(md.Bold(\"Target DAO:\") + \"\\n\" + daoMDLink(p.target)))\n\tb.WriteString(md.Paragraph(\n\t\tmd.Bold(\"Destination:\") + \"\\n\" +\n\t\t\t\"The target's parent DAO receives the target's full balance at execution time.\",\n\t))\n\n\treturn b.String()\n}\n\nfunc (p treasuryClawbackPropDefinition) Validate() error {\n\treturn assertIsProperAncestor(p.dao, p.target)\n}\n\nfunc (p treasuryClawbackPropDefinition) Executor() commondao.ExecFunc {\n\treturn p.execute\n}\n\n// FundingDAOID returns the ID of the DAO whose treasury the clawback\n// sweeps: the target, not the proposing ancestor that hosts the proposal.\nfunc (p treasuryClawbackPropDefinition) FundingDAOID() uint64 {\n\treturn p.target.ID()\n}\n\nfunc (p treasuryClawbackPropDefinition) execute(_ int, sub realm) error {\n\tbalance := treasuryBalance(p.target)\n\tif balance.IsZero() {\n\t\treturn nil\n\t}\n\n\t// sub is the target's sub-identity, minted by the host. A proper\n\t// ancestor exists, so the target always has a parent. Full-balance\n\t// send: cost is O(number of denoms held). A target dusted with many\n\t// realm-minted denoms can push this past block gas (known limitation —\n\t// see the treasury ADR §Sweep gas-bomb).\n\tb := banker.NewBanker(banker.BankerTypeRealmSend, sub)\n\tb.SendCoins(sub.Address(), p.target.Parent().Address(), balance)\n\treturn nil\n}\n\n// newTreasuryFreezePropDefinition creates a proposal definition that\n// freezes or unfreezes a descendant DAO's treasury.\nfunc newTreasuryFreezePropDefinition(dao, target *commondao.CommonDAO, frozen bool) treasuryFreezePropDefinition {\n\tif dao == nil {\n\t\tpanic(\"DAO is required\")\n\t}\n\tif target == nil {\n\t\tpanic(\"target DAO is required\")\n\t}\n\n\treturn treasuryFreezePropDefinition{dao, target, frozen}\n}\n\n// treasuryFreezePropDefinition defines a proposal type for an ancestor\n// DAO to freeze or unfreeze a descendant's treasury. Freezing does not\n// cascade: ancestors freeze each descendant explicitly. Only a proper\n// ancestor can unfreeze — the frozen DAO's own council cannot.\ntype treasuryFreezePropDefinition struct {\n\tdao    *commondao.CommonDAO // proposing DAO, must be a proper ancestor\n\ttarget *commondao.CommonDAO\n\tfrozen bool\n}\n\nfunc (p treasuryFreezePropDefinition) Title() string {\n\tif p.frozen {\n\t\treturn \"Treasury Freeze\"\n\t}\n\treturn \"Treasury Unfreeze\"\n}\n\nfunc (treasuryFreezePropDefinition) VotingPeriod() time.Duration { return time.Hour * 24 * 7 }\n\n// isTrustedMarkdownBody marks Body as self-assembled markdown (a DAO link).\nfunc (treasuryFreezePropDefinition) isTrustedMarkdownBody() {}\n\n// Threshold returns the tally threshold: simple majority, matching the\n// clawback power it safeguards.\nfunc (treasuryFreezePropDefinition) Threshold() commondao.Threshold {\n\treturn commondao.ThresholdSimpleMajority\n}\n\nfunc (p treasuryFreezePropDefinition) Body() string {\n\tvar b strings.Builder\n\n\tb.WriteString(md.Paragraph(md.Bold(\"Target DAO:\") + \"\\n\" + daoMDLink(p.target)))\n\n\taction := \"frozen: no funds can leave the treasury until a proper ancestor unfreezes it.\"\n\tif !p.frozen {\n\t\taction = \"unfrozen: funds can leave the treasury again.\"\n\t}\n\tb.WriteString(md.Paragraph(md.Bold(\"Effect:\") + \"\\nThe target's treasury is \" + action))\n\n\treturn b.String()\n}\n\nfunc (p treasuryFreezePropDefinition) Validate() error {\n\t// Orphan rescue: when every proper ancestor is dissolved, the freezing\n\t// authority class is extinct, so the target's own council may restore\n\t// the constitutional default by unfreezing itself. Without this, a\n\t// frozen DAO orphaned by its ancestors' dissolution would hold its\n\t// funds locked forever. Freezing - and any self-targeting while a\n\t// live ancestor exists - stays ancestor-only.\n\tif !p.frozen \u0026\u0026 p.dao.ID() == p.target.ID() \u0026\u0026 !hasLiveProperAncestor(p.target) {\n\t\treturn nil\n\t}\n\treturn assertIsProperAncestor(p.dao, p.target)\n}\n\nfunc (p treasuryFreezePropDefinition) Executor() commondao.ExecFunc {\n\treturn p.execute\n}\n\nfunc (p treasuryFreezePropDefinition) execute(_ int, sub realm) error {\n\tp.target.SetTreasuryFrozen(p.frozen)\n\treturn nil\n}\n\n// daoMDLink returns a markdown link to a DAO's page.\nfunc daoMDLink(dao *commondao.CommonDAO) string {\n\treturn md.Link(dao.Name(), daoURL(dao.ID()))\n}\n"},{"name":"proposal_treasury_test.gno","body":"package commondao\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nvar (\n\t_ commondao.ProposalDefinition = (*treasurySpendPropDefinition)(nil)\n\t_ commondao.ProposalDefinition = (*treasuryClawbackPropDefinition)(nil)\n\t_ commondao.ProposalDefinition = (*treasuryFreezePropDefinition)(nil)\n)\n\n// newTestTree builds an in-memory root -\u003e mid -\u003e leaf DAO chain.\nfunc newTestTree() (root, mid, leaf *commondao.CommonDAO) {\n\troot = commondao.New(commondao.WithID(101), commondao.WithName(\"Root\"))\n\tmid = commondao.New(commondao.WithID(102), commondao.WithName(\"Mid\"), commondao.WithParent(root))\n\tleaf = commondao.New(commondao.WithID(103), commondao.WithName(\"Leaf\"), commondao.WithParent(mid))\n\treturn\n}\n\nfunc TestTreasurySpendPropDefinition(cur realm, t *testing.T) {\n\tvar member address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\"\n\n\tdao := commondao.New(\n\t\tcommondao.WithID(100),\n\t\tcommondao.WithName(\"Foo\"),\n\t\tcommondao.WithAddress(member), // any valid, unfunded address\n\t)\n\tcoin := chain.NewCoin(\"ugnot\", 100)\n\n\turequire.PanicsWithMessage(t, cur, \"DAO is required\", func() {\n\t\tnewTreasurySpendPropDefinition(nil, member, coin)\n\t})\n\turequire.PanicsWithMessage(t, cur, \"invalid recipient address\", func() {\n\t\tnewTreasurySpendPropDefinition(dao, \"invalid\", coin)\n\t})\n\turequire.PanicsWithMessage(t, cur, \"coin denomination is empty\", func() {\n\t\tnewTreasurySpendPropDefinition(dao, member, chain.Coin{Amount: 1})\n\t})\n\turequire.PanicsWithMessage(t, cur, \"spend amount must be positive\", func() {\n\t\tnewTreasurySpendPropDefinition(dao, member, chain.NewCoin(\"ugnot\", 0))\n\t})\n\n\tdef := newTreasurySpendPropDefinition(dao, member, coin)\n\tuassert.Equal(t, \"Treasury Spend\", def.Title())\n\tuassert.Equal(t, int(commondao.ThresholdSupermajority), int(def.Threshold()))\n\tuassert.NotEmpty(t, def.Body())\n\n\t// The treasury is unfunded\n\tuassert.ErrorContains(t, def.Validate(), \"insufficient treasury balance\")\n\n\t// Frozen treasuries reject spends before the balance check\n\tdao.SetTreasuryFrozen(true)\n\tuassert.ErrorContains(t, def.Validate(), \"DAO treasury is frozen\")\n\tdao.SetTreasuryFrozen(false)\n\n\t// Dissolved DAOs reject spends\n\tdao.Dissolve(\"test\")\n\tuassert.ErrorContains(t, def.Validate(), \"DAO has already been dissolved\")\n}\n\nfunc TestTreasuryClawbackPropDefinition(cur realm, t *testing.T) {\n\troot, mid, leaf := newTestTree()\n\n\turequire.PanicsWithMessage(t, cur, \"DAO is required\", func() {\n\t\tnewTreasuryClawbackPropDefinition(nil, leaf)\n\t})\n\turequire.PanicsWithMessage(t, cur, \"target DAO is required\", func() {\n\t\tnewTreasuryClawbackPropDefinition(root, nil)\n\t})\n\n\tdef := newTreasuryClawbackPropDefinition(root, leaf)\n\tuassert.Equal(t, \"Treasury Clawback\", def.Title())\n\tuassert.Equal(t, int(commondao.ThresholdSimpleMajority), int(def.Threshold()))\n\tuassert.NotEmpty(t, def.Body())\n\n\t// Any proper ancestor qualifies, not just the direct parent\n\tuassert.NoError(t, newTreasuryClawbackPropDefinition(root, mid).Validate())\n\tuassert.NoError(t, newTreasuryClawbackPropDefinition(root, leaf).Validate())\n\tuassert.NoError(t, newTreasuryClawbackPropDefinition(mid, leaf).Validate())\n\n\t// A DAO can never target itself, and descendants and strangers have\n\t// no clawback authority\n\tuassert.ErrorContains(t,\n\t\tnewTreasuryClawbackPropDefinition(root, root).Validate(),\n\t\t\"a DAO cannot target itself\")\n\tuassert.ErrorContains(t,\n\t\tnewTreasuryClawbackPropDefinition(leaf, root).Validate(),\n\t\t\"DAO is not an ancestor of the target DAO\")\n\tuassert.ErrorContains(t,\n\t\tnewTreasuryClawbackPropDefinition(commondao.New(commondao.WithID(999)), leaf).Validate(),\n\t\t\"DAO is not an ancestor of the target DAO\")\n}\n\nfunc TestTreasuryFreezePropDefinition(cur realm, t *testing.T) {\n\troot, mid, leaf := newTestTree()\n\n\turequire.PanicsWithMessage(t, cur, \"DAO is required\", func() {\n\t\tnewTreasuryFreezePropDefinition(nil, leaf, true)\n\t})\n\turequire.PanicsWithMessage(t, cur, \"target DAO is required\", func() {\n\t\tnewTreasuryFreezePropDefinition(root, nil, true)\n\t})\n\n\tfreeze := newTreasuryFreezePropDefinition(root, leaf, true)\n\tuassert.Equal(t, \"Treasury Freeze\", freeze.Title())\n\tuassert.Equal(t, int(commondao.ThresholdSimpleMajority), int(freeze.Threshold()))\n\tuassert.NotEmpty(t, freeze.Body())\n\n\tunfreeze := newTreasuryFreezePropDefinition(root, leaf, false)\n\tuassert.Equal(t, \"Treasury Unfreeze\", unfreeze.Title())\n\tuassert.NotEmpty(t, unfreeze.Body())\n\n\tuassert.NoError(t, freeze.Validate())\n\tuassert.NoError(t, newTreasuryFreezePropDefinition(mid, leaf, true).Validate())\n\tuassert.ErrorContains(t,\n\t\tnewTreasuryFreezePropDefinition(leaf, leaf, false).Validate(),\n\t\t\"a DAO cannot target itself\")\n\tuassert.ErrorContains(t,\n\t\tnewTreasuryFreezePropDefinition(leaf, mid, true).Validate(),\n\t\t\"DAO is not an ancestor of the target DAO\")\n}\n\n// TestTreasuryFreezeBlastRadius pins that a freeze touches exactly its\n// target. Freezing is deliberately not transitive — an ancestor freezes\n// each descendant explicitly — so a change that made it cascade would\n// let one vote lock a whole subtree's treasuries, and one that let it\n// reach upward would let a descendant's freeze bind its own parent.\n// Neither direction was covered.\nfunc TestTreasuryFreezeBlastRadius(cur realm, t *testing.T) {\n\troot, mid, leaf := newTestTree()\n\n\t// Downward: freezing the middle DAO leaves its descendant alone.\n\turequire.NoError(t, newTreasuryFreezePropDefinition(root, mid, true).Executor()(0, cur))\n\tuassert.True(t, mid.IsTreasuryFrozen(), \"target must be frozen\")\n\tuassert.False(t, leaf.IsTreasuryFrozen(), \"freeze must not cascade to descendants\")\n\tuassert.False(t, root.IsTreasuryFrozen(), \"freeze must not touch the proposing ancestor\")\n\n\turequire.NoError(t, newTreasuryFreezePropDefinition(root, mid, false).Executor()(0, cur))\n\tuassert.False(t, mid.IsTreasuryFrozen(), \"unfreeze must clear the target\")\n\n\t// Upward: freezing a grandchild leaves the DAO in between alone. The\n\t// grandchild leg is what catches an upward reach — with root-\u003emid the\n\t// target's parent IS the proposing ancestor, so the two are\n\t// indistinguishable.\n\turequire.NoError(t, newTreasuryFreezePropDefinition(root, leaf, true).Executor()(0, cur))\n\tuassert.True(t, leaf.IsTreasuryFrozen(), \"target must be frozen\")\n\tuassert.False(t, mid.IsTreasuryFrozen(), \"freeze must not touch the target's parent\")\n\tuassert.False(t, root.IsTreasuryFrozen(), \"freeze must not touch the proposing ancestor\")\n}\n"},{"name":"public.gno","body":"package commondao\n\nimport (\n\t// unsafe is used only for the CurrentRealm call in assertRunningPath when\n\t// checking the deployment path. Caller authentication uses cur.Previous().\n\t\"chain/runtime/unsafe\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/commondao/v0\"\n)\n\n// assertCurrent guards every public crossing entry. Authentication reads\n// cur.Previous() and Execute mints cur.Sub(...) for the executor; both\n// require cur to be the live top-of-frame realm (AGENTS.md / interrealm-v2).\n// Every entry is only reachable via a cross today, so this is\n// defense-in-depth against a future non-crossing caller.\nfunc assertCurrent(cur realm) {\n\tif !cur.IsCurrent() {\n\t\tpanic(\"commondao: cur realm is not current\")\n\t}\n}\n\n// assertRunningPath fails closed if this realm is deployed at a package\n// path other than pkgPath. Treasury addresses derive from the pkgPath\n// const (daoAddress), while Execute mints the DAO sub under the running\n// realm's own path (cur.Sub). Those agree only when the running path is\n// pkgPath; a copy deployed elsewhere would read balances at one address\n// and send from another, turning a clean StatusFailed into a mismatch. We\n// reject that at genesis rather than silently diverge.\nfunc assertRunningPath() {\n\tif got := unsafe.CurrentRealm().PkgPath(); got != pkgPath {\n\t\tpanic(\"commondao: realm deployed at \" + got + \", expected \" + pkgPath)\n\t}\n}\n\n// assertCallerIsUser rejects a code realm creating a DAO on someone's\n// behalf. New keys the invite and the creators set on the caller, and\n// invites are granted to people, so the caller must be the user itself:\n// either a direct MsgCall (IsUserCall) or that user's own MsgRun frame\n// (IsUserRun, whose address is the signer's own). Both are IsUser; a\n// published realm is neither.\n//\n// Relaying is what this excludes. If a realm could redeem an invite, one\n// invited realm would become a DAO factory for un-invited callers — the\n// vector pr6012_commondao_ownership_rescope.md closed by keying on the\n// transaction origin. Keying on the caller closes it too, but only while\n// the caller cannot be a realm.\n//\n// It bounds who may *create* a DAO, not who may sit on one: the members\n// argument may still name realm addresses, and sub-DAO councils are set\n// by proposal (see CreateSubDAOProposal). Handing a DAO to a realm is a\n// council decision, not a creation-time one.\nfunc assertCallerIsUser(cur realm) {\n\tif !cur.Previous().IsUser() {\n\t\tpanic(\"only a user can create a DAO\")\n\t}\n}\n\n// Invite invites a user to the realm.\n// A user invitation is required to start creating new DAOs.\nfunc Invite(cur realm, invitee address) {\n\tassertCurrent(cur)\n\n\tif !invitee.IsValid() {\n\t\tpanic(\"invalid address\")\n\t}\n\n\tdao := mustGetDAO(CommonDAOID)\n\tcaller := cur.Previous().Address()\n\tif !dao.Council().Has(caller) {\n\t\tpanic(\"unauthorized\")\n\t}\n\n\tinvites.Set(invitee.String(), caller.String())\n}\n\n// IsInvited checks if an address has an invitation to the realm.\nfunc IsInvited(addr address) bool {\n\treturn isInvited(addr)\n}\n\n// New creates a new CommonDAO and returns its ID.\n// The caller must be a user, not a realm (see assertCallerIsUser), and must\n// hold an invite. The invite is consumed when that caller creates a DAO for\n// the first time, after which the caller may create further DAOs freely.\n// The caller becomes a council member along with any additional member\n// addresses listed on separate lines; those may be realm addresses, so a\n// DAO can be handed to a realm by seating it here and resigning. DAOs with\n// a parent are created through proposals (see CreateSubDAOProposal).\nfunc New(cur realm, name, purpose, description, members string) uint64 {\n\tassertCurrent(cur)\n\tassertCallerIsUser(cur)\n\tcaller := cur.Previous().Address()\n\n\tname = strings.TrimSpace(name)\n\tassertDAONameIsValid(name)\n\n\tpurpose = strings.TrimSpace(purpose)\n\tassertDAOPurposeIsValid(purpose)\n\n\tdescription = strings.TrimSpace(description)\n\tassertDAODescriptionIsValid(description)\n\n\t// The invite is consumed once. The direct user caller is then recorded and\n\t// can create further DAOs without another invite.\n\tif !isCreator(caller) {\n\t\tassertIsInvited(caller)\n\t\tinvites.Remove(caller.String())\n\t\tcreators.Set(caller.String(), struct{}{})\n\t}\n\n\tdao := createDAO(name, purpose, description, parseInitialMembers(caller, members)...)\n\treturn dao.ID()\n}\n\n// parseInitialMembers returns the caller plus the parsed additional\n// members, deduplicated.\nfunc parseInitialMembers(caller address, members string) []address {\n\taddrs := parseAddresses(members)\n\tif containsAddress(addrs, caller) {\n\t\treturn addrs\n\t}\n\treturn append(addrs, caller)\n}\n\n// SetListed adds or removes a DAO from the realm's public home index.\n// Listing is cosmetic — it affects only how this realm presents the DAO\n// in its own UI — so any single council member of the DAO may toggle it,\n// like Resign. It defaults to off.\nfunc SetListed(cur realm, daoID uint64, listed bool) {\n\tassertCurrent(cur)\n\n\tdao := mustGetDAO(daoID)\n\tif dao.IsDeleted() {\n\t\tpanic(commondao.ErrDAOIsDeleted)\n\t}\n\n\tassertCallerIsCouncilMember(cur.Previous().Address(), dao)\n\tsetListed(daoID, listed)\n}\n\n// IsListed reports whether a DAO appears in the realm's public home index.\nfunc IsListed(daoID uint64) bool {\n\treturn isListed(daoID)\n}\n\n// GetView returns a read only view of a common DAO searched by ID.\nfunc GetView(daoID uint64) commondao.ReadonlyCommonDAO {\n\treturn mustGetDAO(daoID).Readonly()\n}\n\n// GetBylawsDoc returns the text of a DAO's bylaws/mandates document, or\n// an empty string when the document does not exist (a stored document is\n// never empty).\nfunc GetBylawsDoc(daoID uint64, path string) string {\n\tmustGetDAO(daoID)\n\n\tif set := bylawsView(daoID); set != nil {\n\t\ttext, _ := set.Get(path)\n\t\treturn text\n\t}\n\treturn \"\"\n}\n\n// ListBylawsDocs returns the sorted paths of a DAO's bylaws/mandates\n// documents under a prefix (empty prefix lists all).\nfunc ListBylawsDocs(daoID uint64, prefix string) []string {\n\tmustGetDAO(daoID)\n\n\tif set := bylawsView(daoID); set != nil {\n\t\treturn set.List(prefix)\n\t}\n\treturn nil\n}\n\n// Vote submits a vote for a DAO proposal.\n// Voting is allowed to the members of the proposal's electorate: the\n// council snapshot taken when the proposal was created.\nfunc Vote(cur realm, daoID, proposalID uint64, vote commondao.VoteChoice, reason string) {\n\tassertCurrent(cur)\n\n\tdao := mustGetDAO(daoID)\n\tcaller := cur.Previous().Address()\n\terr := dao.Vote(caller, proposalID, vote, reason)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Funded is the optional contract a proposal definition implements when its\n// executor moves funds from a DAO other than the proposal's host: it names,\n// by ID, the DAO whose sub-identity address funds the executor. Execute\n// resolves that DAO, mints its terminal RealmSend-only sub and passes it to\n// the ExecFunc; a definition that does not implement Funded receives the\n// host DAO's own sub by default. The returned ID must identify the DAO the\n// definition validates its fund movement against (e.g. the DAO being spent,\n// swept or dissolved).\n//\n// It lives realm-side, not in /p/: minting a DAO sub needs the host realm's\n// cur (cur.Sub), so only the host — never the package — can honor it. /p/\n// dispatches CapExempt/Executable/Validable itself, but Execute (the host)\n// is the sole consumer of Funded, so the package has no reason to know it.\ntype Funded interface {\n\t// FundingDAOID returns the ID of the DAO whose sub-address funds the\n\t// executor.\n\tFundingDAOID() uint64\n}\n\n// Execute executes a DAO proposal.\n//\n// Executing a proposal that passed early (decided by the default Council\n// rules before its voting deadline) requires the caller to be a council\n// member. Once the voting deadline has passed execution is permissionless:\n// the tally is deterministic, so anyone can finalize the proposal.\nfunc Execute(cur realm, daoID, proposalID uint64) {\n\tassertCurrent(cur)\n\n\t// Re-entrancy latch: a proposal executor must not trigger another\n\t// Execute. Without this, an executor could finalize a second proposal\n\t// mid-run — e.g. dissolve its own DAO and leave this frame finalizing on\n\t// a deleted DAO. The latch is global (one executor per tx); it does not\n\t// block Vote/Create*, so an executor may still act as its DAO elsewhere.\n\tenterExecute()\n\tdefer leaveExecute()\n\n\tdao := mustGetDAO(daoID)\n\tp := dao.GetProposal(proposalID)\n\tif p == nil {\n\t\tpanic(commondao.ErrProposalNotFound)\n\t}\n\n\t// Before the deadline, only a council member may execute an\n\t// early-passed proposal. Once the deadline passes the tally is\n\t// deterministic, so finalization is permissionless.\n\tif !p.HasVotingDeadlinePassed() {\n\t\tassertCallerIsCouncilMember(cur.Previous().Address(), dao)\n\t}\n\n\t// Mint the sub-identity that funds the executor and pass it in. The\n\t// operative DAO is the host by default; a fund-moving definition\n\t// (Funded) may name a different DAO by ID (e.g. clawback sweeps the\n\t// target, sub-DAO dissolution sweeps the dissolved descendant). Non-fund\n\t// executors ignore the sub. The sub is terminal and RealmSend-only, so\n\t// the executor can move value only from this one DAO address.\n\top := daoID\n\tif f, ok := p.Definition().(Funded); ok {\n\t\top = f.FundingDAOID()\n\t}\n\tsub := cur.Sub(subpathOf(op))\n\n\terr := dao.Execute(proposalID, sub)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Withdraw withdraws an active DAO proposal that has no votes.\n// Only the proposal creator can withdraw it.\nfunc Withdraw(cur realm, daoID, proposalID uint64) {\n\tassertCurrent(cur)\n\n\tdao := mustGetDAO(daoID)\n\tp := dao.GetProposal(proposalID)\n\tif p == nil {\n\t\tpanic(commondao.ErrProposalNotFound)\n\t}\n\n\tif p.Creator() != cur.Previous().Address() {\n\t\tpanic(\"only the proposal creator can withdraw it\")\n\t}\n\n\tif err := dao.Withdraw(proposalID); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Resign removes the caller from a DAO council.\n// The last remaining council member cannot resign.\nfunc Resign(cur realm, daoID uint64) {\n\tassertCurrent(cur)\n\n\tdao := mustGetDAO(daoID)\n\tif dao.IsDeleted() {\n\t\tpanic(commondao.ErrDAOIsDeleted)\n\t}\n\n\tcaller := cur.Previous().Address()\n\tassertCallerIsCouncilMember(caller, dao)\n\n\tif err := dao.UpdateCouncil(nil, []address{caller}); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc isInvited(addr address) bool {\n\treturn invites.Has(addr.String())\n}\n\nfunc assertIsInvited(addr address) {\n\tif !isInvited(addr) {\n\t\tpanic(\"unauthorized\")\n\t}\n}\n\nfunc assertDAONameIsValid(name string) {\n\tif name == \"\" {\n\t\tpanic(\"DAO name is empty\")\n\t}\n\n\tif len(name) \u003e 60 {\n\t\tpanic(\"DAO name is too long, max length is 60 characters\")\n\t}\n}\n\nfunc assertDAOPurposeIsValid(purpose string) {\n\tif purpose == \"\" {\n\t\tpanic(\"DAO purpose is empty\")\n\t}\n\n\tif len(purpose) \u003e 250 {\n\t\tpanic(\"DAO purpose is too long, max length is 250 characters\")\n\t}\n}\n\nfunc assertDAODescriptionIsValid(description string) {\n\tif len(description) \u003e 250 {\n\t\tpanic(\"DAO description is too long, max length is 250 characters\")\n\t}\n}\n\nfunc assertCallerIsCouncilMember(caller address, dao *commondao.CommonDAO) {\n\tif !dao.Council().Has(caller) {\n\t\tpanic(\"caller is not a council member\")\n\t}\n}\n"},{"name":"public_proposals.gno","body":"package commondao\n\nimport (\n\t\"chain\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/nt/bylaws/v0\"\n\t\"gno.land/p/nt/commondao/v0\"\n)\n\n// CreateTextProposal creates a new general text proposal.\n//\n// Parameters:\n// - daoID: ID of the DAO (required)\n// - title: Title of the proposal (required)\n// - body: Body of the proposal (required)\n// - votingDays: The number of days where proposal accepts votes.\n//\n// The default voting period is 7 days.\nfunc CreateTextProposal(cur realm, daoID uint64, title, body string, votingDays uint8) uint64 {\n\tassertCurrent(cur)\n\n\tdao := mustGetDAO(daoID)\n\tif votingDays \u003e 30 {\n\t\tpanic(\"maximum proposal voting period is 30 days\")\n\t}\n\n\tcaller := cur.Previous().Address()\n\tassertCallerIsCouncilMember(caller, dao)\n\n\tvar votingPeriod time.Duration\n\tif votingDays == 0 {\n\t\tvotingPeriod = time.Hour * 24 * 7\n\t} else {\n\t\tvotingPeriod = time.Hour * 24 * time.Duration(votingDays)\n\t}\n\n\treturn mustPropose(dao, caller, kindText, textArgs{title, body, votingPeriod})\n}\n\n// CreateCouncilUpdateProposal creates a new proposal to add and/or remove\n// council members.\n//\n// Parameters:\n// - daoID: ID of the DAO (required)\n// - newMembers: Newline separated list of addresses to add to the council\n// - removeMembers: Newline separated list of council addresses to remove\nfunc CreateCouncilUpdateProposal(cur realm, daoID uint64, newMembers, removeMembers string) uint64 {\n\tassertCurrent(cur)\n\n\tdao := mustGetDAO(daoID)\n\n\tcaller := cur.Previous().Address()\n\tassertCallerIsCouncilMember(caller, dao)\n\n\targs := councilUpdateArgs{dao, parseAddresses(newMembers), parseAddresses(removeMembers)}\n\treturn mustPropose(dao, caller, kindCouncilUpdate, args)\n}\n\n// CreateAncestorCouncilUpdateProposal creates a proposal for an ancestor\n// DAO to add and/or remove members of a descendant's council\n// (docs/CONSTITUTION.md :1531-1532) — the rescue path for a stuck or\n// empty descendant council. It is hosted and voted in the ancestor\n// (daoID) and decided by supermajority; the proposing DAO must be a\n// proper ancestor of the target, verified at proposal validation.\n//\n// Parameters:\n// - daoID: ID of the proposing ancestor DAO (required)\n// - targetID: ID of the descendant DAO whose council changes (required)\n// - newMembers: Newline separated list of addresses to add to the council\n// - removeMembers: Newline separated list of council addresses to remove\nfunc CreateAncestorCouncilUpdateProposal(cur realm, daoID, targetID uint64, newMembers, removeMembers string) uint64 {\n\tassertCurrent(cur)\n\n\tdao := mustGetDAO(daoID)\n\n\tcaller := cur.Previous().Address()\n\tassertCallerIsCouncilMember(caller, dao)\n\n\ttarget := mustGetDAO(targetID)\n\targs := ancestorCouncilUpdateArgs{dao, target, parseAddresses(newMembers), parseAddresses(removeMembers)}\n\treturn mustPropose(dao, caller, kindAncestorCouncilUpdate, args)\n}\n\n// CreateSubDAOProposal creates a new proposal to create a new SubDAO.\n//\n// Parameters:\n// - daoID: ID of the parent DAO (required)\n// - name: A name for the SubDAO (required)\n// - purpose: A purpose for the SubDAO (required)\n// - description: A description for the SubDAO\n// - members: Newline separated list of initial SubDAO council addresses (required)\nfunc CreateSubDAOProposal(cur realm, daoID uint64, name, purpose, description, members string) uint64 {\n\tassertCurrent(cur)\n\n\tdao := mustGetDAO(daoID)\n\tcaller := cur.Previous().Address()\n\tassertCallerIsCouncilMember(caller, dao)\n\n\targs := subDAOArgs{dao, name, purpose, description, parseAddresses(members)}\n\treturn mustPropose(dao, caller, kindSubDAO, args)\n}\n\n// CreateDissolutionProposal creates a new proposal to dissolve a DAO or SubDAO.\n//\n// SubDAOs can only be dissolved by the parent DAO, which owns and\n// controls its sub-DAOs (docs/CONSTITUTION.md :1507). When the parent is\n// itself already dissolved, the proposal is hosted in the nearest\n// non-dissolved ancestor, so orphans below a dissolved middle DAO remain\n// dissolvable.\n//\n// Dissolution sweeps any remaining treasury balance. A sub-DAO's sweep\n// goes to its parent and destination must be empty; a root DAO has no\n// parent, so a valid destination address is required.\n//\n// Parameters:\n// - daoID: ID of the DAO to dissolve (required)\n// - destination: sweep destination, root DAOs only\nfunc CreateDissolutionProposal(cur realm, daoID uint64, destination address) uint64 {\n\tassertCurrent(cur)\n\n\t// When DAO to dissolve is a SubDAO make sure that proposal is created\n\t// in the parent DAO, or in the nearest non-dissolved ancestor when\n\t// parents were dissolved first.\n\tdao := mustGetDAO(daoID)\n\tdissolveDAO := dao\n\tif parent := dao.Parent(); parent != nil {\n\t\tfor parent.IsDeleted() \u0026\u0026 parent.Parent() != nil {\n\t\t\tparent = parent.Parent()\n\t\t}\n\t\tdao = parent\n\t}\n\n\tcaller := cur.Previous().Address()\n\tassertCallerIsCouncilMember(caller, dao)\n\n\t// The host (nearest live ancestor) gates and votes the proposal, while\n\t// the definition operates on the dissolved descendant carried in args.\n\treturn mustPropose(dao, caller, kindDissolve, dissolveArgs{dissolveDAO, destination})\n}\n\n// CreateTreasurySpendProposal creates a new proposal to send coins from\n// the DAO's own treasury (docs/CONSTITUTION.md :1542-1543).\n//\n// Parameters:\n// - daoID: ID of the DAO whose treasury is spent (required)\n// - to: recipient address (required)\n// - denom: coin denomination, e.g. \"ugnot\" (required)\n// - amount: coin amount, must be positive (required)\nfunc CreateTreasurySpendProposal(cur realm, daoID uint64, to address, denom string, amount int64) uint64 {\n\tassertCurrent(cur)\n\n\tdao := mustGetDAO(daoID)\n\tcaller := cur.Previous().Address()\n\tassertCallerIsCouncilMember(caller, dao)\n\n\targs := treasurySpendArgs{dao, to, chain.NewCoin(denom, amount)}\n\treturn mustPropose(dao, caller, kindTreasurySpend, args)\n}\n\n// CreateTreasuryClawbackProposal creates a new proposal for an ancestor\n// DAO to sweep a descendant DAO's full treasury balance to the target's\n// parent (docs/CONSTITUTION.md :1507). The proposing DAO must be a\n// proper ancestor of the target; a target's own options can never block\n// an ancestor's clawback.\n//\n// Parameters:\n// - daoID: ID of the proposing ancestor DAO (required)\n// - targetID: ID of the descendant DAO to claw back (required)\nfunc CreateTreasuryClawbackProposal(cur realm, daoID, targetID uint64) uint64 {\n\tassertCurrent(cur)\n\n\tdao := mustGetDAO(daoID)\n\tcaller := cur.Previous().Address()\n\tassertCallerIsCouncilMember(caller, dao)\n\n\ttarget := mustGetDAO(targetID)\n\treturn mustPropose(dao, caller, kindTreasuryClawback, treasuryClawbackArgs{dao, target})\n}\n\n// CreateTreasuryFreezeProposal creates a new proposal for an ancestor DAO\n// to freeze or unfreeze a descendant DAO's treasury. While frozen, no\n// treasury spend can execute. Only a proper ancestor can unfreeze — the\n// frozen DAO's own council cannot.\n//\n// Parameters:\n// - daoID: ID of the proposing ancestor DAO (required)\n// - targetID: ID of the descendant DAO to freeze or unfreeze (required)\n// - frozen: true to freeze the target's treasury, false to unfreeze\nfunc CreateTreasuryFreezeProposal(cur realm, daoID, targetID uint64, frozen bool) uint64 {\n\tassertCurrent(cur)\n\n\tdao := mustGetDAO(daoID)\n\tcaller := cur.Previous().Address()\n\tassertCallerIsCouncilMember(caller, dao)\n\n\ttarget := mustGetDAO(targetID)\n\treturn mustPropose(dao, caller, kindTreasuryFreeze, treasuryFreezeArgs{dao, target, frozen})\n}\n\n// CreateExecutionProposal creates a proposal that runs an arbitrary\n// ExecFunc as the DAO's own sub on approval, through the realm's execution\n// kind.\n//\n// The execution kind is opt-in: it is not seeded on new DAOs and must be\n// registered first through a supermajority CreateRegisterKindProposal.\n//\n// Freeze policy: an execution proposal moves value under the DAO's own\n// authority, so it is subject to the treasury freeze exactly like a spend.\n// This wrapper fails fast when the treasury is already frozen, and the\n// definition re-checks at Execute (so a freeze landing after the proposal\n// passed fails it cleanly, StatusFailed, no funds leaving). An ancestor's\n// clawback/dissolution is a separate power and is not blocked by freeze.\n//\n// Sharp edges (known limitations):\n//   - The fn closure cannot be encoded in a CLI transaction, so this wrapper\n//     is reachable only from a PERSISTENT realm that imports this one and is\n//     a council member of the DAO (a realm-in-council). The closure must be\n//     authored in that realm so it survives Propose→Execute; a `maketx run`\n//     script's closure does not persist and cannot execute later.\n//   - A closure that panics or runs out of gas aborts the whole Execute tx, so\n//     the proposal is stuck Active (every retry re-aborts) and can never\n//     finalize. The only recovery is dissolving the DAO (Dissolve dismisses\n//     in-flight proposals). Author closures that return an error instead of\n//     panicking so a bad execution fails cleanly (StatusFailed) and releases.\n//\n// Parameters:\n// - daoID: ID of the DAO (required)\n// - title: proposal title (raw text, escaped when rendered)\n// - body: proposal body (raw text, escaped when rendered)\n// - fn: the closure executed on approval (required, non-nil)\nfunc CreateExecutionProposal(cur realm, daoID uint64, title, body string, fn commondao.ExecFunc) uint64 {\n\tassertCurrent(cur)\n\n\tdao := mustGetDAO(daoID)\n\tcaller := cur.Previous().Address()\n\tassertCallerIsCouncilMember(caller, dao)\n\n\t// Opt-in gate: refuse unless the DAO registered the execution kind.\n\t// Without this, arbitrary code execution would ride on a name a DAO\n\t// never opted into. Propose also rejects an unregistered kind, but this\n\t// fails fast with a clear message before any definition is built.\n\tassertKindRegistered(dao, kindExecution)\n\n\t// Freeze gate (intentional defense in depth): a frozen DAO cannot initiate\n\t// any treasury movement, so refuse up front here even though the\n\t// definition's Validate re-checks the same flag at create and at execute.\n\t// Redundant on purpose — two independent layers on the \"no funds leave a\n\t// frozen DAO\" invariant. Without it an execution proposal could drain a\n\t// frozen DAO's own treasury, defeating an ancestor's freeze.\n\tif dao.IsTreasuryFrozen() {\n\t\tpanic(errTreasuryFrozen)\n\t}\n\n\treturn mustPropose(dao, caller, kindExecution, executionArgs{title: title, body: body, fn: fn})\n}\n\n// CreateRegisterKindProposal creates a proposal to register one of the\n// realm's catalog proposal kinds on a DAO by name, through the permanent\n// manage-kinds kind (e.g. register \"execution\").\n//\n// The proposal is hosted and voted in the DAO itself and decided by\n// supermajority; on approval the named catalog kind is registered, so new\n// proposals of that kind can be created. The manage-kinds kind is seeded\n// on every DAO, so this path is always available.\n//\n// Parameters:\n// - daoID: ID of the DAO (required)\n// - kindName: name of the catalog proposal kind to register (required)\nfunc CreateRegisterKindProposal(cur realm, daoID uint64, kindName string) uint64 {\n\tassertCurrent(cur)\n\n\tdao := mustGetDAO(daoID)\n\tcaller := cur.Previous().Address()\n\tassertCallerIsCouncilMember(caller, dao)\n\n\treturn mustPropose(dao, caller, kindManageKinds, manageKindsProposal{dao: dao, name: kindName})\n}\n\n// CreateDeregisterKindProposal creates a proposal to deregister a proposal\n// kind from a DAO by name, through the permanent manage-kinds kind.\n//\n// The proposal is hosted and voted in the DAO itself and decided by\n// supermajority; on approval the kind is deregistered, which blocks new\n// proposals of that kind while in-flight ones still vote and execute. The\n// manage-kinds kind itself cannot be deregistered, so a DAO always keeps\n// the ability to manage its kind set (and to re-register a catalog kind by\n// name).\n//\n// Parameters:\n// - daoID: ID of the DAO (required)\n// - kindName: name of the proposal kind to deregister (required)\nfunc CreateDeregisterKindProposal(cur realm, daoID uint64, kindName string) uint64 {\n\tassertCurrent(cur)\n\n\tdao := mustGetDAO(daoID)\n\tcaller := cur.Previous().Address()\n\tassertCallerIsCouncilMember(caller, dao)\n\n\treturn mustPropose(dao, caller, kindManageKinds, manageKindsProposal{dao: dao, remove: true, name: kindName})\n}\n\n// CreateAmendBylawsProposal creates a proposal to add, amend or remove one\n// of the DAO's bylaws documents with a verifiable diff patch (see\n// gno.land/p/nt/bylaws/v0). The mandates/ folder is reserved: the\n// Constitution grants a council self-power over its Bylaws only, so\n// mandates change from above (creation or an ancestor's amendment — not\n// implemented yet), never through this proposal.\n// The payload is an encoded patch — build it with\n// AmendBylawsPayload (e.g. through a vm/qeval query) or with\n// bylaws.Diff(...).Encode() from a realm. The patch pins the sha256 of the\n// document text it was diffed against, so an amendment racing a concurrent\n// change to the same document fails cleanly instead of clobbering it; a\n// patch already stale at creation is rejected here. Amendments are decided\n// by supermajority — the default council rule; the Constitution names no\n// special threshold for a council amending its own documents.\n//\n// Parameters:\n// - daoID: ID of the DAO (required)\n// - payload: encoded bylaws patch (required)\nfunc CreateAmendBylawsProposal(cur realm, daoID uint64, payload string) uint64 {\n\tassertCurrent(cur)\n\n\tdao := mustGetDAO(daoID)\n\tcaller := cur.Previous().Address()\n\tassertCallerIsCouncilMember(caller, dao)\n\n\tpatch, err := bylaws.DecodePatch(payload)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn mustPropose(dao, caller, kindAmendBylaws, amendBylawsProposal{\n\t\tdaoID: daoID,\n\t\tset:   bylawsOf(daoID),\n\t\tpatch: patch,\n\t})\n}\n\n// AmendBylawsPayload builds the CreateAmendBylawsProposal payload that\n// changes a DAO's document at path to the proposed text: a new path adds a\n// document, empty proposed text removes one. It diffs against the\n// document's current text and pins its hash, so build the payload fresh\n// (e.g. through a vm/qeval query) and propose promptly — a payload built\n// against superseded text is rejected. Read-only.\nfunc AmendBylawsPayload(daoID uint64, path, proposed string) string {\n\tmustGetDAO(daoID)\n\n\tvar (\n\t\tcur    string\n\t\texists bool\n\t)\n\tif set := bylawsView(daoID); set != nil {\n\t\tcur, exists = set.Get(path)\n\t}\n\n\tp, err := bylaws.DiffTexts(path, cur, proposed, exists)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn p.Encode()\n}\n\n// assertKindRegistered panics unless a proposal kind is registered on the\n// DAO. Used to gate the opt-in propose paths so they work only after the DAO\n// registered the kind through governance.\nfunc assertKindRegistered(dao *commondao.CommonDAO, name string) {\n\tif !dao.HasKind(name) {\n\t\tpanic(\"proposal kind is not registered: \" + name)\n\t}\n}\n\n// mustPropose submits a proposal through one of the DAO's registered\n// proposal kinds and validates it for the current state, panicking on any\n// error. Validation also reruns inside Execute, so this only fails fast at\n// creation.\nfunc mustPropose(dao *commondao.CommonDAO, caller address, kind string, args any) uint64 {\n\tp, err := dao.Propose(caller, kind, args)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tif err = p.Validate(); err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn p.ID()\n}\n\n// parseAddresses parses a newline separated list of addresses,\n// deduplicated, panicking on invalid entries.\nfunc parseAddresses(s string) []address {\n\tvar addrs []address\n\tfor _, raw := range strings.Split(s, \"\\n\") {\n\t\traw = strings.TrimSpace(raw)\n\t\tif raw == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\taddr := address(raw)\n\t\tif !addr.IsValid() {\n\t\t\tpanic(\"invalid address: \" + addr.String())\n\t\t}\n\n\t\tif !containsAddress(addrs, addr) {\n\t\t\taddrs = append(addrs, addr)\n\t\t}\n\t}\n\treturn addrs\n}\n\n// containsAddress checks if an address is present in a list.\nfunc containsAddress(addrs []address, addr address) bool {\n\tfor _, a := range addrs {\n\t\tif a == addr {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"},{"name":"public_test.gno","body":"package commondao\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\n// TestAssertRunningPath pins the deploy-time guard on the package path.\n// Treasury addresses derive from the pkgPath const while Execute mints\n// cur.Sub under the path the realm is actually running at, so a copy\n// deployed elsewhere would read balances at one address family and send\n// from another. The guard turns that into a genesis abort; nothing else\n// in the suite notices if it stops doing so.\nfunc TestAssertRunningPath(cur realm, t *testing.T) {\n\tassertRunningPath() // running at pkgPath: must not panic\n\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/nt/commondao/v1\"))\n\turequire.PanicsWithMessage(t, cur,\n\t\t\"commondao: realm deployed at gno.land/r/nt/commondao/v1, expected gno.land/r/nt/commondao/v0\",\n\t\tfunc() { assertRunningPath() })\n}\n"},{"name":"reentrancy.gno","body":"package commondao\n\n// executing is this realm's GLOBAL re-entrancy latch, the outer of two\n// complementary layers. It is set while a proposal executor is running\n// (inside the public Execute) and rejects any nested Execute — an executor\n// must never trigger another execution, whether of the same DAO or a\n// different one, directly or by crossing back in as its DAO.\n//\n// The two layers guard different invariants and neither subsumes the other:\n//\n//   - The /p/ CommonDAO carries per-DAO `executing` / `proposing` FIELDS\n//     (see the package's commondao.gno). Being fields on the owning DAO\n//     object, they are writable where a mutable /p/ global is not, so they\n//     travel with a standalone /p/ consumer that never wrote a realm-global\n//     latch: they give every consumer same-DAO re-entrancy protection (a\n//     nested Execute or Propose on the SAME DAO).\n//   - This realm-global bool adds what a per-DAO field structurally cannot:\n//     it enforces one-executor-per-transaction across ALL DAOs, closing the\n//     cross-DAO straddle a per-DAO latch misses — the ancestor-hosted\n//     dissolution/clawback case, where an executor running under DAO A's\n//     Execute nests an Execute that deletes a DIFFERENT DAO out from under\n//     A. A latch keyed on the executing DAO never fires there; a target\n//     latch would demand a scattered check at every DAO-mutating executor.\n//     The single global bool makes \"one executor per tx\" a local,\n//     unforgettable invariant instead, and also shuts the conditional-\n//     execution \"free option\" (nest a different DAO's Execute, observe\n//     in-tx, panic to revert).\n//\n// It is realm module state: a mutable /p/ package global is forbidden\n// (writing one panics the borrow/stamping rule), and the latch must be\n// writable. This global is the realm's, complementing — not replacing — the\n// /p/ per-DAO fields.\n//\n// Vote / Create* / Withdraw / Resign are deliberately NOT latched: no executor\n// re-enters them, legitimate executors need them (sub-DAO creation, the\n// dissolution sweep), and a proposal executor acting as its DAO in ANOTHER DAO\n// (e.g. casting a council vote) rides those paths.\n//\n// A rejected nested Execute panics. Thrown across the cross() boundary the\n// re-entrant call arrived through, that aborts the whole transaction.\n//\n// The deferred leaveExecute lowers the flag on normal returns (and on any\n// same-transaction recovered panic). Cross-transaction safety does NOT rely on\n// that defer — a rejected re-entry aborts the transaction, and the aborted\n// transaction's realm writes (including this flag) are never committed, so a\n// later transaction can never observe it stuck true.\nvar executing bool\n\n// enterExecute raises the re-entrancy latch, panicking if it is already raised\n// (i.e. this Execute is nested inside another). Pair with a deferred\n// leaveExecute.\nfunc enterExecute() {\n\tif executing {\n\t\tpanic(\"commondao: re-entrant Execute is not allowed\")\n\t}\n\texecuting = true\n}\n\n// leaveExecute lowers the re-entrancy latch.\nfunc leaveExecute() {\n\texecuting = false\n}\n"},{"name":"reentrancy_test.gno","body":"package commondao\n\nimport \"testing\"\n\n// TestExecuteReentrancyLatch pins the latch mechanism directly: a nested\n// enterExecute (which is what a re-entrant Execute reaches) must panic, and\n// leaveExecute must clear the latch so the next execution can proceed. The\n// end-to-end re-entry through a real proposal closure that crosses back into\n// Execute is covered by the z_20_i filetest.\nfunc TestExecuteReentrancyLatch(t *testing.T) {\n\tleaveExecute() // ensure a clean starting state\n\n\t// First entry raises the latch.\n\tenterExecute()\n\tif !executing {\n\t\tt.Fatal(\"expected the latch to be raised after enterExecute\")\n\t}\n\n\t// A nested entry (the re-entrant Execute) must panic.\n\tfunc() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r == nil {\n\t\t\t\tt.Fatal(\"expected a nested enterExecute to panic\")\n\t\t\t}\n\t\t}()\n\t\tenterExecute()\n\t\tt.Fatal(\"unreachable: the nested enterExecute must panic\")\n\t}()\n\n\t// The rejected re-entry must not have lowered the latch.\n\tif !executing {\n\t\tt.Fatal(\"the latch must stay raised after a rejected re-entry\")\n\t}\n\n\t// Leaving lowers it; the next execution may then proceed.\n\tleaveExecute()\n\tif executing {\n\t\tt.Fatal(\"expected the latch to be lowered after leaveExecute\")\n\t}\n\tenterExecute()\n\tleaveExecute()\n}\n"},{"name":"render.gno","body":"package commondao\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/jeronimoalbi/pager/v0\"\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/moul/mdtable/v0\"\n\t\"gno.land/p/moul/realmpath/v0\"\n\t\"gno.land/p/nt/addrset/v0\"\n\t\"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/p/nt/markdown/sanitize/v0\"\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\nconst dateFormat = \"Mon, 02 Jan 2006 03:04pm MST\"\n\n// trustedMarkdownBody marks proposal definitions whose Body returns\n// markdown the definition assembled itself (escaping any embedded user\n// fields with md.EscapeText). Those bodies are rendered verbatim. Every\n// other body — including the execution kind, whose Body is the proposer's\n// raw text — is treated as user-supplied and escaped by the renderer. A\n// definition from an imported package cannot implement this realm-local\n// marker either, so it is escaped too. The render layer is the single escape\n// point: definitions return raw text and opt out only when they render their\n// own markup.\ntype trustedMarkdownBody interface {\n\tisTrustedMarkdownBody()\n}\n\nfunc Render(path string) string {\n\trouter := mux.NewRouter()\n\trouter.HandleFunc(\"\", renderHome)\n\trouter.HandleFunc(\"{daoID}\", renderDAO)\n\trouter.HandleFunc(\"{daoID}/settings\", renderSettings)\n\trouter.HandleFunc(\"{daoID}/bylaws\", renderBylaws)\n\trouter.HandleFunc(\"{daoID}/proposals\", renderProposalsList)\n\trouter.HandleFunc(\"{daoID}/proposals/{proposalID}\", renderProposal)\n\trouter.HandleFunc(\"{daoID}/proposals/{proposalID}/vote/{address}\", renderProposalVote)\n\treturn router.Render(path)\n}\n\nfunc renderHome(res *mux.ResponseWriter, req *mux.Request) {\n\tres.Write(md.H1(\"Common DAO\"))\n\tres.Write(md.HorizontalRule())\n\tres.Write(ufmt.Sprintf(\n\t\tmd.Paragraph(\"This realm can be used to create CommonDAO instances based on %s package.\"),\n\t\tmd.Link(\"commondao\", \"/p/nt/commondao/v0/\"),\n\t))\n\n\t// Nothing listed: render just the header. pager.New reports a total of\n\t// zero as \"invalid page number\" for every page including page 1, which\n\t// its own Picker links to.\n\tif listed.Size() == 0 {\n\t\treturn\n\t}\n\n\t// Paginate over the LISTED set, not every DAO: listing is opt-in and\n\t// off by default, so a window over all DAOs could be entirely unlisted\n\t// — rendering an empty page that also drops the pager (the len==0 guard\n\t// below), stranding the listed DAOs on unreachable later pages.\n\tpages, err := pager.New(req.RawPath, listed.Size(), pager.WithPageSize(10))\n\tif err != nil {\n\t\t// Its own block: an unterminated error line glues itself onto\n\t\t// whatever is written next (renderCouncil returns into renderDAO,\n\t\t// which goes straight on to write the Treasury heading).\n\t\tres.Write(md.Paragraph(md.EscapeText(err.Error())))\n\t\treturn\n\t}\n\n\tvar items []string\n\tlisted.ReverseIterateByOffset(pages.Offset(), pages.PageSize(), func(_ string, v any) bool {\n\t\tif dao := getDAO(v.(uint64)); dao != nil {\n\t\t\titems = append(items, md.Link(dao.Name(), daoURL(dao.ID())))\n\t\t}\n\t\treturn false\n\t})\n\n\tif len(items) == 0 {\n\t\treturn\n\t}\n\n\tres.Write(md.Paragraph(\"Here is a list of some of the DAOs that were created:\"))\n\tres.Write(md.Paragraph(md.BulletList(items)))\n\n\tif pages.HasPages() {\n\t\tres.Write(md.Paragraph(pager.Picker(pages)))\n\t}\n}\n\nfunc renderDAO(res *mux.ResponseWriter, req *mux.Request) {\n\tdao := mustGetDAOFromRequest(req)\n\n\t// Render header messages\n\tif dao.IsDeleted() {\n\t\tres.Write(md.Blockquote(\"⚠ This DAO has been dissolved\"))\n\t\t// A dissolved DAO with no live proper ancestor can never move funds\n\t\t// again (its own council is gone and no ancestor can claw back), so\n\t\t// warn that late deposits to its treasury address are unrecoverable.\n\t\tif !hasLiveProperAncestor(dao) {\n\t\t\tres.Write(md.Blockquote(\"⚠ No live ancestor DAO remains: coins sent to this treasury address can no longer be recovered.\"))\n\t\t}\n\t}\n\n\t// Render header (Charter: purpose is required, description optional)\n\tres.Write(md.H1(md.EscapeText(dao.Name())))\n\tres.Write(md.Paragraph(md.Bold(\"Purpose:\") + \" \" + md.EscapeText(dao.Purpose())))\n\tif desc := dao.Description(); desc != \"\" {\n\t\tres.Write(md.Paragraph(md.EscapeText(desc)))\n\t}\n\n\t// Render main menu\n\tmenu := []string{\n\t\tmd.Link(\"View Proposals\", daoProposalsURL(dao.ID())),\n\t\tmd.Link(\"View Settings\", settingsURL(dao.ID())),\n\t}\n\n\tif set := bylawsView(dao.ID()); set != nil \u0026\u0026 set.Size() \u003e 0 {\n\t\tmenu = append(menu, md.Link(\"View Bylaws \u0026 Mandates\", bylawsURL(dao.ID())))\n\t}\n\n\tif parentDAO := dao.Parent(); parentDAO != nil {\n\t\tmenu = append(menu, md.Link(\"Go to Parent DAO\", daoURL(parentDAO.ID())))\n\t}\n\n\tres.Write(md.Paragraph(strings.Join(menu, \" • \")))\n\tres.Write(md.HorizontalRule())\n\n\t// Render council\n\tcouncil := dao.Council()\n\tif council.Size() == 0 {\n\t\tres.Write(md.Paragraph(md.Bold(\"⚠ The DAO has no council members\")))\n\t} else {\n\t\trenderCouncil(res, req.RawPath, council)\n\t}\n\n\trenderTreasury(res, dao)\n\n\t// Render organization tree\n\tif dao.ChildrenCount() \u003e 0 {\n\t\tr := parseRealmPath(req.RawPath)\n\t\tdissolvedVisible := r.Query.Has(\"dissolved\") || dao.IsDeleted()\n\t\tres.Write(md.H2(\"Tree\"))\n\n\t\t// Render toggle only when DAO is not dissolved,\n\t\t// otherwise render the whole tree when DAO is dissolved\n\t\tif !dao.IsDeleted() {\n\t\t\tvar toggleLink string\n\t\t\tif dissolvedVisible {\n\t\t\t\tr.Query.Del(\"dissolved\")\n\t\t\t\ttoggleLink = md.Link(\"hide\", r.String())\n\t\t\t} else {\n\t\t\t\tr.Query.Add(\"dissolved\", \"\")\n\t\t\t\ttoggleLink = md.Link(\"show\", r.String())\n\t\t\t}\n\n\t\t\tres.Write(md.Paragraph(\"Dissolved: \" + toggleLink))\n\t\t}\n\n\t\trenderTree(res, dao, \"\", dissolvedVisible)\n\t}\n\n\t// Render latest proposals\n\tif dao.ActiveProposalsSize() \u003e 0 {\n\t\tres.Write(md.H2(\"Latest Proposals\"))\n\t\tdao.IterateActiveProposals(0, 3, true, func(p *commondao.Proposal) bool {\n\t\t\trenderProposalsListItem(res, dao, p)\n\t\t\treturn false\n\t\t})\n\t}\n\n\t// Render proposal creation links\n\tif !dao.IsDeleted() {\n\t\trenderCreateProposalSection(dao, res)\n\t}\n}\n\nfunc renderCreateProposalSection(dao *commondao.CommonDAO, res *mux.ResponseWriter) {\n\tdaoID := dao.ID()\n\n\t// One entry per link-encodable catalog kind, in display order (the\n\t// execution kind has no entry: its closure cannot be encoded in a\n\t// help link). Only the kinds registered on the DAO are rendered.\n\tentries := []struct {\n\t\tkind string\n\t\ttext string\n\t}{\n\t\t{\n\t\t\tkindText,\n\t\t\tmd.Paragraph(textProposalLink(daoID)) +\n\t\t\t\tmd.Paragraph(\n\t\t\t\t\t\"This type of proposal is also known as text proposal which can be used for example \"+\n\t\t\t\t\t\t\"to get consensus on initiatives without actually making any change on-chain.\",\n\t\t\t\t),\n\t\t},\n\t\t{\n\t\t\tkindCouncilUpdate,\n\t\t\tmd.Paragraph(updateCouncilLink(daoID)) +\n\t\t\t\tmd.Paragraph(\n\t\t\t\t\t\"This type of proposal can be used to add new council members to this DAO \"+\n\t\t\t\t\t\t\"and also to remove existing ones.\",\n\t\t\t\t) +\n\t\t\t\tmd.Paragraph(\n\t\t\t\t\t\"A single proposal allows new council members to be added and any number of existing ones \"+\n\t\t\t\t\t\t\"removed within the same proposal.\",\n\t\t\t\t),\n\t\t},\n\t\t{\n\t\t\tkindAncestorCouncilUpdate,\n\t\t\tmd.Paragraph(ancestorCouncilUpdateLink(daoID)) +\n\t\t\t\tmd.Paragraph(\n\t\t\t\t\t\"This type of proposal lets this DAO, as an ancestor, add or \"+\n\t\t\t\t\t\t\"remove council members of one of its descendant DAOs — the \"+\n\t\t\t\t\t\t\"rescue path for a descendant whose council is stuck or empty.\",\n\t\t\t\t),\n\t\t},\n\t\t{\n\t\t\tkindDissolve,\n\t\t\tmd.Paragraph(dissolveSubDAOLink(daoID)) +\n\t\t\t\tmd.Paragraph(\n\t\t\t\t\t\"This type of proposal can be used to dissolve DAOs and SubDAOs.\",\n\t\t\t\t) +\n\t\t\t\tmd.Paragraph(\n\t\t\t\t\t\"Dissolving a DAO can't be undone, once the dissolution proposal passes \"+\n\t\t\t\t\t\t\"and is executed DAO will be readonly.\",\n\t\t\t\t),\n\t\t},\n\t\t{\n\t\t\tkindTreasurySpend,\n\t\t\tmd.Paragraph(treasurySpendLink(daoID)) +\n\t\t\t\tmd.Paragraph(\n\t\t\t\t\t\"This type of proposal sends coins from the DAO's own treasury \"+\n\t\t\t\t\t\t\"when it passes and is executed.\",\n\t\t\t\t),\n\t\t},\n\t\t{\n\t\t\tkindTreasuryClawback,\n\t\t\tmd.Paragraph(treasuryClawbackLink(daoID)) +\n\t\t\t\tmd.Paragraph(\n\t\t\t\t\t\"This type of proposal sweeps a descendant DAO's treasury to \"+\n\t\t\t\t\t\t\"the descendant's parent DAO.\",\n\t\t\t\t),\n\t\t},\n\t\t{\n\t\t\tkindTreasuryFreeze,\n\t\t\tmd.Paragraph(treasuryFreezeLink(daoID)) +\n\t\t\t\tmd.Paragraph(\n\t\t\t\t\t\"This type of proposal freezes or unfreezes a descendant DAO's \"+\n\t\t\t\t\t\t\"treasury. While frozen, no funds can leave the treasury.\",\n\t\t\t\t),\n\t\t},\n\t\t{\n\t\t\tkindSubDAO,\n\t\t\tmd.Paragraph(newSubDAOLink(daoID)) +\n\t\t\t\tmd.Paragraph(\n\t\t\t\t\t\"This type of proposal is used to create SubDAOs, \"+\n\t\t\t\t\t\t\"which are used to create tree based DAOs.\",\n\t\t\t\t),\n\t\t},\n\t\t{\n\t\t\tkindManageKinds,\n\t\t\tmd.Paragraph(registerKindLink(daoID)+\" • \"+deregisterKindLink(daoID)) +\n\t\t\t\tmd.Paragraph(\n\t\t\t\t\t\"This type of proposal registers one of the realm's catalog \"+\n\t\t\t\t\t\t\"proposal kinds on this DAO by name, or deregisters a kind. \"+\n\t\t\t\t\t\t\"Deregistering a kind blocks new proposals of that kind while \"+\n\t\t\t\t\t\t\"in-flight ones still vote and execute. The manage-kinds kind \"+\n\t\t\t\t\t\t\"itself cannot be deregistered.\",\n\t\t\t\t),\n\t\t},\n\t\t{\n\t\t\tkindAmendBylaws,\n\t\t\tmd.Paragraph(amendBylawsLink(daoID)) +\n\t\t\t\tmd.Paragraph(\n\t\t\t\t\t\"This type of proposal adds, amends or removes one of the DAO's \"+\n\t\t\t\t\t\t\"bylaws documents with a diff patch pinned to the current \"+\n\t\t\t\t\t\t\"document text (the mandates folder is reserved: mandates are \"+\n\t\t\t\t\t\t\"set from above, not by the council). Build the payload with \"+\n\t\t\t\t\t\t\"the AmendBylawsPayload query function.\",\n\t\t\t\t),\n\t\t},\n\t}\n\n\tvar cols []string\n\tfor _, e := range entries {\n\t\tif dao.HasKind(e.kind) {\n\t\t\tcols = append(cols, e.text)\n\t\t}\n\t}\n\n\tif len(cols) == 0 {\n\t\treturn\n\t}\n\n\tres.Write(md.H2(\"Create Proposal\"))\n\tres.Write(md.Paragraph(\"These are the proposal supported by this DAO:\"))\n\t// Wrap at 3 per row: gnoweb renders at most 4 columns per row, and a\n\t// DAO with every kind registered has up to ten cards.\n\tres.Write(md.ColumnsN(cols, 3, false))\n}\n\nfunc renderTreasury(res *mux.ResponseWriter, dao *commondao.CommonDAO) {\n\tres.Write(md.H2(\"Treasury\"))\n\tres.Write(md.BulletItem(\"Address: \" + md.EscapeText(string(dao.Address()))))\n\n\tbalance := treasuryBalance(dao)\n\tif balance.IsZero() {\n\t\tres.Write(md.BulletItem(\"Balance: (empty)\"))\n\t} else {\n\t\tres.Write(md.BulletItem(\"Balance: \" + md.EscapeText(balance.String())))\n\t}\n\n\tif dao.IsTreasuryFrozen() {\n\t\t// Blockquote, not a bold paragraph: a paragraph directly after the\n\t\t// balance bullet is a lazy continuation of that list item, so the\n\t\t// warning would render inside the bullet instead of as its own\n\t\t// block. Matches the dissolved-DAO banners.\n\t\tfrozen := \"⚠ The treasury is frozen: no proposal can move funds until a proper ancestor DAO unfreezes it.\"\n\t\tif dao.HasKind(kindExecution) {\n\t\t\t// Freeze gates the realm's own spending paths. A closure from a\n\t\t\t// past execution proposal may hold a retained banker, which\n\t\t\t// reaches the bank keeper without passing through them.\n\t\t\tfrozen += \" This DAO has the execution kind registered, so a\" +\n\t\t\t\t\" capability retained by an earlier execution proposal could\" +\n\t\t\t\t\" still move funds; freeze alone is not containment here.\"\n\t\t}\n\t\tres.Write(md.Blockquote(frozen))\n\t}\n}\n\nfunc renderCouncil(res *mux.ResponseWriter, path string, council *addrset.ReadonlySet) {\n\tpages, err := pager.New(path, council.Size(), pager.WithPageQueryParam(\"members\"), pager.WithPageSize(8))\n\tif err != nil {\n\t\t// Its own block: an unterminated error line glues itself onto\n\t\t// whatever is written next (renderCouncil returns into renderDAO,\n\t\t// which goes straight on to write the Treasury heading).\n\t\tres.Write(md.Paragraph(md.EscapeText(err.Error())))\n\t\treturn\n\t}\n\n\ttable := mdtable.Table{Headers: []string{\"Council\"}}\n\tcouncil.IterateByOffset(pages.Offset(), pages.PageSize(), func(addr address) bool {\n\t\ttable.Append([]string{userLink(addr)})\n\t\treturn false\n\t})\n\n\tres.Write(md.Paragraph(table.String()))\n\n\tif pages.HasPages() {\n\t\tres.Write(md.Paragraph(pager.Picker(pages)))\n\t}\n}\n\nfunc renderTree(res *mux.ResponseWriter, dao *commondao.CommonDAO, indent string, showDissolved bool) {\n\tdaoLink := md.Link(dao.Name(), daoURL(dao.ID()))\n\tif dao.IsDeleted() {\n\t\t// Strikethough dissolved DAO names\n\t\tdaoLink = md.Strikethrough(daoLink)\n\t}\n\n\tres.Write(indent + md.BulletItem(daoLink))\n\n\tindent += \"  \"\n\tdao.IterateChildren(func(subDAO *commondao.CommonDAO) bool {\n\t\tif showDissolved || !subDAO.IsDeleted() {\n\t\t\trenderTree(res, subDAO, indent, showDissolved)\n\t\t}\n\t\treturn false\n\t})\n}\n\nfunc renderSettings(res *mux.ResponseWriter, req *mux.Request) {\n\tdao := mustGetDAOFromRequest(req)\n\n\t// Render header\n\tres.Write(md.H1(md.EscapeText(dao.Name()) + \": Settings\"))\n\n\t// Render main menu\n\tres.Write(md.Paragraph(goToDAOLink(dao.ID())))\n\tres.Write(md.HorizontalRule())\n\n\t// Render info\n\ttable := mdtable.Table{Headers: []string{\"Setting\", \"Value\"}}\n\ttable.Append([]string{\"Listed\", strconv.FormatBool(isListed(dao.ID()))})\n\ttable.Append([]string{\"Max active proposals\", strconv.Itoa(dao.MaxActiveProposals())})\n\ttable.Append([]string{\"Proposal kinds\", md.EscapeText(strings.Join(dao.KindNames(), \", \"))})\n\n\tres.Write(md.H2(\"Info\"))\n\tres.Write(table.String())\n}\n\nfunc renderBylaws(res *mux.ResponseWriter, req *mux.Request) {\n\tdao := mustGetDAOFromRequest(req)\n\n\t// Render header\n\tres.Write(md.H1(md.EscapeText(dao.Name()) + \": Bylaws \u0026 Mandates\"))\n\n\t// Render main menu\n\tres.Write(md.Paragraph(goToDAOLink(dao.ID())))\n\tres.Write(md.HorizontalRule())\n\n\tset := bylawsView(dao.ID())\n\tif set == nil || set.Size() == 0 {\n\t\tres.Write(md.Paragraph(\"This DAO has no bylaws or mandates documents.\"))\n\t\treturn\n\t}\n\n\tpaths := set.List(\"\")\n\tpages, err := pager.New(req.RawPath, len(paths), pager.WithPageSize(5))\n\tif err != nil {\n\t\t// Its own block: an unterminated error line glues itself onto\n\t\t// whatever is written next (renderCouncil returns into renderDAO,\n\t\t// which goes straight on to write the Treasury heading).\n\t\tres.Write(md.Paragraph(md.EscapeText(err.Error())))\n\t\treturn\n\t}\n\n\t// Documents are multi-line plaintext with council-controlled content:\n\t// the path is escaped inline and the text goes through sanitize.Block,\n\t// which preserves paragraph structure (an inline escape would fold the\n\t// whole document to one line) while escaping block-level hazards. The\n\t// hash is what an amendment payload must pin (AmendBylawsPayload reads\n\t// it for you).\n\t// Clamp the offset: pager.New rejects page 0 and pages past the end but\n\t// accepts a negative page, and unlike the tree-backed pagers this one\n\t// indexes a slice — a raw negative offset panics the whole render.\n\tstart := pages.Offset()\n\tif start \u003c 0 {\n\t\tstart = 0\n\t}\n\tfor i := start; i \u003c len(paths) \u0026\u0026 i \u003c start+pages.PageSize(); i++ {\n\t\tpath := paths[i]\n\t\ttext, _ := set.Get(path)\n\t\tres.Write(md.H2(md.EscapeText(path)))\n\t\tres.Write(md.Paragraph(\"sha256: \" + set.Hash(path)))\n\t\tres.Write(sanitize.Block(text))\n\t}\n\n\tif pages.HasPages() {\n\t\tres.Write(md.Paragraph(pager.Picker(pages)))\n\t}\n}\n\nfunc renderProposalsList(res *mux.ResponseWriter, req *mux.Request) {\n\tdao := mustGetDAOFromRequest(req)\n\n\t// Render header\n\tres.Write(md.H1(md.EscapeText(dao.Name()) + \": Proposals\"))\n\n\t// Render main menu\n\tres.Write(md.Paragraph(goToDAOLink(dao.ID())))\n\tres.Write(md.HorizontalRule())\n\n\t// Render proposals\n\tif dao.ActiveProposalsSize() == 0 \u0026\u0026 dao.FinishedProposalsSize() == 0 {\n\t\tres.Write(md.Paragraph(md.Bold(\"⚠ The DAO has no proposals\")))\n\t\treturn\n\t}\n\n\tsize := dao.ActiveProposalsSize()\n\titerate := dao.IterateActiveProposals\n\trenderFinished := req.Query.Has(\"finished\")\n\tif renderFinished {\n\t\tsize = dao.FinishedProposalsSize()\n\t\titerate = dao.IterateFinishedProposals\n\t}\n\n\tvar viewLink, sortLink string\n\n\tr := parseRealmPath(req.RawPath)\n\tr.Query.Del(\"page\") // a view switch resets to page 1 (the other view may have fewer pages)\n\tif renderFinished {\n\t\tr.Query.Del(\"finished\")\n\t\tviewLink = md.Link(\"active\", r.String())\n\t} else {\n\t\tr.Query.Add(\"finished\", \"\")\n\t\tviewLink = md.Link(\"finished\", r.String())\n\t}\n\n\tr = parseRealmPath(req.RawPath)\n\tr.Query.Del(\"page\") // a sort switch resets to page 1\n\treverseSort := r.Query.Get(\"order\") != \"asc\"\n\tif reverseSort {\n\t\tr.Query.Set(\"order\", \"asc\")\n\t\tsortLink = md.Link(\"oldest\", r.String())\n\t} else {\n\t\tr.Query.Set(\"order\", \"desc\")\n\t\tsortLink = md.Link(\"newest\", r.String())\n\t}\n\n\tres.Write(md.Paragraph(\"View: \" + viewLink + \" • Sort by: \" + sortLink))\n\n\t// The empty view is handled before the pager: pager.New reports a total\n\t// of zero as \"invalid page number\" for every page including page 1,\n\t// which the other view's Picker links to — so switching into an empty\n\t// view would answer with an error instead of the empty-state message.\n\tif size == 0 {\n\t\tif renderFinished {\n\t\t\tres.Write(md.Paragraph(\"Currently there are no finished proposals\"))\n\t\t} else {\n\t\t\tres.Write(md.Paragraph(\"Currently there are no active proposals\"))\n\t\t}\n\t\treturn\n\t}\n\n\tpages, err := pager.New(req.RawPath, size, pager.WithPageSize(8))\n\tif err != nil {\n\t\tres.Write(md.Paragraph(md.EscapeText(err.Error())))\n\t\treturn\n\t}\n\n\titerate(pages.Offset(), pages.PageSize(), reverseSort, func(p *commondao.Proposal) bool {\n\t\trenderProposalsListItem(res, dao, p)\n\t\treturn false\n\t})\n\n\t// Render pager\n\tif pages.HasPages() {\n\t\tres.Write(md.HorizontalRule())\n\t\tres.Write(pager.Picker(pages))\n\t}\n}\n\nfunc renderProposalsListItem(res *mux.ResponseWriter, dao *commondao.CommonDAO, p *commondao.Proposal) {\n\tdef := p.Definition()\n\trecord := p.VotingRecord()\n\n\t// Render title\n\tres.Write(ufmt.Sprintf(\"**[#%d %s](%s)**  \\n\", p.ID(), md.EscapeText(def.Title()), proposalURL(dao.ID(), p.ID())))\n\n\t// Render details\n\tres.Write(ufmt.Sprintf(\"Created by %s  \\n\", userLink(p.Creator())))\n\tres.Write(ufmt.Sprintf(\"Voting ends on %s  \\n\", p.VotingDeadline().UTC().Format(dateFormat)))\n\n\t// Render status\n\tstatus := []string{\n\t\tufmt.Sprintf(\"Votes: **%d**\", record.Size()),\n\t\tufmt.Sprintf(\"Status: **%s**\", string(p.Status())),\n\t}\n\n\t// Render actions\n\tif isVotingPeriodActive(p) {\n\t\tstatus = append(status, voteLink(dao.ID(), p.ID()))\n\t}\n\n\tif isExecutionAllowed(p) {\n\t\tstatus = append(status, executeLink(dao.ID(), p.ID()))\n\t}\n\n\tres.Write(md.Paragraph(strings.Join(status, \" • \")))\n}\n\nfunc renderProposal(res *mux.ResponseWriter, req *mux.Request) {\n\tdao := mustGetDAOFromRequest(req)\n\tp := mustGetProposalFromRequest(req, dao)\n\n\t// Check that proposal has no issues\n\tif err := p.Validate(); err != nil {\n\t\t// Escape the error text (the bold label is ours): a definition's\n\t\t// Validate may embed user-supplied values in its message.\n\t\tres.Write(md.Blockquote(\"⚠ **ERROR**: \" + md.EscapeText(err.Error())))\n\t}\n\n\tvotingActive := isVotingPeriodActive(p)\n\tif votingActive {\n\t\tres.Write(\n\t\t\tmd.Blockquote(\"Voting ends on \" + md.Bold(p.VotingDeadline().UTC().Format(dateFormat))),\n\t\t)\n\t}\n\n\tdef := p.Definition()\n\n\t// Render header\n\tres.Write(md.H1(\"#\" + strconv.FormatUint(p.ID(), 10) + \" \" + md.EscapeText(def.Title())))\n\n\t// Render main menu\n\titems := []string{goToDAOLink(dao.ID())}\n\tif votingActive {\n\t\titems = append(items, voteLink(dao.ID(), p.ID()))\n\t}\n\n\tif isExecutionAllowed(p) {\n\t\titems = append(items, executeLink(dao.ID(), p.ID()))\n\t}\n\n\tres.Write(md.Paragraph(strings.Join(items, \" • \")))\n\tres.Write(md.HorizontalRule())\n\n\t// Render details\n\tres.Write(md.H2(\"Details\"))\n\tres.Write(md.BulletItem(\"Proposer: \" + userLink(p.Creator())))\n\tres.Write(md.BulletItem(\"Submit Time: \" + p.CreatedAt().UTC().Format(time.RFC1123)))\n\n\trecord := p.VotingRecord()\n\tif p.Status() == commondao.StatusActive {\n\t\t// Vote settles the outcome early — it flips a proposal to passed or\n\t\t// dismissed the moment the tally allows — so while voting is open a\n\t\t// still-active proposal is undecided, and \"pending\" is the honest\n\t\t// label (\"fail\" would read as a prediction of defeat on a proposal\n\t\t// nobody has voted on yet). Once the deadline has passed no further\n\t\t// vote is accepted and Execute's re-tally dismisses an undecided\n\t\t// proposal, so there \"pending\" would be the misleading one.\n\t\t//\n\t\t// The decided arms are reachable in principle: /p/ honors a\n\t\t// non-constant Threshold (see ProposalDefinition.Threshold), though\n\t\t// every definition in this realm returns a constant.\n\t\tswitch p.ExpectedOutcome() {\n\t\tcase commondao.OutcomePassed:\n\t\t\tres.Write(md.BulletItem(\"Expected Outcome: **pass** ☑\"))\n\t\tcase commondao.OutcomeDismissed:\n\t\t\tres.Write(md.BulletItem(\"Expected Outcome: **dismiss** ☒\"))\n\t\tdefault:\n\t\t\tif votingActive {\n\t\t\t\tres.Write(md.BulletItem(\"Expected Outcome: **pending** ⏳\"))\n\t\t\t} else {\n\t\t\t\tres.Write(md.BulletItem(\"Expected Outcome: **dismiss** ☒\"))\n\t\t\t}\n\t\t}\n\t}\n\n\tstatusItem := \"Status: \" + md.Bold(string(p.Status()))\n\tif reason := p.StatusReason(); reason != \"\" {\n\t\tstatusItem += \" • \" + md.Italic(md.EscapeText(reason))\n\t}\n\tres.Write(md.BulletItem(statusItem))\n\n\t// Render proposal body. Bodies are raw user text and escaped here by\n\t// default; only definitions that assemble their own markdown (marked\n\t// trustedMarkdownBody) are rendered verbatim.\n\tif body := def.Body(); body != \"\" {\n\t\tres.Write(md.H2(\"Description\"))\n\t\tif _, trusted := def.(trustedMarkdownBody); !trusted {\n\t\t\t// Inline escape, deliberately: it neutralizes inline markup and\n\t\t\t// links in proposer-supplied text. sanitize.Block would preserve\n\t\t\t// the body's line structure but also its inline links — a\n\t\t\t// phishing vector on a page councils read before voting. The\n\t\t\t// cost is that a multi-line body renders as one line; a\n\t\t\t// structure-preserving fix must escape inline per line rather\n\t\t\t// than switch to Block (see z_10_b, which pins the escaping).\n\t\t\tbody = md.EscapeText(body)\n\t\t}\n\t\tres.Write(md.Paragraph(body))\n\t}\n\n\t// Render voting stats and votes\n\tif record.Size() \u003e 0 {\n\t\trenderProposalStats(res, record)\n\t\trenderProposalVotes(res, req.RawPath, dao, p)\n\t}\n}\n\nfunc renderProposalStats(res *mux.ResponseWriter, record commondao.ReadonlyVotingRecord) {\n\ttotalCount := float64(record.Size())\n\ttable := mdtable.Table{Headers: []string{\"Vote Choices\", \"Percentage of Votes\"}}\n\n\trecord.IterateVotesCount(func(c commondao.VoteChoice, voteCount int) bool {\n\t\t// A changed vote leaves a zeroed counter behind; skip it so the\n\t\t// table shows only choices with live votes.\n\t\tif voteCount == 0 {\n\t\t\treturn false\n\t\t}\n\n\t\tpercentage := float64(voteCount*100) / totalCount\n\t\ttable.Append([]string{md.EscapeText(string(c)), strconv.FormatFloat(percentage, 'f', 2, 64) + \"%\"})\n\t\treturn false\n\t})\n\n\tres.Write(md.H2(\"Stats\"))\n\tres.Write(md.Paragraph(table.String()))\n}\n\nfunc renderProposalVotes(res *mux.ResponseWriter, path string, dao *commondao.CommonDAO, p *commondao.Proposal) {\n\tres.Write(md.H2(\"Votes\")) // Render title here so it appears before any pager errors\n\n\trecord := p.VotingRecord()\n\tpages, err := pager.New(path, record.Size(), pager.WithPageQueryParam(\"votes\"), pager.WithPageSize(5))\n\tif err != nil {\n\t\t// Its own block: an unterminated error line glues itself onto\n\t\t// whatever is written next (renderCouncil returns into renderDAO,\n\t\t// which goes straight on to write the Treasury heading).\n\t\tres.Write(md.Paragraph(md.EscapeText(err.Error())))\n\t\treturn\n\t}\n\n\ttable := mdtable.Table{Headers: []string{\"Users\", \"Votes\"}}\n\trecord.Iterate(pages.Offset(), pages.PageSize(), false, func(v commondao.Vote) bool {\n\t\tvoteDetails := md.Link(string(v.Choice()), voteURL(dao.ID(), p.ID(), v.Address()))\n\t\tif v.Reason() != \"\" {\n\t\t\tvoteDetails += \" with a reason\"\n\t\t}\n\n\t\ttable.Append([]string{userLink(v.Address()), voteDetails})\n\t\treturn false\n\t})\n\n\tres.Write(ufmt.Sprintf(\"Total number of votes: **%d**\\n\", record.Size()))\n\tres.Write(md.Paragraph(table.String()))\n\n\tif pages.HasPages() {\n\t\tres.Write(md.Paragraph(pager.Picker(pages)))\n\t}\n}\n\nfunc renderProposalVote(res *mux.ResponseWriter, req *mux.Request) {\n\t// Resolve the DAO and proposal before validating the address, so a bad\n\t// daoID is reported as such instead of being blamed on the address, and\n\t// write the header first so every branch below is a navigable page\n\t// rather than a bare dead-end string.\n\tdao := mustGetDAOFromRequest(req)\n\tp := mustGetProposalFromRequest(req, dao)\n\n\tlinks := []string{\n\t\tgoToDAOLink(dao.ID()),\n\t\tgoToProposalLink(dao.ID(), p.ID()),\n\t}\n\n\tres.Write(md.H1(ufmt.Sprintf(\"Vote: Proposal #%d\", p.ID())))\n\tres.Write(md.Paragraph(strings.Join(links, \" • \")))\n\tres.Write(md.HorizontalRule())\n\n\tmember := address(req.GetVar(\"address\"))\n\tif !member.IsValid() {\n\t\tres.Write(md.Paragraph(\"Invalid address.\"))\n\t\treturn\n\t}\n\n\tv, found := p.VotingRecord().GetVote(member)\n\tif !found {\n\t\t// Distinguish the two ways a vote can be absent: a member who has\n\t\t// not voted yet is an ordinary state, an outsider is not.\n\t\tif p.Electorate().Has(member) {\n\t\t\tres.Write(md.Paragraph(\"This council member has not voted on this proposal yet.\"))\n\t\t} else {\n\t\t\tres.Write(md.Paragraph(\"This account is not a member of the proposal's electorate.\"))\n\t\t}\n\t\treturn\n\t}\n\n\tres.Write(md.H2(\"Details\"))\n\tres.Write(md.BulletItem(\"User: \" + userLink(v.Address())))\n\tres.Write(md.BulletItem(\"Vote: \" + md.EscapeText(string(v.Choice()))))\n\n\tif v.Reason() != \"\" {\n\t\tres.Write(md.H2(\"Reason\"))\n\t\t// Inline escape for the same reason as a proposal body: a voter's\n\t\t// reason is untrusted text, so its inline links must not render.\n\t\tres.Write(md.Paragraph(md.EscapeText(v.Reason())))\n\t}\n}\n\nfunc mustGetDAOFromRequest(req *mux.Request) *commondao.CommonDAO {\n\trawID := req.GetVar(\"daoID\")\n\tdaoID, err := strconv.ParseUint(rawID, 10, 64)\n\tif err != nil {\n\t\tpanic(\"invalid DAO ID\")\n\t}\n\n\treturn mustGetDAO(daoID)\n}\n\nfunc mustGetProposalFromRequest(req *mux.Request, dao *commondao.CommonDAO) *commondao.Proposal {\n\trawID := req.GetVar(\"proposalID\")\n\tproposalID, err := strconv.ParseUint(rawID, 10, 64)\n\tif err != nil {\n\t\tpanic(\"invalid proposal ID\")\n\t}\n\n\tp := dao.GetProposal(proposalID)\n\tif p == nil {\n\t\tpanic(\"proposal not found\")\n\t}\n\treturn p\n}\n\nfunc parseRealmPath(path string) *realmpath.Request {\n\tr := realmpath.Parse(path)\n\tr.Realm = string(realmLink)\n\treturn r\n}\n\nfunc voteLink(daoID, proposalID uint64) string {\n\treturn md.Link(\"Vote\", realmLink.Call(\n\t\t\"Vote\",\n\t\t\"daoID\", strconv.FormatUint(daoID, 10),\n\t\t\"proposalID\", strconv.FormatUint(proposalID, 10),\n\t\t\"vote\", \"\",\n\t\t\"reason\", \"\",\n\t))\n}\n\nfunc executeLink(daoID, proposalID uint64) string {\n\treturn md.Link(\"Execute\", realmLink.Call(\n\t\t\"Execute\",\n\t\t\"daoID\", strconv.FormatUint(daoID, 10),\n\t\t\"proposalID\", strconv.FormatUint(proposalID, 10),\n\t))\n}\n\nfunc textProposalLink(daoID uint64) string {\n\treturn ufmt.Sprintf(\"[General Proposal](%s)\", realmLink.Call(\n\t\t\"CreateTextProposal\",\n\t\t\"daoID\", strconv.FormatUint(daoID, 10),\n\t\t\"title\", \"\",\n\t\t\"body\", \"\",\n\t\t\"votingDays\", \"7\",\n\t))\n}\n\nfunc updateCouncilLink(daoID uint64) string {\n\treturn md.Link(\"Update Council\", realmLink.Call(\n\t\t\"CreateCouncilUpdateProposal\",\n\t\t\"daoID\", strconv.FormatUint(daoID, 10),\n\t\t\"newMembers\", \"\",\n\t\t\"removeMembers\", \"\",\n\t))\n}\n\nfunc ancestorCouncilUpdateLink(daoID uint64) string {\n\treturn md.Link(\"Ancestor Council Update\", realmLink.Call(\n\t\t\"CreateAncestorCouncilUpdateProposal\",\n\t\t\"daoID\", strconv.FormatUint(daoID, 10),\n\t\t\"targetID\", \"\",\n\t\t\"newMembers\", \"\",\n\t\t\"removeMembers\", \"\",\n\t))\n}\n\nfunc newSubDAOLink(daoID uint64) string {\n\treturn md.Link(\"New SubDAO\", realmLink.Call(\n\t\t\"CreateSubDAOProposal\",\n\t\t\"daoID\", strconv.FormatUint(daoID, 10),\n\t\t\"name\", \"\",\n\t\t\"purpose\", \"\",\n\t\t\"description\", \"\",\n\t\t\"members\", \"\",\n\t))\n}\n\nfunc dissolveSubDAOLink(daoID uint64) string {\n\treturn md.Link(\"Dissolve DAO\", realmLink.Call(\n\t\t\"CreateDissolutionProposal\",\n\t\t\"daoID\", strconv.FormatUint(daoID, 10),\n\t\t\"destination\", \"\",\n\t))\n}\n\nfunc treasuryClawbackLink(daoID uint64) string {\n\treturn md.Link(\"Treasury Clawback\", realmLink.Call(\n\t\t\"CreateTreasuryClawbackProposal\",\n\t\t\"daoID\", strconv.FormatUint(daoID, 10),\n\t\t\"targetID\", \"\",\n\t))\n}\n\nfunc treasuryFreezeLink(daoID uint64) string {\n\treturn md.Link(\"Treasury Freeze\", realmLink.Call(\n\t\t\"CreateTreasuryFreezeProposal\",\n\t\t\"daoID\", strconv.FormatUint(daoID, 10),\n\t\t\"targetID\", \"\",\n\t\t\"frozen\", \"true\",\n\t))\n}\n\nfunc treasurySpendLink(daoID uint64) string {\n\treturn md.Link(\"Treasury Spend\", realmLink.Call(\n\t\t\"CreateTreasurySpendProposal\",\n\t\t\"daoID\", strconv.FormatUint(daoID, 10),\n\t\t\"to\", \"\",\n\t\t\"denom\", \"ugnot\",\n\t\t\"amount\", \"\",\n\t))\n}\n\nfunc registerKindLink(daoID uint64) string {\n\treturn md.Link(\"Register Proposal Kind\", realmLink.Call(\n\t\t\"CreateRegisterKindProposal\",\n\t\t\"daoID\", strconv.FormatUint(daoID, 10),\n\t\t\"kindName\", \"\",\n\t))\n}\n\nfunc deregisterKindLink(daoID uint64) string {\n\treturn md.Link(\"Deregister Proposal Kind\", realmLink.Call(\n\t\t\"CreateDeregisterKindProposal\",\n\t\t\"daoID\", strconv.FormatUint(daoID, 10),\n\t\t\"kindName\", \"\",\n\t))\n}\n\nfunc amendBylawsLink(daoID uint64) string {\n\treturn md.Link(\"Amend Bylaws\", realmLink.Call(\n\t\t\"CreateAmendBylawsProposal\",\n\t\t\"daoID\", strconv.FormatUint(daoID, 10),\n\t\t\"payload\", \"\",\n\t))\n}\n\nfunc goToDAOLink(daoID uint64) string {\n\treturn md.Link(\"Go to DAO\", daoURL(daoID))\n}\n\nfunc goToProposalLink(daoID, proposalID uint64) string {\n\treturn md.Link(\"Go to Proposal\", proposalURL(daoID, proposalID))\n}\n\nfunc userLink(addr address) string {\n\tuser := users.ResolveAddress(addr)\n\tif user != nil {\n\t\treturn user.RenderLink(\"\")\n\t}\n\treturn addr.String()\n}\n\nfunc isVotingPeriodActive(p *commondao.Proposal) bool {\n\treturn p.Status() == commondao.StatusActive \u0026\u0026 time.Now().Before(p.VotingDeadline())\n}\n\nfunc isExecutionAllowed(p *commondao.Proposal) bool {\n\t// Early passed proposals can be executed right away; active proposals\n\t// can be finalized once their voting deadline passes.\n\tif p.Status() == commondao.StatusPassed {\n\t\treturn true\n\t}\n\treturn p.Status() == commondao.StatusActive \u0026\u0026 !time.Now().Before(p.VotingDeadline())\n}\n"},{"name":"uri.gno","body":"package commondao\n\nimport (\n\t\"chain/runtime\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nfunc currentRealmPath() string {\n\treturn strings.TrimPrefix(string(realmLink), runtime.ChainDomain())\n}\n\nfunc daoURL(daoID uint64) string {\n\tpath := currentRealmPath()\n\treturn ufmt.Sprintf(\"%s:%d\", path, daoID)\n}\n\nfunc settingsURL(daoID uint64) string {\n\tpath := currentRealmPath()\n\treturn ufmt.Sprintf(\"%s:%d/settings\", path, daoID)\n}\n\nfunc daoProposalsURL(daoID uint64) string {\n\tpath := currentRealmPath()\n\treturn ufmt.Sprintf(\"%s:%d/proposals\", path, daoID)\n}\n\nfunc bylawsURL(daoID uint64) string {\n\tpath := currentRealmPath()\n\treturn ufmt.Sprintf(\"%s:%d/bylaws\", path, daoID)\n}\n\nfunc proposalURL(daoID, proposalID uint64) string {\n\tpath := currentRealmPath()\n\treturn ufmt.Sprintf(\"%s:%d/proposals/%d\", path, daoID, proposalID)\n}\n\nfunc voteURL(daoID, proposalID uint64, addr address) string {\n\tpath := currentRealmPath()\n\treturn ufmt.Sprintf(\"%s:%d/proposals/%d/vote/%s\", path, daoID, proposalID, addr)\n}\n"},{"name":"z_10_a_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_10_a_filetest\n\npackage z_10_a_filetest\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"*Pwn* [link](https://evil.example) # H1\", \"Purpose\", \"\", \"\")\n}\n\nfunc main() {\n\t// DAO names are user controlled: markdown metacharacters must\n\t// render escaped, never as markup\n\tprintln(commondao.Render(strconv.FormatUint(daoID, 10)))\n}\n\n// Output:\n// # \\*Pwn\\* \\[link\\]\\(https://evil\\.example\\) \\# H1\n// **Purpose:** Purpose\n//\n// [View Proposals](/r/nt/commondao/v0:2/proposals) • [View Settings](/r/nt/commondao/v0:2/settings)\n//\n// ---\n// | Council |\n// | --- |\n// | g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 |\n//\n//\n// ## Treasury\n// - Address: g1vpah5sslxspfdvj9vk07rukek327dzmtxqkmk2\n// - Balance: (empty)\n// ## Create Proposal\n// These are the proposal supported by this DAO:\n//\n// \u003cgno-columns\u003e\n// [General Proposal](/r/nt/commondao/v0$help\u0026func=CreateTextProposal\u0026body=\u0026daoID=2\u0026title=\u0026votingDays=7)\n//\n// This type of proposal is also known as text proposal which can be used for example to get consensus on initiatives without actually making any change on-chain.\n//\n//\n// \u003cgno-columns-sep\u003e\n// [Update Council](/r/nt/commondao/v0$help\u0026func=CreateCouncilUpdateProposal\u0026daoID=2\u0026newMembers=\u0026removeMembers=)\n//\n// This type of proposal can be used to add new council members to this DAO and also to remove existing ones.\n//\n// A single proposal allows new council members to be added and any number of existing ones removed within the same proposal.\n//\n//\n// \u003cgno-columns-sep\u003e\n// [Ancestor Council Update](/r/nt/commondao/v0$help\u0026func=CreateAncestorCouncilUpdateProposal\u0026daoID=2\u0026newMembers=\u0026removeMembers=\u0026targetID=)\n//\n// This type of proposal lets this DAO, as an ancestor, add or remove council members of one of its descendant DAOs — the rescue path for a descendant whose council is stuck or empty.\n//\n//\n// \u003c/gno-columns\u003e\n// \u003cgno-columns\u003e\n// [Dissolve DAO](/r/nt/commondao/v0$help\u0026func=CreateDissolutionProposal\u0026daoID=2\u0026destination=)\n//\n// This type of proposal can be used to dissolve DAOs and SubDAOs.\n//\n// Dissolving a DAO can't be undone, once the dissolution proposal passes and is executed DAO will be readonly.\n//\n//\n// \u003cgno-columns-sep\u003e\n// [Treasury Spend](/r/nt/commondao/v0$help\u0026func=CreateTreasurySpendProposal\u0026amount=\u0026daoID=2\u0026denom=ugnot\u0026to=)\n//\n// This type of proposal sends coins from the DAO's own treasury when it passes and is executed.\n//\n//\n// \u003cgno-columns-sep\u003e\n// [Treasury Clawback](/r/nt/commondao/v0$help\u0026func=CreateTreasuryClawbackProposal\u0026daoID=2\u0026targetID=)\n//\n// This type of proposal sweeps a descendant DAO's treasury to the descendant's parent DAO.\n//\n//\n// \u003c/gno-columns\u003e\n// \u003cgno-columns\u003e\n// [Treasury Freeze](/r/nt/commondao/v0$help\u0026func=CreateTreasuryFreezeProposal\u0026daoID=2\u0026frozen=true\u0026targetID=)\n//\n// This type of proposal freezes or unfreezes a descendant DAO's treasury. While frozen, no funds can leave the treasury.\n//\n//\n// \u003cgno-columns-sep\u003e\n// [New SubDAO](/r/nt/commondao/v0$help\u0026func=CreateSubDAOProposal\u0026daoID=2\u0026description=\u0026members=\u0026name=\u0026purpose=)\n//\n// This type of proposal is used to create SubDAOs, which are used to create tree based DAOs.\n//\n//\n// \u003cgno-columns-sep\u003e\n// [Register Proposal Kind](/r/nt/commondao/v0$help\u0026func=CreateRegisterKindProposal\u0026daoID=2\u0026kindName=) • [Deregister Proposal Kind](/r/nt/commondao/v0$help\u0026func=CreateDeregisterKindProposal\u0026daoID=2\u0026kindName=)\n//\n// This type of proposal registers one of the realm's catalog proposal kinds on this DAO by name, or deregisters a kind. Deregistering a kind blocks new proposals of that kind while in-flight ones still vote and execute. The manage-kinds kind itself cannot be deregistered.\n//\n//\n// \u003c/gno-columns\u003e\n// \u003cgno-columns\u003e\n// [Amend Bylaws](/r/nt/commondao/v0$help\u0026func=CreateAmendBylawsProposal\u0026daoID=2\u0026payload=)\n//\n// This type of proposal adds, amends or removes one of the DAO's bylaws documents with a diff patch pinned to the current document text (the mandates folder is reserved: mandates are set from above, not by the council). Build the payload with the AmendBylawsPayload query function.\n//\n//\n// \u003c/gno-columns\u003e\n"},{"name":"z_10_b_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_10_b_filetest\n\npackage z_10_b_filetest\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\tdaoID        uint64\n\tproposalPath string\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n\n\tpID := commondao.CreateTextProposal(cross(cur), daoID, \"*Title* [t](https://evil.example)\", \"**Body** with \u003cimg src=x\u003e and [b](https://evil.example)\", 0)\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"*reason* [r](https://evil.example)\")\n\tproposalPath = strconv.FormatUint(daoID, 10) + \"/proposals/\" + strconv.FormatUint(pID, 10)\n}\n\nfunc main() {\n\t// Proposal titles, bodies and vote reasons are user controlled:\n\t// markdown metacharacters must render escaped, never as markup\n\tprintln(commondao.Render(proposalPath))\n\tprintln(commondao.Render(proposalPath + \"/vote/\" + string(user)))\n}\n\n// Output:\n// # #1 \\*Title\\* \\[t\\]\\(https://evil\\.example\\)\n// [Go to DAO](/r/nt/commondao/v0:2) • [Execute](/r/nt/commondao/v0$help\u0026func=Execute\u0026daoID=2\u0026proposalID=1)\n//\n// ---\n// ## Details\n// - Proposer: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n// - Submit Time: Fri, 13 Feb 2009 23:31:30 UTC\n// - Status: **passed**\n// ## Description\n// \\*\\*Body\\*\\* with \\\u003cimg src=x\\\u003e and \\[b\\]\\(https://evil\\.example\\)\n//\n// ## Stats\n// | Vote Choices | Percentage of Votes |\n// | --- | --- |\n// | YES | 100.00% |\n//\n//\n// ## Votes\n// Total number of votes: **1**\n// | Users | Votes |\n// | --- | --- |\n// | g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 | [YES](/r/nt/commondao/v0:2/proposals/1/vote/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5) with a reason |\n//\n//\n//\n// # Vote: Proposal #1\n// [Go to DAO](/r/nt/commondao/v0:2) • [Go to Proposal](/r/nt/commondao/v0:2/proposals/1)\n//\n// ---\n// ## Details\n// - User: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n// - Vote: YES\n// ## Reason\n// \\*reason\\* \\[r\\]\\(https://evil\\.example\\)\n"},{"name":"z_10_c_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_10_c_filetest\n\npackage z_10_c_filetest\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar proposalPath string\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID := commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n\n\t// The SubDAO name is user controlled; the proposal body is pre-built\n\t// markdown chrome that must render formatted, with the name escaped.\n\tpID := commondao.CreateSubDAOProposal(cross(cur), daoID, \"*Evil* [x](https://evil.example)\", \"Purpose\", \"\", string(owner))\n\tproposalPath = strconv.FormatUint(daoID, 10) + \"/proposals/\" + strconv.FormatUint(pID, 10)\n}\n\nfunc main() {\n\tprintln(commondao.Render(proposalPath))\n}\n\n// Output:\n//\n// \u003e Voting ends on **Fri, 20 Feb 2009 11:31pm UTC**\n//\n// # #1 New SubDAO: \\*Evil\\* \\[x\\]\\(https://evil\\.example\\)\n// [Go to DAO](/r/nt/commondao/v0:2) • [Vote](/r/nt/commondao/v0$help\u0026func=Vote\u0026daoID=2\u0026proposalID=1\u0026reason=\u0026vote=)\n//\n// ---\n// ## Details\n// - Proposer: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n// - Submit Time: Fri, 13 Feb 2009 23:31:30 UTC\n// - Expected Outcome: **pending** ⏳\n// - Status: **active**\n// ## Description\n// **Parent DAO:**\n// [Foo](/r/nt/commondao/v0:2)\n//\n// **SubDAO Name:**\n// \\*Evil\\* \\[x\\]\\(https://evil\\.example\\)\n//\n// **SubDAO Purpose:**\n// Purpose\n//\n// **Council Members:**\n// - g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\n"},{"name":"z_10_d_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_10_d_filetest\n\npackage z_10_d_filetest\n\nimport (\n\t\"chain\"\n\t\"strconv\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar subID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\trootID := commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", \"\")\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), rootID, \"Sub\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), rootID, pID)\n\t}\n\tsubID = 3\n\n\ttesting.IssueCoins(commondao.GetView(subID).Address(), chain.NewCoins(chain.NewCoin(\"ugnot\", 1234)))\n\n\tfID := commondao.CreateTreasuryFreezeProposal(cross(cur), rootID, subID, true)\n\tcommondao.Vote(cross(cur), rootID, fID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, fID)\n}\n\nfunc main() {\n\t// The DAO page shows the funded balance and the frozen warning; the\n\t// settings page shows the treasury proposals flag\n\tprintln(commondao.Render(strconv.FormatUint(subID, 10)))\n\tprintln(commondao.Render(strconv.FormatUint(subID, 10) + \"/settings\"))\n}\n\n// Output:\n// # Sub\n// **Purpose:** Purpose\n//\n// [View Proposals](/r/nt/commondao/v0:3/proposals) • [View Settings](/r/nt/commondao/v0:3/settings) • [Go to Parent DAO](/r/nt/commondao/v0:2)\n//\n// ---\n// | Council |\n// | --- |\n// | g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 |\n//\n//\n// ## Treasury\n// - Address: g17pr3grpaag8ketn84dl2y0nncef6u3jw8ca9gl\n// - Balance: 1234ugnot\n//\n// \u003e ⚠ The treasury is frozen: no proposal can move funds until a proper ancestor DAO unfreezes it.\n//\n// ## Create Proposal\n// These are the proposal supported by this DAO:\n//\n// \u003cgno-columns\u003e\n// [General Proposal](/r/nt/commondao/v0$help\u0026func=CreateTextProposal\u0026body=\u0026daoID=3\u0026title=\u0026votingDays=7)\n//\n// This type of proposal is also known as text proposal which can be used for example to get consensus on initiatives without actually making any change on-chain.\n//\n//\n// \u003cgno-columns-sep\u003e\n// [Update Council](/r/nt/commondao/v0$help\u0026func=CreateCouncilUpdateProposal\u0026daoID=3\u0026newMembers=\u0026removeMembers=)\n//\n// This type of proposal can be used to add new council members to this DAO and also to remove existing ones.\n//\n// A single proposal allows new council members to be added and any number of existing ones removed within the same proposal.\n//\n//\n// \u003cgno-columns-sep\u003e\n// [Ancestor Council Update](/r/nt/commondao/v0$help\u0026func=CreateAncestorCouncilUpdateProposal\u0026daoID=3\u0026newMembers=\u0026removeMembers=\u0026targetID=)\n//\n// This type of proposal lets this DAO, as an ancestor, add or remove council members of one of its descendant DAOs — the rescue path for a descendant whose council is stuck or empty.\n//\n//\n// \u003c/gno-columns\u003e\n// \u003cgno-columns\u003e\n// [Dissolve DAO](/r/nt/commondao/v0$help\u0026func=CreateDissolutionProposal\u0026daoID=3\u0026destination=)\n//\n// This type of proposal can be used to dissolve DAOs and SubDAOs.\n//\n// Dissolving a DAO can't be undone, once the dissolution proposal passes and is executed DAO will be readonly.\n//\n//\n// \u003cgno-columns-sep\u003e\n// [Treasury Spend](/r/nt/commondao/v0$help\u0026func=CreateTreasurySpendProposal\u0026amount=\u0026daoID=3\u0026denom=ugnot\u0026to=)\n//\n// This type of proposal sends coins from the DAO's own treasury when it passes and is executed.\n//\n//\n// \u003cgno-columns-sep\u003e\n// [Treasury Clawback](/r/nt/commondao/v0$help\u0026func=CreateTreasuryClawbackProposal\u0026daoID=3\u0026targetID=)\n//\n// This type of proposal sweeps a descendant DAO's treasury to the descendant's parent DAO.\n//\n//\n// \u003c/gno-columns\u003e\n// \u003cgno-columns\u003e\n// [Treasury Freeze](/r/nt/commondao/v0$help\u0026func=CreateTreasuryFreezeProposal\u0026daoID=3\u0026frozen=true\u0026targetID=)\n//\n// This type of proposal freezes or unfreezes a descendant DAO's treasury. While frozen, no funds can leave the treasury.\n//\n//\n// \u003cgno-columns-sep\u003e\n// [New SubDAO](/r/nt/commondao/v0$help\u0026func=CreateSubDAOProposal\u0026daoID=3\u0026description=\u0026members=\u0026name=\u0026purpose=)\n//\n// This type of proposal is used to create SubDAOs, which are used to create tree based DAOs.\n//\n//\n// \u003cgno-columns-sep\u003e\n// [Register Proposal Kind](/r/nt/commondao/v0$help\u0026func=CreateRegisterKindProposal\u0026daoID=3\u0026kindName=) • [Deregister Proposal Kind](/r/nt/commondao/v0$help\u0026func=CreateDeregisterKindProposal\u0026daoID=3\u0026kindName=)\n//\n// This type of proposal registers one of the realm's catalog proposal kinds on this DAO by name, or deregisters a kind. Deregistering a kind blocks new proposals of that kind while in-flight ones still vote and execute. The manage-kinds kind itself cannot be deregistered.\n//\n//\n// \u003c/gno-columns\u003e\n// \u003cgno-columns\u003e\n// [Amend Bylaws](/r/nt/commondao/v0$help\u0026func=CreateAmendBylawsProposal\u0026daoID=3\u0026payload=)\n//\n// This type of proposal adds, amends or removes one of the DAO's bylaws documents with a diff patch pinned to the current document text (the mandates folder is reserved: mandates are set from above, not by the council). Build the payload with the AmendBylawsPayload query function.\n//\n//\n// \u003c/gno-columns\u003e\n//\n// # Sub: Settings\n// [Go to DAO](/r/nt/commondao/v0:3)\n//\n// ---\n// ## Info\n// | Setting | Value |\n// | --- | --- |\n// | Listed | false |\n// | Max active proposals | 32 |\n// | Proposal kinds | amend\\-bylaws, ancestor\\-council\\-update, council\\-update, dissolve, manage\\-kinds, subdao, text, treasury\\-clawback, treasury\\-freeze, treasury\\-spend |\n"},{"name":"z_10_e_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_10_e_filetest\n\npackage z_10_e_filetest\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\tdaoID uint64\n\tpID   uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\t// A 3-member council keeps the proposal active after one vote, so the\n\t// voter can change their choice.\n\tmembers := \"g1cyh9qm4y43w38gfk77qxczrhqj2asdr8l7uk54\\ng1x95kxqpwvqjf98qpkvsay7tppkh6zs9fr7wwda\"\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", members)\n\tpID = commondao.CreateTextProposal(cross(cur), daoID, \"Title\", \"Body\", 30)\n\n\t// Vote YES, then change to NO: the vote stats must show only NO,\n\t// never a stale \"YES | 0.00%\" row from the zeroed counter.\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceNo, \"\")\n}\n\nfunc main() {\n\tprintln(commondao.Render(strconv.FormatUint(daoID, 10) + \"/proposals/\" + strconv.FormatUint(pID, 10)))\n}\n\n// Output:\n//\n// \u003e Voting ends on **Sun, 15 Mar 2009 11:31pm UTC**\n//\n// # #1 Title\n// [Go to DAO](/r/nt/commondao/v0:2) • [Vote](/r/nt/commondao/v0$help\u0026func=Vote\u0026daoID=2\u0026proposalID=1\u0026reason=\u0026vote=)\n//\n// ---\n// ## Details\n// - Proposer: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n// - Submit Time: Fri, 13 Feb 2009 23:31:30 UTC\n// - Expected Outcome: **pending** ⏳\n// - Status: **active**\n// ## Description\n// Body\n//\n// ## Stats\n// | Vote Choices | Percentage of Votes |\n// | --- | --- |\n// | NO | 100.00% |\n//\n//\n// ## Votes\n// Total number of votes: **1**\n// | Users | Votes |\n// | --- | --- |\n// | g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 | [NO](/r/nt/commondao/v0:2/proposals/1/vote/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5) |\n"},{"name":"z_11_a_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_11_a_filetest\n\npackage z_11_a_filetest\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\tdaoID uint64\n\tpID   uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n\n\ttesting.IssueCoins(commondao.GetView(daoID).Address(), chain.NewCoins(chain.NewCoin(\"ugnot\", 1000)))\n\n\tpID = commondao.CreateTreasurySpendProposal(cross(cur), daoID, owner, \"ugnot\", 400)\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tcommondao.Execute(cross(cur), daoID, pID)\n\n\tview := commondao.GetView(daoID)\n\tp, _ := view.GetProposal(pID)\n\tprintln(\"status:\", string(p.Status()))\n\n\tb := banker.NewReadonlyBanker()\n\tprintln(\"treasury:\", b.GetCoins(view.Address()).String())\n\tprintln(\"recipient:\", b.GetCoins(owner).String())\n}\n\n// Output:\n// status: executed\n// treasury: 600ugnot\n// recipient: 400ugnot\n"},{"name":"z_11_c_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_11_c_filetest\n\npackage z_11_c_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n}\n\nfunc main(cur realm) {\n\t// Call as a user which is not a council member\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tcommondao.CreateTreasurySpendProposal(cross(cur), daoID, owner, \"ugnot\", 1)\n}\n\n// Error:\n// caller is not a council member\n"},{"name":"z_11_d_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_11_d_filetest\n\npackage z_11_d_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// The treasury is empty, so validation fails at proposal creation\n\tcommondao.CreateTreasurySpendProposal(cross(cur), daoID, owner, \"ugnot\", 1)\n}\n\n// Error:\n// insufficient treasury balance\n"},{"name":"z_11_e_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_11_e_filetest\n\npackage z_11_e_filetest\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\trootID  uint64\n\tsubID   uint64\n\tspendID uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\trootID = commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", \"\")\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), rootID, \"Sub\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), rootID, pID)\n\t}\n\tsubID = 3\n\n\ttesting.IssueCoins(commondao.GetView(subID).Address(), chain.NewCoins(chain.NewCoin(\"ugnot\", 500)))\n\n\t// Pass a spend on the sub-DAO but do not execute it yet\n\tspendID = commondao.CreateTreasurySpendProposal(cross(cur), subID, owner, \"ugnot\", 500)\n\tcommondao.Vote(cross(cur), subID, spendID, pdao.ChoiceYes, \"\")\n\n\t// The ancestor freezes the sub-DAO treasury before the spend executes\n\tfID := commondao.CreateTreasuryFreezeProposal(cross(cur), rootID, subID, true)\n\tcommondao.Vote(cross(cur), rootID, fID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, fID)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// The standing passed spend fails cleanly once the treasury is frozen\n\tcommondao.Execute(cross(cur), subID, spendID)\n\n\tview := commondao.GetView(subID)\n\tp, _ := view.GetProposal(spendID)\n\tprintln(\"status:\", string(p.Status()))\n\tprintln(\"reason:\", p.StatusReason())\n\tprintln(\"frozen:\", view.IsTreasuryFrozen())\n\tprintln(\"treasury:\", banker.NewReadonlyBanker().GetCoins(view.Address()).String())\n}\n\n// Output:\n// status: failed\n// reason: DAO treasury is frozen\n// frozen: true\n// treasury: 500ugnot\n"},{"name":"z_11_f_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_11_f_filetest\n\npackage z_11_f_filetest\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\tdaoID uint64\n\tp1    uint64\n\tp2    uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n\n\ttesting.IssueCoins(commondao.GetView(daoID).Address(), chain.NewCoins(chain.NewCoin(\"ugnot\", 1000)))\n\n\t// Two passed spends over-commit the 1000ugnot balance\n\tp1 = commondao.CreateTreasurySpendProposal(cross(cur), daoID, owner, \"ugnot\", 800)\n\tcommondao.Vote(cross(cur), daoID, p1, pdao.ChoiceYes, \"\")\n\tp2 = commondao.CreateTreasurySpendProposal(cross(cur), daoID, owner, \"ugnot\", 800)\n\tcommondao.Vote(cross(cur), daoID, p2, pdao.ChoiceYes, \"\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tcommondao.Execute(cross(cur), daoID, p1)\n\tcommondao.Execute(cross(cur), daoID, p2)\n\n\tview := commondao.GetView(daoID)\n\tv1, _ := view.GetProposal(p1)\n\tv2, _ := view.GetProposal(p2)\n\tprintln(\"first:\", string(v1.Status()))\n\tprintln(\"second:\", string(v2.Status()), \"-\", v2.StatusReason())\n\tprintln(\"treasury:\", banker.NewReadonlyBanker().GetCoins(view.Address()).String())\n}\n\n// Output:\n// first: executed\n// second: failed - insufficient treasury balance\n// treasury: 200ugnot\n"},{"name":"z_11_g_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_11_g_filetest\n\npackage z_11_g_filetest\n\nimport (\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\trootID uint64\n\tsubID  uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\trootID = commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", \"\")\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), rootID, \"Sub\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), rootID, pID)\n\t}\n\tsubID = 3\n\n\tfID := commondao.CreateTreasuryFreezeProposal(cross(cur), rootID, subID, true)\n\tcommondao.Vote(cross(cur), rootID, fID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, fID)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tcommondao.CreateTreasurySpendProposal(cross(cur), subID, owner, \"ugnot\", 1)\n}\n\n// Error:\n// DAO treasury is frozen\n"},{"name":"z_11_h_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_11_h_filetest\n\npackage z_11_h_filetest\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"strings\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\n// A treasury spend requires a Supermajority (\u003e= 2/3), not a simple\n// majority. A 5-member council splitting 3 YES / 2 NO clears a simple\n// majority (2*3 \u003e 5) but NOT a supermajority (3*3 \u003c 2*5), so at the\n// deadline the spend is dismissed and the treasury is untouched. This\n// pins the spend threshold behaviourally: flipping it to SimpleMajority\n// would let this same vote pass and spend the funds.\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n\tmoul  = address(\"g1manfred47kzduec920z88wfr64ylksmdcedlf5\") // @moul\n\tmF    = address(\"g1cyh9qm4y43w38gfk77qxczrhqj2asdr8l7uk54\")\n\tmG    = address(\"g1x95kxqpwvqjf98qpkvsay7tppkh6zs9fr7wwda\")\n)\n\nvar (\n\tdaoID uint64\n\tpID   uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\t// A 5-member council (caller + 4 additional members).\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tmembers := strings.Join([]string{owner.String(), moul.String(), mF.String(), mG.String()}, \"\\n\")\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", members)\n\n\ttesting.IssueCoins(commondao.GetView(daoID).Address(), chain.NewCoins(chain.NewCoin(\"ugnot\", 1000)))\n\n\tpID = commondao.CreateTreasurySpendProposal(cross(cur), daoID, owner, \"ugnot\", 400)\n\n\t// 3 YES / 2 NO from distinct council members. NO votes first so a\n\t// (would-be) simple-majority pass under a mutated threshold lands on\n\t// the last vote, never on an already-decided proposal.\n\ttesting.SetRealm(testing.NewUserRealm(mF))\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceNo, \"\")\n\ttesting.SetRealm(testing.NewUserRealm(mG))\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceNo, \"\")\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\ttesting.SetRealm(testing.NewUserRealm(moul))\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\ttesting.SkipHeights(121000) // 5s blocks: pass the 7 day voting deadline\n\n\tcommondao.Execute(cross(cur), daoID, pID)\n\n\tview := commondao.GetView(daoID)\n\tp, _ := view.GetProposal(pID)\n\tprintln(\"status:\", string(p.Status()))\n\tprintln(\"treasury:\", banker.NewReadonlyBanker().GetCoins(view.Address()).String())\n}\n\n// Output:\n// status: dismissed\n// treasury: 1000ugnot\n"},{"name":"z_12_a_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_12_a_filetest\n\npackage z_12_a_filetest\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\trootID uint64\n\tsubID  uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\trootID = commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", \"\")\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), rootID, \"Sub\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), rootID, pID)\n\t}\n\tsubID = 3\n\n\ttesting.IssueCoins(commondao.GetView(subID).Address(), chain.NewCoins(chain.NewCoin(\"ugnot\", 750)))\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// The ancestor claws back the sub-DAO treasury at simple majority\n\tpID := commondao.CreateTreasuryClawbackProposal(cross(cur), rootID, subID)\n\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, pID)\n\n\tb := banker.NewReadonlyBanker()\n\tprintln(\"sub treasury:\", b.GetCoins(commondao.GetView(subID).Address()).String())\n\tprintln(\"root treasury:\", b.GetCoins(commondao.GetView(rootID).Address()).String())\n\n\t// Clawing back an already-empty treasury executes as a no-op\n\tpID = commondao.CreateTreasuryClawbackProposal(cross(cur), rootID, subID)\n\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, pID)\n\tp, _ := commondao.GetView(rootID).GetProposal(pID)\n\tprintln(\"empty clawback:\", string(p.Status()))\n\tprintln(\"root treasury:\", b.GetCoins(commondao.GetView(rootID).Address()).String())\n}\n\n// Output:\n// sub treasury:\n// root treasury: 750ugnot\n// empty clawback: executed\n// root treasury: 750ugnot\n"},{"name":"z_12_b_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_12_b_filetest\n\npackage z_12_b_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// A DAO can never claw back its own treasury\n\tcommondao.CreateTreasuryClawbackProposal(cross(cur), daoID, daoID)\n}\n\n// Error:\n// a DAO cannot target itself\n"},{"name":"z_12_c_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_12_c_filetest\n\npackage z_12_c_filetest\n\nimport (\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\tsubA uint64\n\tsubB uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\trootID := commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", \"\")\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), rootID, \"A\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), rootID, pID)\n\t}\n\tsubA = 3\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), rootID, \"B\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), rootID, pID)\n\t}\n\tsubB = 4\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Siblings have no authority over each other\n\tcommondao.CreateTreasuryClawbackProposal(cross(cur), subA, subB)\n}\n\n// Error:\n// DAO is not an ancestor of the target DAO\n"},{"name":"z_12_d_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_12_d_filetest\n\npackage z_12_d_filetest\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\trootID uint64\n\tsubID  uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\trootID = commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", \"\")\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), rootID, \"Sub\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), rootID, pID)\n\t}\n\tsubID = 3\n\n\t// Freeze the sub-DAO, then dissolve it (the dissolution sweep runs on\n\t// an empty treasury), then fund its address afterwards: coins landing\n\t// on a dead DAO remain rescuable by an ancestor clawback.\n\tfID := commondao.CreateTreasuryFreezeProposal(cross(cur), rootID, subID, true)\n\tcommondao.Vote(cross(cur), rootID, fID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, fID)\n\n\tdID := commondao.CreateDissolutionProposal(cross(cur), subID, \"\")\n\tcommondao.Vote(cross(cur), rootID, dID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, dID)\n\n\ttesting.IssueCoins(commondao.GetView(subID).Address(), chain.NewCoins(chain.NewCoin(\"ugnot\", 300)))\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tpID := commondao.CreateTreasuryClawbackProposal(cross(cur), rootID, subID)\n\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, pID)\n\n\tview := commondao.GetView(subID)\n\tprintln(\"deleted:\", view.IsDeleted())\n\tprintln(\"frozen:\", view.IsTreasuryFrozen())\n\tb := banker.NewReadonlyBanker()\n\tprintln(\"sub treasury:\", b.GetCoins(view.Address()).String())\n\tprintln(\"root treasury:\", b.GetCoins(commondao.GetView(rootID).Address()).String())\n}\n\n// Output:\n// deleted: true\n// frozen: true\n// sub treasury:\n// root treasury: 300ugnot\n"},{"name":"z_12_e_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_12_e_filetest\n\npackage z_12_e_filetest\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\trootID uint64\n\tmidID  uint64\n\tleafID uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\trootID = commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", \"\")\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), rootID, \"Mid\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), rootID, pID)\n\t}\n\tmidID = 3\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), midID, \"Leaf\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), midID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), midID, pID)\n\t}\n\tleafID = 4\n\n\ttesting.IssueCoins(commondao.GetView(leafID).Address(), chain.NewCoins(chain.NewCoin(\"ugnot\", 420)))\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// The grandparent proposes; swept funds must land on the target's\n\t// parent (one step up the tree), never on the proposing DAO\n\tpID := commondao.CreateTreasuryClawbackProposal(cross(cur), rootID, leafID)\n\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, pID)\n\n\tb := banker.NewReadonlyBanker()\n\tprintln(\"leaf treasury:\", b.GetCoins(commondao.GetView(leafID).Address()).String())\n\tprintln(\"mid treasury:\", b.GetCoins(commondao.GetView(midID).Address()).String())\n\tprintln(\"root treasury:\", b.GetCoins(commondao.GetView(rootID).Address()).String())\n}\n\n// Output:\n// leaf treasury:\n// mid treasury: 420ugnot\n// root treasury:\n"},{"name":"z_12_f_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_12_f_filetest\n\npackage z_12_f_filetest\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"strings\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\n// Treasury clawback passes by SIMPLE majority. A 5-member ancestor council\n// splitting 3 YES / 2 NO clears a simple majority (2*3 \u003e 5) but not a\n// supermajority (3*3 \u003c 2*5), so under the correct Simple threshold the\n// clawback executes and empties the target; a mutated Super threshold would\n// dismiss it, leaving the funds.\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n\tmoul  = address(\"g1manfred47kzduec920z88wfr64ylksmdcedlf5\") // @moul\n\tmF    = address(\"g1cyh9qm4y43w38gfk77qxczrhqj2asdr8l7uk54\")\n\tmG    = address(\"g1x95kxqpwvqjf98qpkvsay7tppkh6zs9fr7wwda\")\n)\n\nvar (\n\trootID uint64\n\tsubID  uint64\n\tpID    uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tmembers := strings.Join([]string{owner.String(), moul.String(), mF.String(), mG.String()}, \"\\n\")\n\trootID = commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", members)\n\n\t// Create a funded sub-DAO of root (sub creation itself is simple\n\t// majority: 3 of 5 YES passes).\n\tsp := commondao.CreateSubDAOProposal(cross(cur), rootID, \"Sub\", \"Purpose\", \"\", string(user))\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tcommondao.Vote(cross(cur), rootID, sp, pdao.ChoiceYes, \"\")\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Vote(cross(cur), rootID, sp, pdao.ChoiceYes, \"\")\n\ttesting.SetRealm(testing.NewUserRealm(moul))\n\tcommondao.Vote(cross(cur), rootID, sp, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, sp)\n\tsubID = 3\n\ttesting.IssueCoins(commondao.GetView(subID).Address(), chain.NewCoins(chain.NewCoin(\"ugnot\", 500)))\n\n\t// Clawback proposal, hosted in the ancestor (root), 3 YES / 2 NO.\n\tpID = commondao.CreateTreasuryClawbackProposal(cross(cur), rootID, subID)\n\ttesting.SetRealm(testing.NewUserRealm(mF))\n\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceNo, \"\")\n\ttesting.SetRealm(testing.NewUserRealm(mG))\n\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceNo, \"\")\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\ttesting.SetRealm(testing.NewUserRealm(moul))\n\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\ttesting.SkipHeights(121000)\n\n\tcommondao.Execute(cross(cur), rootID, pID)\n\n\tp, _ := commondao.GetView(rootID).GetProposal(pID)\n\tprintln(\"status:\", string(p.Status()))\n\tprintln(\"sub treasury:\", banker.NewReadonlyBanker().GetCoins(commondao.GetView(subID).Address()).String())\n}\n\n// Output:\n// status: executed\n// sub treasury:\n"},{"name":"z_12_g_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_12_g_filetest\n\npackage z_12_g_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tcommondao.CreateTreasuryClawbackProposal(cross(cur), daoID, 404)\n}\n\n// Error:\n// DAO not found\n"},{"name":"z_12_h_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_12_h_filetest\n\npackage z_12_h_filetest\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\trootID  uint64\n\tsubID   uint64\n\tspendID uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\trootID = commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", \"\")\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), rootID, \"Sub\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), rootID, pID)\n\t}\n\tsubID = 3\n\n\ttesting.IssueCoins(commondao.GetView(subID).Address(), chain.NewCoins(chain.NewCoin(\"ugnot\", 500)))\n\n\t// Pass a spend on the sub-DAO but do not execute it yet\n\tspendID = commondao.CreateTreasurySpendProposal(cross(cur), subID, owner, \"ugnot\", 500)\n\tcommondao.Vote(cross(cur), subID, spendID, pdao.ChoiceYes, \"\")\n\n\t// The parent claws back the full balance before the spend executes\n\tcID := commondao.CreateTreasuryClawbackProposal(cross(cur), rootID, subID)\n\tcommondao.Vote(cross(cur), rootID, cID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, cID)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// The standing passed spend fails cleanly on the drained treasury\n\tcommondao.Execute(cross(cur), subID, spendID)\n\n\tp, _ := commondao.GetView(subID).GetProposal(spendID)\n\tprintln(\"status:\", string(p.Status()))\n\tprintln(\"reason:\", p.StatusReason())\n\tb := banker.NewReadonlyBanker()\n\tprintln(\"sub treasury:\", b.GetCoins(commondao.GetView(subID).Address()).String())\n\tprintln(\"root treasury:\", b.GetCoins(commondao.GetView(rootID).Address()).String())\n\tprintln(\"recipient:\", b.GetCoins(owner).String())\n}\n\n// Output:\n// status: failed\n// reason: insufficient treasury balance\n// sub treasury:\n// root treasury: 500ugnot\n// recipient:\n"},{"name":"z_12_i_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_12_i_filetest\n\npackage z_12_i_filetest\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\trootID uint64\n\tmidID  uint64\n\tleafID uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\trootID = commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", \"\")\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), rootID, \"Mid\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), rootID, pID)\n\t}\n\tmidID = 3\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), midID, \"Leaf\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), midID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), midID, pID)\n\t}\n\tleafID = 4\n\n\ttesting.IssueCoins(commondao.GetView(leafID).Address(), chain.NewCoins(chain.NewCoin(\"ugnot\", 900)))\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Root and mid both pass clawbacks against the same leaf\n\trID := commondao.CreateTreasuryClawbackProposal(cross(cur), rootID, leafID)\n\tcommondao.Vote(cross(cur), rootID, rID, pdao.ChoiceYes, \"\")\n\tmID := commondao.CreateTreasuryClawbackProposal(cross(cur), midID, leafID)\n\tcommondao.Vote(cross(cur), midID, mID, pdao.ChoiceYes, \"\")\n\n\t// Funds move exactly once, to the leaf's parent; the second clawback\n\t// executes as an empty no-op and does not hoist mid's new balance\n\tcommondao.Execute(cross(cur), rootID, rID)\n\tcommondao.Execute(cross(cur), midID, mID)\n\n\trp, _ := commondao.GetView(rootID).GetProposal(rID)\n\tmp, _ := commondao.GetView(midID).GetProposal(mID)\n\tprintln(\"root clawback:\", string(rp.Status()))\n\tprintln(\"mid clawback:\", string(mp.Status()))\n\tb := banker.NewReadonlyBanker()\n\tprintln(\"leaf treasury:\", b.GetCoins(commondao.GetView(leafID).Address()).String())\n\tprintln(\"mid treasury:\", b.GetCoins(commondao.GetView(midID).Address()).String())\n\tprintln(\"root treasury:\", b.GetCoins(commondao.GetView(rootID).Address()).String())\n}\n\n// Output:\n// root clawback: executed\n// mid clawback: executed\n// leaf treasury:\n// mid treasury: 900ugnot\n// root treasury:\n"},{"name":"z_12_j_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_12_j_filetest\n\npackage z_12_j_filetest\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\trootID uint64\n\tsubID  uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\trootID = commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", \"\")\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), rootID, \"Sub\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), rootID, pID)\n\t}\n\tsubID = 3\n\n\ttesting.IssueCoins(commondao.GetView(subID).Address(), chain.NewCoins(\n\t\tchain.NewCoin(\"ugnot\", 100),\n\t\tchain.NewCoin(\"zzfoo\", 50),\n\t))\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Clawback sweeps the full multi-denomination balance\n\tpID := commondao.CreateTreasuryClawbackProposal(cross(cur), rootID, subID)\n\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, pID)\n\n\tb := banker.NewReadonlyBanker()\n\tprintln(\"sub treasury:\", b.GetCoins(commondao.GetView(subID).Address()).String())\n\tprintln(\"root treasury:\", b.GetCoins(commondao.GetView(rootID).Address()).String())\n}\n\n// Output:\n// sub treasury:\n// root treasury: 100ugnot,50zzfoo\n"},{"name":"z_12_k_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_12_k_filetest\n\n// Only a council member may propose an ancestor clawback.\n//\n// The ancestry check authorizes which treasury may be swept; it says\n// nothing about who may ask. Without this gate a stranger could fill a\n// DAO's active-proposal cap with clawback items it never chose to\n// consider.\npackage z_12_k_filetest\n\nimport (\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig, genesis council only\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1, owns the tree below\n)\n\nvar (\n\trootID uint64\n\tsubID  uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\trootID = commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", \"\")\n\n\tpID := commondao.CreateSubDAOProposal(cross(cur), rootID, \"Sub\", \"Purpose\", \"\", string(user))\n\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, pID)\n\tsubID = 3\n}\n\nfunc main(cur realm) {\n\t// A valid ancestry (root over sub) proposed by a non-member.\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tcommondao.CreateTreasuryClawbackProposal(cross(cur), rootID, subID)\n}\n\n// Error:\n// caller is not a council member\n"},{"name":"z_13_a_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_13_a_filetest\n\npackage z_13_a_filetest\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\trootID uint64\n\tsubID  uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\trootID = commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", \"\")\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), rootID, \"Sub\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), rootID, pID)\n\t}\n\tsubID = 3\n\n\ttesting.IssueCoins(commondao.GetView(subID).Address(), chain.NewCoins(chain.NewCoin(\"ugnot\", 900)))\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tpID := commondao.CreateDissolutionProposal(cross(cur), subID, \"\")\n\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, pID)\n\n\tview := commondao.GetView(subID)\n\tprintln(\"deleted:\", view.IsDeleted())\n\tb := banker.NewReadonlyBanker()\n\tprintln(\"sub treasury:\", b.GetCoins(view.Address()).String())\n\tprintln(\"root treasury:\", b.GetCoins(commondao.GetView(rootID).Address()).String())\n}\n\n// Output:\n// deleted: true\n// sub treasury:\n// root treasury: 900ugnot\n"},{"name":"z_13_b_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_13_b_filetest\n\npackage z_13_b_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tcommondao.CreateDissolutionProposal(cross(cur), daoID, \"\")\n}\n\n// Error:\n// root DAO dissolution requires a valid sweep destination\n"},{"name":"z_13_c_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_13_c_filetest\n\npackage z_13_c_filetest\n\nimport (\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\trootID uint64\n\tsubID  uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\trootID = commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", \"\")\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), rootID, \"Sub\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), rootID, pID)\n\t}\n\tsubID = 3\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tcommondao.CreateDissolutionProposal(cross(cur), subID, owner)\n}\n\n// Error:\n// sub-DAO dissolution sweeps to the parent DAO; destination must be empty\n"},{"name":"z_13_d_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_13_d_filetest\n\npackage z_13_d_filetest\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\trootID uint64\n\tmidID  uint64\n\tleafID uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\trootID = commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", \"\")\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), rootID, \"Mid\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), rootID, pID)\n\t}\n\tmidID = 3\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), midID, \"Leaf\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), midID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), midID, pID)\n\t}\n\tleafID = 4\n\n\ttesting.IssueCoins(commondao.GetView(leafID).Address(), chain.NewCoins(chain.NewCoin(\"ugnot\", 150)))\n\n\t// Dissolve the middle DAO first, orphaning the leaf\n\tpID := commondao.CreateDissolutionProposal(cross(cur), midID, \"\")\n\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, pID)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// The leaf's dissolution proposal is hosted in the nearest live\n\t// ancestor (the root), because its parent is already dissolved\n\tpID := commondao.CreateDissolutionProposal(cross(cur), leafID, \"\")\n\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, pID)\n\n\tprintln(\"mid deleted:\", commondao.GetView(midID).IsDeleted())\n\tprintln(\"leaf deleted:\", commondao.GetView(leafID).IsDeleted())\n\n\t// The orphan's sweep skips the dissolved parent and lands on the\n\t// nearest LIVE ancestor. Sweeping onto the dead mid would leave the\n\t// funds usable only through a further clawback — and lost outright\n\t// once the root is dissolved too, since a deleted DAO cannot be\n\t// clawed back.\n\tb := banker.NewReadonlyBanker()\n\tprintln(\"leaf treasury:\", b.GetCoins(commondao.GetView(leafID).Address()).String())\n\tprintln(\"mid treasury (dissolved, skipped):\", b.GetCoins(commondao.GetView(midID).Address()).String())\n\tprintln(\"root treasury (nearest live ancestor):\", b.GetCoins(commondao.GetView(rootID).Address()).String())\n}\n\n// Output:\n// mid deleted: true\n// leaf deleted: true\n// leaf treasury:\n// mid treasury (dissolved, skipped):\n// root treasury (nearest live ancestor): 150ugnot\n"},{"name":"z_13_e_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_13_e_filetest\n\npackage z_13_e_filetest\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n\n\ttesting.IssueCoins(commondao.GetView(daoID).Address(), chain.NewCoins(chain.NewCoin(\"ugnot\", 650)))\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tpID := commondao.CreateDissolutionProposal(cross(cur), daoID, owner)\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), daoID, pID)\n\n\tview := commondao.GetView(daoID)\n\tprintln(\"deleted:\", view.IsDeleted())\n\tb := banker.NewReadonlyBanker()\n\tprintln(\"treasury:\", b.GetCoins(view.Address()).String())\n\tprintln(\"destination:\", b.GetCoins(owner).String())\n}\n\n// Output:\n// deleted: true\n// treasury:\n// destination: 650ugnot\n"},{"name":"z_13_f_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_13_f_filetest\n\npackage z_13_f_filetest\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\trootID uint64\n\tsubID  uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\trootID = commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", \"\")\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), rootID, \"Sub\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), rootID, pID)\n\t}\n\tsubID = 3\n\n\ttesting.IssueCoins(commondao.GetView(subID).Address(), chain.NewCoins(chain.NewCoin(\"ugnot\", 800)))\n\n\tfID := commondao.CreateTreasuryFreezeProposal(cross(cur), rootID, subID, true)\n\tcommondao.Vote(cross(cur), rootID, fID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, fID)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// A frozen treasury does not block dissolution: the sweep sends the\n\t// funds where a clawback would put them\n\tpID := commondao.CreateDissolutionProposal(cross(cur), subID, \"\")\n\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, pID)\n\n\tview := commondao.GetView(subID)\n\tprintln(\"deleted:\", view.IsDeleted())\n\tprintln(\"frozen:\", view.IsTreasuryFrozen())\n\tb := banker.NewReadonlyBanker()\n\tprintln(\"sub treasury:\", b.GetCoins(view.Address()).String())\n\tprintln(\"root treasury:\", b.GetCoins(commondao.GetView(rootID).Address()).String())\n}\n\n// Output:\n// deleted: true\n// frozen: true\n// sub treasury:\n// root treasury: 800ugnot\n"},{"name":"z_13_g_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_13_g_filetest\n\npackage z_13_g_filetest\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\trootID  uint64\n\tsubID   uint64\n\tspendID uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\trootID = commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", \"\")\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), rootID, \"Sub\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), rootID, pID)\n\t}\n\tsubID = 3\n\n\ttesting.IssueCoins(commondao.GetView(subID).Address(), chain.NewCoins(chain.NewCoin(\"ugnot\", 700)))\n\n\t// Pass a spend on the sub-DAO but do not execute it\n\tspendID = commondao.CreateTreasurySpendProposal(cross(cur), subID, owner, \"ugnot\", 700)\n\tcommondao.Vote(cross(cur), subID, spendID, pdao.ChoiceYes, \"\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Dissolution dismisses the passed-but-unexecuted spend and sweeps the\n\t// full balance to the parent; the recipient never receives anything\n\tdID := commondao.CreateDissolutionProposal(cross(cur), subID, \"\")\n\tcommondao.Vote(cross(cur), rootID, dID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, dID)\n\n\tp, _ := commondao.GetView(subID).GetProposal(spendID)\n\tprintln(\"spend:\", string(p.Status()))\n\tprintln(\"reason:\", p.StatusReason())\n\tb := banker.NewReadonlyBanker()\n\tprintln(\"sub treasury:\", b.GetCoins(commondao.GetView(subID).Address()).String())\n\tprintln(\"root treasury:\", b.GetCoins(commondao.GetView(rootID).Address()).String())\n\tprintln(\"recipient:\", b.GetCoins(owner).String())\n}\n\n// Output:\n// spend: dismissed\n// reason: DAO dissolved\n// sub treasury:\n// root treasury: 700ugnot\n// recipient:\n"},{"name":"z_14_a_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_14_a_filetest\n\npackage z_14_a_filetest\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\trootID uint64\n\tsubID  uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\trootID = commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", \"\")\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), rootID, \"Sub\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), rootID, pID)\n\t}\n\tsubID = 3\n\n\ttesting.IssueCoins(commondao.GetView(subID).Address(), chain.NewCoins(chain.NewCoin(\"ugnot\", 100)))\n\n\tfID := commondao.CreateTreasuryFreezeProposal(cross(cur), rootID, subID, true)\n\tcommondao.Vote(cross(cur), rootID, fID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, fID)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tprintln(\"frozen:\", commondao.GetView(subID).IsTreasuryFrozen())\n\n\t// The ancestor unfreezes the treasury, re-enabling spends\n\tuID := commondao.CreateTreasuryFreezeProposal(cross(cur), rootID, subID, false)\n\tcommondao.Vote(cross(cur), rootID, uID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, uID)\n\tprintln(\"frozen:\", commondao.GetView(subID).IsTreasuryFrozen())\n\n\tsID := commondao.CreateTreasurySpendProposal(cross(cur), subID, owner, \"ugnot\", 100)\n\tcommondao.Vote(cross(cur), subID, sID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), subID, sID)\n\tprintln(\"recipient:\", banker.NewReadonlyBanker().GetCoins(owner).String())\n}\n\n// Output:\n// frozen: true\n// frozen: false\n// recipient: 100ugnot\n"},{"name":"z_14_b_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_14_b_filetest\n\npackage z_14_b_filetest\n\nimport (\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\trootID uint64\n\tsubID  uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\trootID = commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", \"\")\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), rootID, \"Sub\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), rootID, pID)\n\t}\n\tsubID = 3\n\n\tfID := commondao.CreateTreasuryFreezeProposal(cross(cur), rootID, subID, true)\n\tcommondao.Vote(cross(cur), rootID, fID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, fID)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// The frozen DAO cannot target itself: only a proper ancestor unfreezes\n\tcommondao.CreateTreasuryFreezeProposal(cross(cur), subID, subID, false)\n}\n\n// Error:\n// a DAO cannot target itself\n"},{"name":"z_14_c_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_14_c_filetest\n\npackage z_14_c_filetest\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\trootID uint64\n\tmidID  uint64\n\tleafID uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\trootID = commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", \"\")\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), rootID, \"Mid\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), rootID, pID)\n\t}\n\tmidID = 3\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), midID, \"Leaf\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), midID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), midID, pID)\n\t}\n\tleafID = 4\n\n\ttesting.IssueCoins(commondao.GetView(leafID).Address(), chain.NewCoins(chain.NewCoin(\"ugnot\", 100)))\n\n\t// The root (grandparent) freezes the leaf, then only the middle DAO\n\t// is dissolved. The root remains a live proper ancestor.\n\tfID := commondao.CreateTreasuryFreezeProposal(cross(cur), rootID, leafID, true)\n\tcommondao.Vote(cross(cur), rootID, fID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, fID)\n\n\tdID := commondao.CreateDissolutionProposal(cross(cur), midID, \"\")\n\tcommondao.Vote(cross(cur), rootID, dID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, dID)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Self-unfreeze is still forbidden: a live proper ancestor (the root)\n\t// remains, so the orphan-rescue path does not open. The rescue\n\t// requires ALL proper ancestors dissolved, not just the parent.\n\tcommondao.CreateTreasuryFreezeProposal(cross(cur), leafID, leafID, false)\n}\n\n// Error:\n// a DAO cannot target itself\n"},{"name":"z_14_d_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_14_d_filetest\n\npackage z_14_d_filetest\n\nimport (\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\trootID uint64\n\tmidID  uint64\n\tleafID uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\trootID = commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", \"\")\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), rootID, \"Mid\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), rootID, pID)\n\t}\n\tmidID = 3\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), midID, \"Leaf\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), midID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), midID, pID)\n\t}\n\tleafID = 4\n\n\t// Dissolve the leaf's entire ancestor chain\n\tdID := commondao.CreateDissolutionProposal(cross(cur), midID, \"\")\n\tcommondao.Vote(cross(cur), rootID, dID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, dID)\n\n\tdID = commondao.CreateDissolutionProposal(cross(cur), rootID, owner)\n\tcommondao.Vote(cross(cur), rootID, dID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, dID)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Even a full orphan may only UNFREEZE itself, never freeze itself:\n\t// the rescue branch is guarded on !frozen.\n\tcommondao.CreateTreasuryFreezeProposal(cross(cur), leafID, leafID, true)\n}\n\n// Error:\n// a DAO cannot target itself\n"},{"name":"z_14_e_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_14_e_filetest\n\npackage z_14_e_filetest\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\trootID uint64\n\tmidID  uint64\n\tleafID uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\trootID = commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", \"\")\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), rootID, \"Mid\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), rootID, pID)\n\t}\n\tmidID = 3\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), midID, \"Leaf\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), midID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), midID, pID)\n\t}\n\tleafID = 4\n\n\ttesting.IssueCoins(commondao.GetView(leafID).Address(), chain.NewCoins(chain.NewCoin(\"ugnot\", 100)))\n\n\t// The middle DAO freezes the leaf, then the root dissolves the middle\n\tfID := commondao.CreateTreasuryFreezeProposal(cross(cur), midID, leafID, true)\n\tcommondao.Vote(cross(cur), midID, fID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), midID, fID)\n\n\tdID := commondao.CreateDissolutionProposal(cross(cur), midID, \"\")\n\tcommondao.Vote(cross(cur), rootID, dID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, dID)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Freeze state lives on the target: any live proper ancestor can\n\t// unfreeze it after the freezing ancestor is gone\n\tuID := commondao.CreateTreasuryFreezeProposal(cross(cur), rootID, leafID, false)\n\tcommondao.Vote(cross(cur), rootID, uID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, uID)\n\tprintln(\"frozen:\", commondao.GetView(leafID).IsTreasuryFrozen())\n\n\tsID := commondao.CreateTreasurySpendProposal(cross(cur), leafID, owner, \"ugnot\", 100)\n\tcommondao.Vote(cross(cur), leafID, sID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), leafID, sID)\n\tprintln(\"recipient:\", banker.NewReadonlyBanker().GetCoins(owner).String())\n}\n\n// Output:\n// frozen: false\n// recipient: 100ugnot\n"},{"name":"z_14_f_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_14_f_filetest\n\npackage z_14_f_filetest\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\trootID uint64\n\tmidID  uint64\n\tleafID uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\trootID = commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", \"\")\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), rootID, \"Mid\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), rootID, pID)\n\t}\n\tmidID = 3\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), midID, \"Leaf\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), midID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), midID, pID)\n\t}\n\tleafID = 4\n\n\ttesting.IssueCoins(commondao.GetView(leafID).Address(), chain.NewCoins(chain.NewCoin(\"ugnot\", 300)))\n\n\t// Freeze the leaf, then dissolve its whole ancestor chain\n\tfID := commondao.CreateTreasuryFreezeProposal(cross(cur), rootID, leafID, true)\n\tcommondao.Vote(cross(cur), rootID, fID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, fID)\n\n\tdID := commondao.CreateDissolutionProposal(cross(cur), midID, \"\")\n\tcommondao.Vote(cross(cur), rootID, dID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, dID)\n\n\tdID = commondao.CreateDissolutionProposal(cross(cur), rootID, owner)\n\tcommondao.Vote(cross(cur), rootID, dID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, dID)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Orphan rescue: every proper ancestor is dissolved, so the freezing\n\t// authority class is extinct and the leaf's own council may unfreeze\n\t// itself, restoring access to its funds\n\tuID := commondao.CreateTreasuryFreezeProposal(cross(cur), leafID, leafID, false)\n\tcommondao.Vote(cross(cur), leafID, uID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), leafID, uID)\n\tprintln(\"frozen:\", commondao.GetView(leafID).IsTreasuryFrozen())\n\n\tsID := commondao.CreateTreasurySpendProposal(cross(cur), leafID, owner, \"ugnot\", 300)\n\tcommondao.Vote(cross(cur), leafID, sID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), leafID, sID)\n\tprintln(\"recipient:\", banker.NewReadonlyBanker().GetCoins(owner).String())\n}\n\n// Output:\n// frozen: false\n// recipient: 300ugnot\n"},{"name":"z_14_g_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_14_g_filetest\n\npackage z_14_g_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\n// Treasury freeze passes by SIMPLE majority. A 5-member ancestor council\n// splitting 3 YES / 2 NO clears a simple majority (2*3 \u003e 5) but not a\n// supermajority (3*3 \u003c 2*5), so under the correct Simple threshold the\n// target is frozen; a mutated Super threshold would dismiss it.\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n\tmoul  = address(\"g1manfred47kzduec920z88wfr64ylksmdcedlf5\") // @moul\n\tmF    = address(\"g1cyh9qm4y43w38gfk77qxczrhqj2asdr8l7uk54\")\n\tmG    = address(\"g1x95kxqpwvqjf98qpkvsay7tppkh6zs9fr7wwda\")\n)\n\nvar (\n\trootID uint64\n\tsubID  uint64\n\tpID    uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tmembers := strings.Join([]string{owner.String(), moul.String(), mF.String(), mG.String()}, \"\\n\")\n\trootID = commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", members)\n\n\tsp := commondao.CreateSubDAOProposal(cross(cur), rootID, \"Sub\", \"Purpose\", \"\", string(user))\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tcommondao.Vote(cross(cur), rootID, sp, pdao.ChoiceYes, \"\")\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Vote(cross(cur), rootID, sp, pdao.ChoiceYes, \"\")\n\ttesting.SetRealm(testing.NewUserRealm(moul))\n\tcommondao.Vote(cross(cur), rootID, sp, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, sp)\n\tsubID = 3\n\n\t// Freeze proposal, hosted in the ancestor (root), 3 YES / 2 NO.\n\tpID = commondao.CreateTreasuryFreezeProposal(cross(cur), rootID, subID, true)\n\ttesting.SetRealm(testing.NewUserRealm(mF))\n\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceNo, \"\")\n\ttesting.SetRealm(testing.NewUserRealm(mG))\n\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceNo, \"\")\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\ttesting.SetRealm(testing.NewUserRealm(moul))\n\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\ttesting.SkipHeights(121000)\n\n\tcommondao.Execute(cross(cur), rootID, pID)\n\n\tp, _ := commondao.GetView(rootID).GetProposal(pID)\n\tprintln(\"status:\", string(p.Status()))\n\tprintln(\"frozen:\", commondao.GetView(subID).IsTreasuryFrozen())\n}\n\n// Output:\n// status: executed\n// frozen: true\n"},{"name":"z_14_h_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_14_h_filetest\n\npackage z_14_h_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tcommondao.CreateTreasuryFreezeProposal(cross(cur), daoID, 404, true)\n}\n\n// Error:\n// DAO not found\n"},{"name":"z_14_i_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_14_i_filetest\n\n// Only a council member may propose an ancestor treasury freeze.\n//\n// Freeze is the containment lever an ancestor holds over a descendant,\n// so who may put it on the agenda matters as much as who may pass it.\npackage z_14_i_filetest\n\nimport (\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig, genesis council only\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1, owns the tree below\n)\n\nvar (\n\trootID uint64\n\tsubID  uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\trootID = commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", \"\")\n\n\tpID := commondao.CreateSubDAOProposal(cross(cur), rootID, \"Sub\", \"Purpose\", \"\", string(user))\n\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, pID)\n\tsubID = 3\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tcommondao.CreateTreasuryFreezeProposal(cross(cur), rootID, subID, true)\n}\n\n// Error:\n// caller is not a council member\n"},{"name":"z_15_a_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_15_a_filetest\n\npackage z_15_a_filetest\n\nimport (\n\t\"chain\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\tdaoA uint64\n\tdaoB uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoA = commondao.New(cross(cur), \"A\", \"Purpose\", \"\", \"\")\n\tdaoB = commondao.New(cross(cur), \"B\", \"Purpose\", \"\", \"\")\n}\n\nfunc main() {\n\ta := commondao.GetView(daoA).Address()\n\tb := commondao.GetView(daoB).Address()\n\n\tprintln(\"valid:\", a.IsValid() \u0026\u0026 b.IsValid())\n\tprintln(\"distinct:\", a != b)\n\n\t// The address is a pure derivation from the realm path and DAO ID\n\tderived := chain.DerivePkgSubAddr(\"gno.land/r/nt/commondao/v0\", \"dao/\"+strconv.FormatUint(daoA, 10))\n\tprintln(\"derived:\", a == derived)\n\n\t// The genesis DAO carries a derived address too\n\tg := commondao.GetView(1).Address()\n\tprintln(\"genesis:\", g == chain.DerivePkgSubAddr(\"gno.land/r/nt/commondao/v0\", \"dao/1\"))\n}\n\n// Output:\n// valid: true\n// distinct: true\n// derived: true\n// genesis: true\n"},{"name":"z_16_a_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_16_a_filetest\n\npackage z_16_a_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n}\n\nfunc main(cur realm) {\n\t// Listing defaults off\n\tprintln(\"default:\", commondao.IsListed(daoID))\n\n\t// A council member lists the DAO\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tcommondao.SetListed(cross(cur), daoID, true)\n\tprintln(\"after list:\", commondao.IsListed(daoID))\n\n\t// ...and can unlist it\n\tcommondao.SetListed(cross(cur), daoID, false)\n\tprintln(\"after unlist:\", commondao.IsListed(daoID))\n}\n\n// Output:\n// default: false\n// after list: true\n// after unlist: false\n"},{"name":"z_16_b_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_16_b_filetest\n\npackage z_16_b_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner  = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser   = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n\tnonMbr = address(\"g1manfred47kzduec920z88wfr64ylksmdcedlf5\") // @moul\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n}\n\nfunc main(cur realm) {\n\t// A non-council member cannot change listing\n\ttesting.SetRealm(testing.NewUserRealm(nonMbr))\n\tcommondao.SetListed(cross(cur), daoID, true)\n}\n\n// Error:\n// caller is not a council member\n"},{"name":"z_16_c_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_16_c_filetest\n\npackage z_16_c_filetest\n\nimport (\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n\tcommondao.SetListed(cross(cur), daoID, true)\n\n\t// Dissolve the DAO through a passed proposal\n\tpID := commondao.CreateDissolutionProposal(cross(cur), daoID, user)\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), daoID, pID)\n}\n\nfunc main() {\n\t// Dissolution auto-unlists the DAO from the home index.\n\tprintln(\"listed before dissolve was true; after dissolve:\", commondao.IsListed(daoID))\n}\n\n// Output:\n// listed before dissolve was true; after dissolve: false\n"},{"name":"z_16_d_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_16_d_filetest\n\npackage z_16_d_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tlistedID := commondao.New(cross(cur), \"Shown\", \"Purpose\", \"\", \"\")\n\tcommondao.New(cross(cur), \"Hidden\", \"Purpose\", \"\", \"\")\n\n\tcommondao.SetListed(cross(cur), listedID, true)\n}\n\nfunc main() {\n\t// The home index shows only listed DAOs: \"Shown\" appears, \"Hidden\"\n\t// (default unlisted) does not.\n\tprintln(commondao.Render(\"\"))\n}\n\n// Output:\n// # Common DAO\n// ---\n// This realm can be used to create CommonDAO instances based on [commondao](/p/nt/commondao/v0/) package.\n//\n// Here is a list of some of the DAOs that were created:\n//\n// - [Shown](/r/nt/commondao/v0:2)\n// - [Common DAO](/r/nt/commondao/v0:1)\n"},{"name":"z_16_e_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_16_e_filetest\n\npackage z_16_e_filetest\n\nimport (\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n\n\tpID := commondao.CreateDissolutionProposal(cross(cur), daoID, user)\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), daoID, pID)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// A dissolved DAO cannot be listed.\n\tcommondao.SetListed(cross(cur), daoID, true)\n}\n\n// Error:\n// DAO is deleted\n"},{"name":"z_16_f_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_16_f_filetest\n\npackage z_16_f_filetest\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n\n\t// Dissolve the root (root dissolution names a destination).\n\tpID := commondao.CreateDissolutionProposal(cross(cur), daoID, owner)\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), daoID, pID)\n}\n\nfunc main() {\n\t// A dissolved root has no live ancestor, so its page carries both the\n\t// dissolved banner and the unrecoverable-deposit warning.\n\tprintln(commondao.Render(strconv.FormatUint(daoID, 10)))\n}\n\n// Output:\n//\n// \u003e ⚠ This DAO has been dissolved\n//\n//\n// \u003e ⚠ No live ancestor DAO remains: coins sent to this treasury address can no longer be recovered.\n//\n// # Foo\n// **Purpose:** Purpose\n//\n// [View Proposals](/r/nt/commondao/v0:2/proposals) • [View Settings](/r/nt/commondao/v0:2/settings)\n//\n// ---\n// | Council |\n// | --- |\n// | g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 |\n//\n//\n// ## Treasury\n// - Address: g1vpah5sslxspfdvj9vk07rukek327dzmtxqkmk2\n// - Balance: (empty)\n"},{"name":"z_16_g_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_16_g_filetest\n\npackage z_16_g_filetest\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\trootID uint64\n\tleafID uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\trootID = commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", \"\")\n\n\t// A sub-DAO of the (still-live) root.\n\tpID := commondao.CreateSubDAOProposal(cross(cur), rootID, \"Leaf\", \"Purpose\", \"\", string(user))\n\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, pID)\n\tleafID = 3\n\n\t// Dissolve the leaf (the proposal is hosted in its parent, the root).\n\tdID := commondao.CreateDissolutionProposal(cross(cur), leafID, \"\")\n\tcommondao.Vote(cross(cur), rootID, dID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, dID)\n}\n\nfunc main() {\n\t// The leaf is dissolved but its root is alive, so the page shows the\n\t// dissolved banner WITHOUT the unrecoverable-treasury warning: a live\n\t// proper ancestor can still claw back the leaf's funds. This pins the\n\t// !hasLiveProperAncestor gate's negative branch.\n\tprintln(commondao.Render(strconv.FormatUint(leafID, 10)))\n}\n\n// Output:\n//\n// \u003e ⚠ This DAO has been dissolved\n//\n// # Leaf\n// **Purpose:** Purpose\n//\n// [View Proposals](/r/nt/commondao/v0:3/proposals) • [View Settings](/r/nt/commondao/v0:3/settings) • [Go to Parent DAO](/r/nt/commondao/v0:2)\n//\n// ---\n// | Council |\n// | --- |\n// | g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 |\n//\n//\n// ## Treasury\n// - Address: g17pr3grpaag8ketn84dl2y0nncef6u3jw8ca9gl\n// - Balance: (empty)\n"},{"name":"z_17_a_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_17_a_filetest\n\npackage z_17_a_filetest\n\nimport (\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner  = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser   = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n\trescue = address(\"g1manfred47kzduec920z88wfr64ylksmdcedlf5\") // @moul\n)\n\nvar (\n\trootID uint64\n\tsubID  uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\trootID = commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", \"\")\n\n\t// A sub-DAO whose only council member is `user`\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), rootID, \"Sub\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), rootID, pID)\n\t}\n\tsubID = 3\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// The parent (root), as a proper ancestor, adds a new member to the\n\t// sub-DAO's council and removes the original — the spec's rescue path\n\t// (:1531-1532). Hosted and voted in the root at supermajority.\n\tpID := commondao.CreateAncestorCouncilUpdateProposal(cross(cur), rootID, subID, string(rescue), string(user))\n\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, pID)\n\n\tview := commondao.GetView(subID)\n\tcouncil := view.Council()\n\tprintln(\"size:\", council.Size())\n\tprintln(\"has rescue:\", council.Has(rescue))\n\tprintln(\"has old:\", council.Has(user))\n}\n\n// Output:\n// size: 1\n// has rescue: true\n// has old: false\n"},{"name":"z_17_b_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_17_b_filetest\n\npackage z_17_b_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// A DAO cannot modify its own council through the ancestor path\n\t// (self-mutation goes through CreateCouncilUpdateProposal instead).\n\tcommondao.CreateAncestorCouncilUpdateProposal(cross(cur), daoID, daoID, string(owner), \"\")\n}\n\n// Error:\n// a DAO cannot target itself\n"},{"name":"z_17_c_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_17_c_filetest\n\npackage z_17_c_filetest\n\nimport (\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\trootID uint64\n\tsubID  uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\trootID = commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", \"\")\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), rootID, \"Sub\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), rootID, pID)\n\t}\n\tsubID = 3\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// An ancestor update that would empty the descendant's council fails\n\t// cleanly (the package would-empty guard applies through the ancestor\n\t// path too): the proposal ends Failed and the council is unchanged.\n\tpID := commondao.CreateAncestorCouncilUpdateProposal(cross(cur), rootID, subID, \"\", string(user))\n\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, pID)\n\n\tp, _ := commondao.GetView(rootID).GetProposal(pID)\n\tprintln(\"status:\", string(p.Status()))\n\tprintln(\"reason:\", p.StatusReason())\n\tprintln(\"sub council size:\", commondao.GetView(subID).Council().Size())\n}\n\n// Output:\n// status: failed\n// reason: council update would remove every council member\n// sub council size: 1\n"},{"name":"z_18_a_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_18_a_filetest\n\npackage z_18_a_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// A Charter requires a purpose: an empty purpose is rejected.\n\tcommondao.New(cross(cur), \"Foo\", \"\", \"\", \"\")\n}\n\n// Error:\n// DAO purpose is empty\n"},{"name":"z_18_b_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_18_b_filetest\n\npackage z_18_b_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Purpose is capped at 250 characters.\n\tcommondao.New(cross(cur), \"Foo\", strings.Repeat(\"A\", 251), \"\", \"\")\n}\n\n// Error:\n// DAO purpose is too long, max length is 250 characters\n"},{"name":"z_18_c_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_18_c_filetest\n\npackage z_18_c_filetest\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\t// The Purpose is user-controlled Charter text: markdown metacharacters\n\t// must render escaped, never as live markup.\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"*Evil* [x](https://evil.example) # H1\", \"\", \"\")\n}\n\nfunc main() {\n\tprintln(commondao.Render(strconv.FormatUint(daoID, 10)))\n}\n\n// Output:\n// # Foo\n// **Purpose:** \\*Evil\\* \\[x\\]\\(https://evil\\.example\\) \\# H1\n//\n// [View Proposals](/r/nt/commondao/v0:2/proposals) • [View Settings](/r/nt/commondao/v0:2/settings)\n//\n// ---\n// | Council |\n// | --- |\n// | g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 |\n//\n//\n// ## Treasury\n// - Address: g1vpah5sslxspfdvj9vk07rukek327dzmtxqkmk2\n// - Balance: (empty)\n// ## Create Proposal\n// These are the proposal supported by this DAO:\n//\n// \u003cgno-columns\u003e\n// [General Proposal](/r/nt/commondao/v0$help\u0026func=CreateTextProposal\u0026body=\u0026daoID=2\u0026title=\u0026votingDays=7)\n//\n// This type of proposal is also known as text proposal which can be used for example to get consensus on initiatives without actually making any change on-chain.\n//\n//\n// \u003cgno-columns-sep\u003e\n// [Update Council](/r/nt/commondao/v0$help\u0026func=CreateCouncilUpdateProposal\u0026daoID=2\u0026newMembers=\u0026removeMembers=)\n//\n// This type of proposal can be used to add new council members to this DAO and also to remove existing ones.\n//\n// A single proposal allows new council members to be added and any number of existing ones removed within the same proposal.\n//\n//\n// \u003cgno-columns-sep\u003e\n// [Ancestor Council Update](/r/nt/commondao/v0$help\u0026func=CreateAncestorCouncilUpdateProposal\u0026daoID=2\u0026newMembers=\u0026removeMembers=\u0026targetID=)\n//\n// This type of proposal lets this DAO, as an ancestor, add or remove council members of one of its descendant DAOs — the rescue path for a descendant whose council is stuck or empty.\n//\n//\n// \u003c/gno-columns\u003e\n// \u003cgno-columns\u003e\n// [Dissolve DAO](/r/nt/commondao/v0$help\u0026func=CreateDissolutionProposal\u0026daoID=2\u0026destination=)\n//\n// This type of proposal can be used to dissolve DAOs and SubDAOs.\n//\n// Dissolving a DAO can't be undone, once the dissolution proposal passes and is executed DAO will be readonly.\n//\n//\n// \u003cgno-columns-sep\u003e\n// [Treasury Spend](/r/nt/commondao/v0$help\u0026func=CreateTreasurySpendProposal\u0026amount=\u0026daoID=2\u0026denom=ugnot\u0026to=)\n//\n// This type of proposal sends coins from the DAO's own treasury when it passes and is executed.\n//\n//\n// \u003cgno-columns-sep\u003e\n// [Treasury Clawback](/r/nt/commondao/v0$help\u0026func=CreateTreasuryClawbackProposal\u0026daoID=2\u0026targetID=)\n//\n// This type of proposal sweeps a descendant DAO's treasury to the descendant's parent DAO.\n//\n//\n// \u003c/gno-columns\u003e\n// \u003cgno-columns\u003e\n// [Treasury Freeze](/r/nt/commondao/v0$help\u0026func=CreateTreasuryFreezeProposal\u0026daoID=2\u0026frozen=true\u0026targetID=)\n//\n// This type of proposal freezes or unfreezes a descendant DAO's treasury. While frozen, no funds can leave the treasury.\n//\n//\n// \u003cgno-columns-sep\u003e\n// [New SubDAO](/r/nt/commondao/v0$help\u0026func=CreateSubDAOProposal\u0026daoID=2\u0026description=\u0026members=\u0026name=\u0026purpose=)\n//\n// This type of proposal is used to create SubDAOs, which are used to create tree based DAOs.\n//\n//\n// \u003cgno-columns-sep\u003e\n// [Register Proposal Kind](/r/nt/commondao/v0$help\u0026func=CreateRegisterKindProposal\u0026daoID=2\u0026kindName=) • [Deregister Proposal Kind](/r/nt/commondao/v0$help\u0026func=CreateDeregisterKindProposal\u0026daoID=2\u0026kindName=)\n//\n// This type of proposal registers one of the realm's catalog proposal kinds on this DAO by name, or deregisters a kind. Deregistering a kind blocks new proposals of that kind while in-flight ones still vote and execute. The manage-kinds kind itself cannot be deregistered.\n//\n//\n// \u003c/gno-columns\u003e\n// \u003cgno-columns\u003e\n// [Amend Bylaws](/r/nt/commondao/v0$help\u0026func=CreateAmendBylawsProposal\u0026daoID=2\u0026payload=)\n//\n// This type of proposal adds, amends or removes one of the DAO's bylaws documents with a diff patch pinned to the current document text (the mandates folder is reserved: mandates are set from above, not by the council). Build the payload with the AmendBylawsPayload query function.\n//\n//\n// \u003c/gno-columns\u003e\n"},{"name":"z_19_a_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_19_a_filetest\n\npackage z_19_a_filetest\n\nimport (\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\t// Single-member council: one YES decides any proposal immediately\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n\n\t// Governance-deregister the text kind\n\tpID := commondao.CreateDeregisterKindProposal(cross(cur), daoID, \"text\")\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), daoID, pID)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// The text kind is deregistered while every other kind still works\n\tprintln(\"text registered:\", commondao.HasProposalKind(daoID, \"text\"))\n\tpID := commondao.CreateCouncilUpdateProposal(cross(cur), daoID, string(owner), \"\")\n\tprintln(\"council update created:\", pID)\n\n\t// Creating a proposal of the deregistered kind is rejected\n\tcommondao.CreateTextProposal(cross(cur), daoID, \"Title\", \"Body\", 0)\n}\n\n// Output:\n// text registered: false\n// council update created: 2\n\n// Error:\n// proposal kind not found\n"},{"name":"z_19_b_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_19_b_filetest\n\npackage z_19_b_filetest\n\nimport (\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\tdaoID uint64\n\tpID   uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n\n\t// A text proposal is created while its kind is still registered\n\tpID = commondao.CreateTextProposal(cross(cur), daoID, \"Title\", \"Body\", 0)\n\n\t// Governance-deregister the text kind after the proposal exists\n\tgID := commondao.CreateDeregisterKindProposal(cross(cur), daoID, \"text\")\n\tcommondao.Vote(cross(cur), daoID, gID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), daoID, gID)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// The kind registry is read only at Propose, so the in-flight proposal\n\t// created before the kind was deregistered still votes and executes\n\tprintln(\"text registered:\", commondao.HasProposalKind(daoID, \"text\"))\n\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), daoID, pID)\n\n\tp, _ := commondao.GetView(daoID).GetProposal(pID)\n\tprintln(\"status:\", string(p.Status()))\n}\n\n// Output:\n// text registered: false\n// status: executed\n"},{"name":"z_19_c_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_19_c_filetest\n\npackage z_19_c_filetest\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n\n\t// Governance-deregister the text kind: new text proposals are blocked\n\t// (see z_19_a for the terminal rejection)\n\tpID := commondao.CreateDeregisterKindProposal(cross(cur), daoID, \"text\")\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), daoID, pID)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tprintln(\"after deregister:\", commondao.HasProposalKind(daoID, \"text\"))\n\n\t// The DAO page drops the deregistered kind's create link\n\tout := commondao.Render(strconv.FormatUint(daoID, 10))\n\tprintln(\"create link after deregister:\", strings.Contains(out, \"CreateTextProposal\"))\n\n\t// ...and the settings kinds row drops the kind name (sorted row would\n\t// read \"subdao, text, treasury\\-clawback\" when registered)\n\tout = commondao.Render(strconv.FormatUint(daoID, 10) + \"/settings\")\n\tprintln(\"settings row lists text after deregister:\", strings.Contains(out, \"subdao, text\"))\n\n\t// Re-register the kind by name through a second governance proposal\n\tpID := commondao.CreateRegisterKindProposal(cross(cur), daoID, \"text\")\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), daoID, pID)\n\tprintln(\"after re-register:\", commondao.HasProposalKind(daoID, \"text\"))\n\n\t// ...and shows it again once the kind is re-registered\n\tout = commondao.Render(strconv.FormatUint(daoID, 10))\n\tprintln(\"create link after re-register:\", strings.Contains(out, \"CreateTextProposal\"))\n\n\t// Text proposals can be created again\n\ttID := commondao.CreateTextProposal(cross(cur), daoID, \"Title\", \"Body\", 0)\n\tprintln(\"created:\", tID)\n}\n\n// Output:\n// after deregister: false\n// create link after deregister: false\n// settings row lists text after deregister: false\n// after re-register: true\n// create link after re-register: true\n// created: 3\n"},{"name":"z_19_d_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_19_d_filetest\n\npackage z_19_d_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\n// Manage-kinds proposals are decided by SUPERMAJORITY: with a 5-member\n// council, 3 YES votes (a simple majority) settle nothing (3*3 \u003c 2*5), so\n// the proposal stays active and cannot be executed before its deadline. A\n// mutant deciding it by simple majority would pass it early here.\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n\tmoul  = address(\"g1manfred47kzduec920z88wfr64ylksmdcedlf5\") // @moul\n\tmemD  = address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\") // @test2\n\tmemE  = address(\"g1cyh9qm4y43w38gfk77qxczrhqj2asdr8l7uk54\")\n)\n\nvar (\n\tdaoID uint64\n\tpID   uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\t// Council is {user, owner, moul, memD, memE}\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tmembers := strings.Join([]string{owner.String(), moul.String(), memD.String(), memE.String()}, \"\\n\")\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", members)\n\n\tpID = commondao.CreateDeregisterKindProposal(cross(cur), daoID, \"text\")\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\ttesting.SetRealm(testing.NewUserRealm(moul))\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n}\n\nfunc main(cur realm) {\n\t// 3 of 5 YES: passed under a simple majority, undecided under the\n\t// required supermajority\n\tp, _ := commondao.GetView(daoID).GetProposal(pID)\n\tif p.Status() != pdao.StatusActive {\n\t\tpanic(\"expected the proposal to remain active\")\n\t}\n\n\t// An undecided proposal cannot be executed before its voting deadline\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tcommondao.Execute(cross(cur), daoID, pID)\n}\n\n// Error:\n// voting deadline not met\n"},{"name":"z_19_e_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_19_e_filetest\n\npackage z_19_e_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner    = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser     = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n\tstranger = address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\") // @test2\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n}\n\nfunc main(cur realm) {\n\t// Only council members can propose kind changes\n\ttesting.SetRealm(testing.NewUserRealm(stranger))\n\tcommondao.CreateDeregisterKindProposal(cross(cur), daoID, \"text\")\n}\n\n// Error:\n// caller is not a council member\n"},{"name":"z_19_f_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_19_f_filetest\n\npackage z_19_f_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n}\n\nfunc main(cur realm) {\n\t// Self-brick guard: deregistering the manage-kinds kind itself would\n\t// leave the DAO unable to ever manage its kinds, so it fails at creation\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tcommondao.CreateDeregisterKindProposal(cross(cur), daoID, \"manage-kinds\")\n}\n\n// Error:\n// the manage-kinds kind cannot be deregistered\n"},{"name":"z_1_a_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_1_a_filetest\n\npackage z_1_a_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tcommondao.Invite(cross(cur), user)\n\n\tprintln(commondao.IsInvited(user))\n}\n\n// Output:\n// true\n"},{"name":"z_1_b_filetest.gno","body":"package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tprintln(commondao.IsInvited(user))\n}\n\n// Output:\n// false\n"},{"name":"z_1_c_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_1_c_filetest\n\npackage z_1_c_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\tuser    = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n\tinvitee = address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\") // @test2\n)\n\nfunc main(cur realm) {\n\t// Call as a user which is not a Common DAO member\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tcommondao.Invite(cross(cur), invitee)\n}\n\n// Error:\n// unauthorized\n"},{"name":"z_20_a_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_20_a_filetest\n\npackage z_20_a_filetest\n\nimport (\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\tdaoID uint64\n\tran   bool\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\t// Single-member council: one YES decides any proposal immediately\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n\n\t// The execution kind is opt-in: register it by name through governance first.\n\tpID := commondao.CreateRegisterKindProposal(cross(cur), daoID, \"execution\")\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), daoID, pID)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tprintln(\"execution registered:\", commondao.HasProposalKind(daoID, \"execution\"))\n\n\t// The closure is authored in this persistent realm, so it survives\n\t// Propose-\u003eExecute. It sets a package var so execution is observable.\n\tpID := commondao.CreateExecutionProposal(cross(cur), daoID, \"Run it\", \"Body\", func(_ int, _ realm) error {\n\t\tran = true\n\t\treturn nil\n\t})\n\tprintln(\"created:\", pID)\n\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), daoID, pID)\n\tprintln(\"closure ran:\", ran)\n}\n\n// Output:\n// execution registered: true\n// created: 2\n// closure ran: true\n"},{"name":"z_20_b_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_20_b_filetest\n\npackage z_20_b_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// The execution kind is NOT seeded on new DAOs. Creating an execution\n\t// proposal before the DAO registers the kind is refused by the opt-in\n\t// register-gate (assertKindRegistered). Removing that gate would let this\n\t// call reach Propose, which rejects the unregistered kind with a\n\t// different message (\"proposal kind not found\") — this golden pins the\n\t// gate.\n\tprintln(\"execution registered:\", commondao.HasProposalKind(daoID, \"execution\"))\n\tcommondao.CreateExecutionProposal(cross(cur), daoID, \"Run it\", \"Body\", func(_ int, _ realm) error {\n\t\treturn nil\n\t})\n}\n\n// Output:\n// execution registered: false\n\n// Error:\n// proposal kind is not registered: execution\n"},{"name":"z_20_c_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_20_c_filetest\n\npackage z_20_c_filetest\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n\n\tpID := commondao.CreateRegisterKindProposal(cross(cur), daoID, \"execution\")\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), daoID, pID)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// The execution kind's title and body are raw, user-supplied text\n\t// (executionDef, a package type that cannot implement the realm's\n\t// trustedMarkdownBody marker). The renderer must escape both so\n\t// markdown metacharacters render as text, never as markup.\n\tpID := commondao.CreateExecutionProposal(\n\t\tcross(cur),\n\t\tdaoID,\n\t\t\"*Pwn* # H1\",\n\t\t\"[link](https://evil.example) # Injected\",\n\t\tfunc(_ int, _ realm) error { return nil },\n\t)\n\n\tout := commondao.Render(strconv.FormatUint(daoID, 10) + \"/proposals/\" + strconv.FormatUint(pID, 10))\n\n\t// Escaped forms are present; raw markup is not.\n\tprintln(\"title escaped:\", strings.Contains(out, `\\*Pwn\\* \\# H1`))\n\tprintln(\"body escaped:\", strings.Contains(out, `\\[link\\]\\(https://evil\\.example\\) \\# Injected`))\n\tprintln(\"raw H1 injected:\", strings.Contains(out, \"\\n# Injected\"))\n\n\t// The council is told the proposal runs code it cannot read; the\n\t// proposer's own prose must never be the only thing on the page.\n\tprintln(\"arbitrary-code disclosure shown:\", strings.Contains(out, \"runs arbitrary code with the DAO's own authority\"))\n}\n\n// Output:\n// title escaped: true\n// body escaped: true\n// raw H1 injected: false\n// arbitrary-code disclosure shown: true\n"},{"name":"z_20_d_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_20_d_filetest\n\npackage z_20_d_filetest\n\nimport (\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\t// Single-member council: one YES decides any proposal immediately.\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Deregister-by-name round trip through the permanent manage-kinds kind:\n\t// the text kind is seeded, so a supermajority deregister removes it and\n\t// HasProposalKind flips to false. Deregistering blocks only new proposals\n\t// of the kind; the registry is read at Propose time.\n\tprintln(\"text registered before:\", commondao.HasProposalKind(daoID, \"text\"))\n\n\tpID := commondao.CreateDeregisterKindProposal(cross(cur), daoID, \"text\")\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), daoID, pID)\n\tprintln(\"text registered after:\", commondao.HasProposalKind(daoID, \"text\"))\n\n\t// A catalog kind deregistered by name can be re-registered by name: the\n\t// manage-kinds kind is never bricked, so the kind set stays governable.\n\tpID = commondao.CreateRegisterKindProposal(cross(cur), daoID, \"text\")\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), daoID, pID)\n\tprintln(\"text re-registered:\", commondao.HasProposalKind(daoID, \"text\"))\n}\n\n// Output:\n// text registered before: true\n// text registered after: false\n// text re-registered: true\n"},{"name":"z_20_e_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_20_e_filetest\n\npackage z_20_e_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// No-op register: the text kind is already seeded, so a council vote to\n\t// register it again is rejected at creation (a vote must be about a real\n\t// change).\n\tcommondao.CreateRegisterKindProposal(cross(cur), daoID, \"text\")\n}\n\n// Error:\n// proposal kind is already registered\n"},{"name":"z_20_f_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_20_f_filetest\n\npackage z_20_f_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// No-op deregister: the execution kind is opt-in and not seeded, so a\n\t// council vote to deregister an absent kind is rejected at creation.\n\tcommondao.CreateDeregisterKindProposal(cross(cur), daoID, \"execution\")\n}\n\n// Error:\n// proposal kind is not registered: execution\n"},{"name":"z_20_g_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_20_g_filetest\n\npackage z_20_g_filetest\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\trootID uint64\n\tsubID  uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\trootID = commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", \"\")\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), rootID, \"Sub\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), rootID, pID)\n\t}\n\tsubID = 3\n\n\t// Fund the sub-DAO and enable the execution kind on it.\n\ttesting.IssueCoins(commondao.GetView(subID).Address(), chain.NewCoins(chain.NewCoin(\"ugnot\", 500)))\n\t{\n\t\tpID := commondao.CreateRegisterKindProposal(cross(cur), subID, \"execution\")\n\t\tcommondao.Vote(cross(cur), subID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), subID, pID)\n\t}\n\n\t// The ancestor freezes the sub-DAO treasury.\n\tfID := commondao.CreateTreasuryFreezeProposal(cross(cur), rootID, subID, true)\n\tcommondao.Vote(cross(cur), rootID, fID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, fID)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// The sub-DAO is frozen by a proper ancestor. Freeze means no funds can\n\t// leave, so an execution proposal that would drain the sub's own treasury\n\t// is refused at CREATE time (fail-fast), defeating any attempt to bypass\n\t// the ancestor's freeze through arbitrary execution.\n\tcommondao.CreateExecutionProposal(cross(cur), subID, \"Drain\", \"Body\", func(_ int, sub realm) error {\n\t\tb := banker.NewBanker(banker.BankerTypeRealmSend, sub)\n\t\tb.SendCoins(sub.Address(), user, chain.NewCoins(chain.NewCoin(\"ugnot\", 500)))\n\t\treturn nil\n\t})\n}\n\n// Error:\n// commondao: treasury is frozen\n"},{"name":"z_20_h_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_20_h_filetest\n\npackage z_20_h_filetest\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\trootID uint64\n\tsubID  uint64\n\texecID uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\trootID = commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", \"\")\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), rootID, \"Sub\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), rootID, pID)\n\t}\n\tsubID = 3\n\n\t// Fund the sub-DAO and enable the execution kind on it.\n\ttesting.IssueCoins(commondao.GetView(subID).Address(), chain.NewCoins(chain.NewCoin(\"ugnot\", 500)))\n\t{\n\t\tpID := commondao.CreateRegisterKindProposal(cross(cur), subID, \"execution\")\n\t\tcommondao.Vote(cross(cur), subID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), subID, pID)\n\t}\n\n\t// Pass an execution proposal that drains the sub's own treasury, but do\n\t// not execute it yet. It is created while the sub is still unfrozen.\n\texecID = commondao.CreateExecutionProposal(cross(cur), subID, \"Drain\", \"Body\", func(_ int, sub realm) error {\n\t\tb := banker.NewBanker(banker.BankerTypeRealmSend, sub)\n\t\tb.SendCoins(sub.Address(), user, chain.NewCoins(chain.NewCoin(\"ugnot\", 500)))\n\t\treturn nil\n\t})\n\tcommondao.Vote(cross(cur), subID, execID, pdao.ChoiceYes, \"\")\n\n\t// The ancestor freezes the sub-DAO treasury before the execution runs.\n\tfID := commondao.CreateTreasuryFreezeProposal(cross(cur), rootID, subID, true)\n\tcommondao.Vote(cross(cur), rootID, fID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), rootID, fID)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// The standing passed execution proposal fails cleanly once the treasury\n\t// is frozen: the freeze check reruns inside Execute, so the closure never\n\t// runs and no funds leave (StatusFailed, not a stuck-Passed tx panic).\n\tcommondao.Execute(cross(cur), subID, execID)\n\n\tview := commondao.GetView(subID)\n\tp, _ := view.GetProposal(execID)\n\tprintln(\"status:\", string(p.Status()))\n\tprintln(\"reason:\", p.StatusReason())\n\tprintln(\"frozen:\", view.IsTreasuryFrozen())\n\tprintln(\"treasury:\", banker.NewReadonlyBanker().GetCoins(view.Address()).String())\n\n\t// A frozen DAO that has the execution kind registered gets the extra\n\t// warning: freeze stops the realm's own spending paths, but a banker\n\t// retained by an earlier execution closure reaches the bank keeper\n\t// without passing through them, so freeze alone is not containment.\n\tout := commondao.Render(strconv.FormatUint(subID, 10))\n\tprintln(\"capability warning shown:\", strings.Contains(out, \"freeze alone is not containment here\"))\n}\n\n// Output:\n// status: failed\n// reason: commondao: treasury is frozen\n// frozen: true\n// treasury: 500ugnot\n// capability warning shown: true\n"},{"name":"z_20_i_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_20_i_filetest\n\npackage z_20_i_filetest\n\nimport (\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\tdaoID  uint64\n\ttextID uint64\n\texecID uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\t// Single-member council: one YES decides any proposal immediately.\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n\n\t// Enable the execution kind through governance.\n\t{\n\t\tpID := commondao.CreateRegisterKindProposal(cross(cur), daoID, \"execution\")\n\t\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), daoID, pID)\n\t}\n\n\t// A second proposal, passed but left unexecuted: the re-entrant target.\n\ttextID = commondao.CreateTextProposal(cross(cur), daoID, \"Text\", \"Body\", 7)\n\tcommondao.Vote(cross(cur), daoID, textID, pdao.ChoiceYes, \"\")\n\n\t// An execution proposal whose closure crosses back in as the DAO's own\n\t// sub and calls Execute on the passed text proposal — a re-entrant\n\t// Execute. The closure is authored here so it survives Propose-\u003eExecute.\n\texecID = commondao.CreateExecutionProposal(cross(cur), daoID, \"Re-enter\", \"Body\", func(_ int, sub realm) error {\n\t\tcommondao.Execute(cross(sub), daoID, textID)\n\t\treturn nil\n\t})\n\tcommondao.Vote(cross(cur), daoID, execID, pdao.ChoiceYes, \"\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Executing the execution proposal runs its closure, which re-enters\n\t// Execute while the realm-global latch is raised. The nested Execute\n\t// panics; thrown across the cross(sub) boundary it arrived through, that\n\t// aborts the whole transaction.\n\tcommondao.Execute(cross(cur), daoID, execID)\n}\n\n// Error:\n// commondao: re-entrant Execute is not allowed\n"},{"name":"z_20_j_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_20_j_filetest\n\n// Only a council member may propose arbitrary execution.\n//\n// /p/ Propose does not check council membership — it checks the DAO is\n// live, looks up the kind, takes the latch and counts the cap — so this\n// wrapper's gate is the only thing standing between a stranger and an\n// ExecFunc closure of their choosing running with the DAO's authority.\n// The council would still have to vote it through, but the payload\n// author would be unauthenticated and the closure is not displayable.\npackage z_20_j_filetest\n\nimport (\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig, genesis council only\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1, owns the DAO below\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n\n\t// Opt the DAO into the execution kind, so the gate below is the only\n\t// thing left to refuse the call.\n\tpID := commondao.CreateRegisterKindProposal(cross(cur), daoID, \"execution\")\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), daoID, pID)\n}\n\nfunc main(cur realm) {\n\t// owner sits on the genesis DAO's council, not on this DAO's.\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tcommondao.CreateExecutionProposal(cross(cur), daoID, \"T\", \"B\",\n\t\tfunc(_ int, sub realm) error { return nil })\n}\n\n// Error:\n// caller is not a council member\n"},{"name":"z_21_a_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_21_a_filetest\n\n// A DAO acting as a council member of another DAO.\n//\n// P (the \"parent\") has two council members: a person (`user`) and another DAO\n// (`M`). A DAO has no private key, so M cannot sign a Vote transaction. A DAO\n// votes by running an EXECUTION proposal in its own governance whose closure\n// calls commondao.Vote as M's own sub-identity: the host mints M's sub\n// (cur.Sub(\"dao/\u003cM\u003e\")) and the closure crosses into Vote with it, so P sees\n// M's address as the caller and records M's vote.\n//\n// The closure must be authored in a PERSISTENT realm — a closure from a CLI\n// `maketx run` does not survive Propose-\u003eExecute. This filetest is itself a\n// persistent realm, so it plays that role; on chain you would deploy a small\n// \"policy\" realm that sits on M's council and authors the closure.\n//\n// Transaction sequence (each commondao.* / policy-realm call is one tx):\n//\n//\tsetup: create M; register the execution kind on M; create P with {user, M}\n//\tin P:  1. CreateTextProposal      (user)         -\u003e propP\n//\t       2. Vote YES on propP        (user)         [propP: 1/2, active]\n//\tin M:  3. CreateExecutionProposal  (policy realm) -\u003e propM\n//\t             closure := Vote YES on (P, propP) as M\n//\t       4. Vote YES on propM        (M's council)\n//\t       5. Execute propM            -\u003e runs closure -\u003e M votes YES in P\n//\t                                                     [propP: 2/2, passed]\n//\tin P:  6. Execute propP            -\u003e Executed\npackage z_21_a_filetest\n\nimport (\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig (genesis council)\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1 (operator)\n)\n\nvar (\n\tmemberDAO uint64  // M\n\tparentDAO uint64  // P\n\tmAddr     address // M's on-chain address (a council member of P)\n)\n\nfunc init(cur realm) {\n\t// The genesis council invites the operator, who may then create DAOs.\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Create M, the DAO that will be a member of P. (Single-member council\n\t// here, standing in for however M is really governed.)\n\tmemberDAO = commondao.New(cross(cur), \"MemberDAO\", \"Acts as a member of another DAO\", \"\", \"\")\n\n\t// A DAO can only act as its own address through the execution kind, which\n\t// is opt-in. Register it on M through M's own governance.\n\tpid := commondao.CreateRegisterKindProposal(cross(cur), memberDAO, \"execution\")\n\tcommondao.Vote(cross(cur), memberDAO, pid, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), memberDAO, pid)\n\n\t// M's address is what we place on P's council.\n\tmAddr = commondao.GetView(memberDAO).Address()\n\n\t// Create P with two council members: the person `user` and the DAO `M`.\n\tparentDAO = commondao.New(cross(cur), \"ParentDAO\", \"Governed by a person and a DAO\", \"\", mAddr.String())\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tprintln(\"P has the member DAO on its council:\", commondao.GetView(parentDAO).Council().Has(mAddr))\n\n\t// Tx 1 + 2: a proposal in P, and the human member's YES vote. One YES is\n\t// not enough: a two-member council needs both to reach the threshold.\n\tpropP := commondao.CreateTextProposal(cross(cur), parentDAO, \"Parent decision\", \"Should P do X?\", 7)\n\tcommondao.Vote(cross(cur), parentDAO, propP, pdao.ChoiceYes, \"person votes yes\")\n\tprintln(\"propP after the person's vote:\", string(statusOf(parentDAO, propP)))\n\n\t// Tx 3: M creates an execution proposal. The closure is authored in this\n\t// persistent realm and captures P's IDs; at execution it receives M's sub\n\t// and votes as M.\n\tpropM := commondao.CreateExecutionProposal(cross(cur), memberDAO,\n\t\t\"Vote in ParentDAO\", \"Cast this DAO's YES vote on the parent proposal.\",\n\t\tfunc(_ int, sub realm) error {\n\t\t\tcommondao.Vote(cross(sub), parentDAO, propP, pdao.ChoiceYes, \"cast as the member DAO\")\n\t\t\treturn nil\n\t\t},\n\t)\n\n\t// Tx 4 + 5: M's council passes the execution proposal, then executes it.\n\t// Executing runs the closure, which casts M's vote in P as M's own address.\n\tcommondao.Vote(cross(cur), memberDAO, propM, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), memberDAO, propM)\n\n\tpP, _ := commondao.GetView(parentDAO).GetProposal(propP)\n\tprintln(\"M's vote recorded in P:\", pP.VotingRecord().HasVoted(mAddr))\n\tprintln(\"propP YES votes:\", pP.VotingRecord().VoteCount(pdao.ChoiceYes))\n\tprintln(\"propP after the member DAO's vote:\", string(pP.Status()))\n\n\t// Tx 6: finalize the now-passed parent proposal.\n\tcommondao.Execute(cross(cur), parentDAO, propP)\n\tprintln(\"propP final status:\", string(statusOf(parentDAO, propP)))\n}\n\nfunc statusOf(daoID, propID uint64) pdao.ProposalStatus {\n\tp, _ := commondao.GetView(daoID).GetProposal(propID)\n\treturn p.Status()\n}\n\n// Output:\n// P has the member DAO on its council: true\n// propP after the person's vote: active\n// M's vote recorded in P: true\n// propP YES votes: 2\n// propP after the member DAO's vote: passed\n// propP final status: executed\n"},{"name":"z_22_a_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_22_a_filetest\n\n// Bylaws lifecycle through governance: create a document with a diff\n// patch, amend it, list the set, render the bylaws page, remove it.\npackage z_22_a_filetest\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\t// Single-member council: one YES decides any proposal immediately\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Create a document through governance: the payload is a create patch.\n\tpayload := commondao.AmendBylawsPayload(daoID, \"bylaws/quorum.md\", \"Quorum is half the council.\")\n\tpID := commondao.CreateAmendBylawsProposal(cross(cur), daoID, payload)\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), daoID, pID)\n\tprintln(\"created:\", commondao.GetBylawsDoc(daoID, \"bylaws/quorum.md\"))\n\n\t// Amend it with a diff patch against the current text.\n\tpayload = commondao.AmendBylawsPayload(daoID, \"bylaws/quorum.md\", \"Quorum is two thirds of the council.\")\n\tpID = commondao.CreateAmendBylawsProposal(cross(cur), daoID, payload)\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), daoID, pID)\n\tprintln(\"amended:\", commondao.GetBylawsDoc(daoID, \"bylaws/quorum.md\"))\n\n\t// Add a second document, then list the whole set (sorted).\n\tpayload = commondao.AmendBylawsPayload(daoID, \"bylaws/spend.md\", \"Spend only by vote.\")\n\tpID = commondao.CreateAmendBylawsProposal(cross(cur), daoID, payload)\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), daoID, pID)\n\tfor _, path := range commondao.ListBylawsDocs(daoID, \"\") {\n\t\tprintln(\"doc:\", path)\n\t}\n\n\t// The bylaws page renders every document with its base hash.\n\tprintln(commondao.Render(strconv.FormatUint(daoID, 10) + \"/bylaws\"))\n\n\t// Remove it: an empty proposed text is a remove patch.\n\tpayload = commondao.AmendBylawsPayload(daoID, \"bylaws/spend.md\", \"\")\n\tpID = commondao.CreateAmendBylawsProposal(cross(cur), daoID, payload)\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), daoID, pID)\n\tprintln(\"removed:\", commondao.GetBylawsDoc(daoID, \"bylaws/spend.md\") == \"\")\n}\n\n// Output:\n// created: Quorum is half the council.\n// amended: Quorum is two thirds of the council.\n// doc: bylaws/quorum.md\n// doc: bylaws/spend.md\n// # Foo: Bylaws \u0026 Mandates\n// [Go to DAO](/r/nt/commondao/v0:2)\n//\n// ---\n// ## bylaws/quorum\\.md\n// sha256: 20fd0dbce30ca94d0c6210a877e7b7c50c81c9b66e60265a5a9b6125805db799\n//\n//\n//\n// Quorum is two thirds of the council.\n//\n// ## bylaws/spend\\.md\n// sha256: c890333668c0f8104cf5a6607139f61a962f51077c5e2c8cb3933930ff5e9dc5\n//\n//\n//\n// Spend only by vote.\n//\n//\n// removed: true\n"},{"name":"z_22_b_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_22_b_filetest\n\n// Two bylaws amendments race from the same base: the first to execute\n// wins, the second fails cleanly (StatusFailed) and clobbers nothing.\npackage z_22_b_filetest\n\nimport (\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\t// Single-member council: one YES decides any proposal immediately\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Both amendments diff against the same (absent) document.\n\tp1 := commondao.CreateAmendBylawsProposal(cross(cur), daoID, commondao.AmendBylawsPayload(daoID, \"bylaws/a.md\", \"one\"))\n\tp2 := commondao.CreateAmendBylawsProposal(cross(cur), daoID, commondao.AmendBylawsPayload(daoID, \"bylaws/a.md\", \"two\"))\n\n\tcommondao.Vote(cross(cur), daoID, p1, pdao.ChoiceYes, \"\")\n\tcommondao.Vote(cross(cur), daoID, p2, pdao.ChoiceYes, \"\")\n\n\t// The first to execute wins and re-pins the document hash.\n\tcommondao.Execute(cross(cur), daoID, p1)\n\tprintln(\"doc after winner:\", commondao.GetBylawsDoc(daoID, \"bylaws/a.md\"))\n\n\t// The second is now stale: it fails cleanly instead of clobbering.\n\tcommondao.Execute(cross(cur), daoID, p2)\n\tprop, _ := commondao.GetView(daoID).GetProposal(p2)\n\tprintln(\"loser status:\", string(prop.Status()))\n\tprintln(\"loser reason:\", prop.StatusReason())\n\tprintln(\"doc after loser:\", commondao.GetBylawsDoc(daoID, \"bylaws/a.md\"))\n}\n\n// Output:\n// doc after winner: one\n// loser status: failed\n// loser reason: bylaws: document changed since the patch base\n// doc after loser: one\n"},{"name":"z_22_c_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_22_c_filetest\n\n// A bylaws amendment payload built against superseded document text is\n// rejected at proposal creation (stale base), so a proposal can never be\n// created against text that no longer exists.\npackage z_22_c_filetest\n\nimport (\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\t// Single-member council: one YES decides any proposal immediately\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// A payload built against the current (absent) document...\n\tstale := commondao.AmendBylawsPayload(daoID, \"bylaws/a.md\", \"one\")\n\n\t// ...is superseded when another amendment lands first.\n\tpID := commondao.CreateAmendBylawsProposal(cross(cur), daoID, commondao.AmendBylawsPayload(daoID, \"bylaws/a.md\", \"two\"))\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), daoID, pID)\n\n\t// Proposing the superseded payload is rejected at creation.\n\tcommondao.CreateAmendBylawsProposal(cross(cur), daoID, stale)\n}\n\n// Error:\n// bylaws: document changed since the patch base\n"},{"name":"z_22_d_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_22_d_filetest\n\n// Bylaws rendering is escape-safe and line-preserving: hostile markdown\n// in document text and in patch insert literals is neutralized on both\n// the proposal page (the change summary) and the bylaws page (the stored\n// document), and multi-line content keeps its line structure. A literal\n// line starting with \"- \" cannot masquerade as a deletion marker: every\n// literal line is marker-prefixed by the summary.\npackage z_22_d_filetest\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\t// Single-member council: one YES decides any proposal immediately\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\thostile := \"# Heading injection\\n\\n\u003cdiv\u003ehtml block\u003c/div\u003e\\n\\n[link](https://attacker.example)\\n\\nplain closing line\"\n\n\t// Create the document; render the ACTIVE proposal page first: the\n\t// change summary shows the hostile content escaped, line by line.\n\tpID := commondao.CreateAmendBylawsProposal(cross(cur), daoID, commondao.AmendBylawsPayload(daoID, \"bylaws/policy.md\", hostile))\n\tprintln(commondao.Render(strconv.FormatUint(daoID, 10) + \"/proposals/\" + strconv.FormatUint(pID, 10)))\n\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), daoID, pID)\n\n\t// Amend with an insert whose lines mimic summary markers.\n\tpID = commondao.CreateAmendBylawsProposal(cross(cur), daoID, commondao.AmendBylawsPayload(daoID, \"bylaws/policy.md\", hostile+\"\\n- spoofed deletion marker\\n= 9 unchanged\"))\n\tprintln(commondao.Render(strconv.FormatUint(daoID, 10) + \"/proposals/\" + strconv.FormatUint(pID, 10)))\n\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), daoID, pID)\n\n\t// The stored multi-line document renders escaped with its line\n\t// structure preserved.\n\tprintln(commondao.Render(strconv.FormatUint(daoID, 10) + \"/bylaws\"))\n}\n\n// Output:\n//\n// \u003e Voting ends on **Fri, 20 Feb 2009 11:31pm UTC**\n//\n// # #1 Add Bylaws Document: bylaws/policy\\.md\n// [Go to DAO](/r/nt/commondao/v0:2) • [Vote](/r/nt/commondao/v0$help\u0026func=Vote\u0026daoID=2\u0026proposalID=1\u0026reason=\u0026vote=)\n//\n// ---\n// ## Details\n// - Proposer: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n// - Submit Time: Fri, 13 Feb 2009 23:31:30 UTC\n// - Expected Outcome: **pending** ⏳\n// - Status: **active**\n// ## Description\n// **Document:** bylaws/policy\\.md\n//\n// ```\n// + # Heading injection\n// +\n// + \u003cdiv\u003ehtml block\u003c/div\u003e\n// +\n// + [link](https://attacker.example)\n// +\n// + plain closing line\n//\n// ```\n//\n//\n//\n//\n// \u003e Voting ends on **Fri, 20 Feb 2009 11:31pm UTC**\n//\n// # #2 Amend Bylaws Document: bylaws/policy\\.md\n// [Go to DAO](/r/nt/commondao/v0:2) • [Vote](/r/nt/commondao/v0$help\u0026func=Vote\u0026daoID=2\u0026proposalID=2\u0026reason=\u0026vote=)\n//\n// ---\n// ## Details\n// - Proposer: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n// - Submit Time: Fri, 13 Feb 2009 23:31:30 UTC\n// - Expected Outcome: **pending** ⏳\n// - Status: **active**\n// ## Description\n// **Document:** bylaws/policy\\.md\n//\n// ```\n// = 96 unchanged\n// +\n// + - spoofed deletion marker\n// + = 9 unchanged\n//\n// ```\n//\n//\n//\n// # Foo: Bylaws \u0026 Mandates\n// [Go to DAO](/r/nt/commondao/v0:2)\n//\n// ---\n// ## bylaws/policy\\.md\n// sha256: 9ce0a63e0a9255e7a01911bcd9ae19db2880c7a3af153fe7a3363a332bdf1ea6\n//\n//\n//\n// \\# Heading injection\n//\n// \u003cdiv\u003ehtml block\u003c/div\u003e\n//\n// [link](https://attacker.example)\n//\n// plain closing line\n// \\- spoofed deletion marker\n// = 9 unchanged\n"},{"name":"z_22_e_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_22_e_filetest\n\n// The mandates/ folder is reserved: the Constitution grants a council\n// self-power over its Bylaws only, so a council amendment targeting a\n// mandates path is rejected at proposal creation.\npackage z_22_e_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tpayload := commondao.AmendBylawsPayload(daoID, \"mandates/spend.md\", \"Spend only by vote.\")\n\tcommondao.CreateAmendBylawsProposal(cross(cur), daoID, payload)\n}\n\n// Error:\n// mandates are not council-amendable: they are set at creation or by an ancestor (ancestor amendment is not implemented yet)\n"},{"name":"z_23_a_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_23_a_filetest\n\n// The proposals list route: pagination, and the view/sort toggles.\n//\n// The load-bearing assertion is on page 2: the \"View:\" and \"Sort by:\"\n// links must NOT carry the current page. Switching to a view with fewer\n// pages while page=2 is pinned would land on \"invalid page number\".\npackage z_23_a_filetest\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar listPath string\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID := commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n\tlistPath = strconv.FormatUint(daoID, 10) + \"/proposals\"\n\n\t// Nine proposals over a page size of eight: two pages.\n\tfor i := 0; i \u003c 9; i++ {\n\t\tcommondao.CreateTextProposal(cross(cur), daoID, \"Proposal \"+strconv.Itoa(i+1), \"Body\", 7)\n\t}\n}\n\nfunc main() {\n\t// Page 1, then page 2 — where the toggle links must drop the page.\n\tprintln(commondao.Render(listPath))\n\tprintln(commondao.Render(listPath + \"?page=2\"))\n\n\t// Oldest-first, and the (empty) finished view.\n\tprintln(commondao.Render(listPath + \"?order=asc\"))\n\tprintln(commondao.Render(listPath + \"?finished=\"))\n}\n\n// Output:\n// # Foo: Proposals\n// [Go to DAO](/r/nt/commondao/v0:2)\n//\n// ---\n// View: [finished](/r/nt/commondao/v0:2/proposals?finished=) • Sort by: [oldest](/r/nt/commondao/v0:2/proposals?order=asc)\n//\n// **[#9 Proposal 9](/r/nt/commondao/v0:2/proposals/9)**\n// Created by g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n// Voting ends on Fri, 20 Feb 2009 11:31pm UTC\n// Votes: **0** • Status: **active** • [Vote](/r/nt/commondao/v0$help\u0026func=Vote\u0026daoID=2\u0026proposalID=9\u0026reason=\u0026vote=)\n//\n// **[#8 Proposal 8](/r/nt/commondao/v0:2/proposals/8)**\n// Created by g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n// Voting ends on Fri, 20 Feb 2009 11:31pm UTC\n// Votes: **0** • Status: **active** • [Vote](/r/nt/commondao/v0$help\u0026func=Vote\u0026daoID=2\u0026proposalID=8\u0026reason=\u0026vote=)\n//\n// **[#7 Proposal 7](/r/nt/commondao/v0:2/proposals/7)**\n// Created by g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n// Voting ends on Fri, 20 Feb 2009 11:31pm UTC\n// Votes: **0** • Status: **active** • [Vote](/r/nt/commondao/v0$help\u0026func=Vote\u0026daoID=2\u0026proposalID=7\u0026reason=\u0026vote=)\n//\n// **[#6 Proposal 6](/r/nt/commondao/v0:2/proposals/6)**\n// Created by g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n// Voting ends on Fri, 20 Feb 2009 11:31pm UTC\n// Votes: **0** • Status: **active** • [Vote](/r/nt/commondao/v0$help\u0026func=Vote\u0026daoID=2\u0026proposalID=6\u0026reason=\u0026vote=)\n//\n// **[#5 Proposal 5](/r/nt/commondao/v0:2/proposals/5)**\n// Created by g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n// Voting ends on Fri, 20 Feb 2009 11:31pm UTC\n// Votes: **0** • Status: **active** • [Vote](/r/nt/commondao/v0$help\u0026func=Vote\u0026daoID=2\u0026proposalID=5\u0026reason=\u0026vote=)\n//\n// **[#4 Proposal 4](/r/nt/commondao/v0:2/proposals/4)**\n// Created by g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n// Voting ends on Fri, 20 Feb 2009 11:31pm UTC\n// Votes: **0** • Status: **active** • [Vote](/r/nt/commondao/v0$help\u0026func=Vote\u0026daoID=2\u0026proposalID=4\u0026reason=\u0026vote=)\n//\n// **[#3 Proposal 3](/r/nt/commondao/v0:2/proposals/3)**\n// Created by g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n// Voting ends on Fri, 20 Feb 2009 11:31pm UTC\n// Votes: **0** • Status: **active** • [Vote](/r/nt/commondao/v0$help\u0026func=Vote\u0026daoID=2\u0026proposalID=3\u0026reason=\u0026vote=)\n//\n// **[#2 Proposal 2](/r/nt/commondao/v0:2/proposals/2)**\n// Created by g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n// Voting ends on Fri, 20 Feb 2009 11:31pm UTC\n// Votes: **0** • Status: **active** • [Vote](/r/nt/commondao/v0$help\u0026func=Vote\u0026daoID=2\u0026proposalID=2\u0026reason=\u0026vote=)\n//\n// ---\n// \\- | page 1 of 2 | [»](?page=2)\n// # Foo: Proposals\n// [Go to DAO](/r/nt/commondao/v0:2)\n//\n// ---\n// View: [finished](/r/nt/commondao/v0:2/proposals?finished=) • Sort by: [oldest](/r/nt/commondao/v0:2/proposals?order=asc)\n//\n// **[#1 Proposal 1](/r/nt/commondao/v0:2/proposals/1)**\n// Created by g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n// Voting ends on Fri, 20 Feb 2009 11:31pm UTC\n// Votes: **0** • Status: **active** • [Vote](/r/nt/commondao/v0$help\u0026func=Vote\u0026daoID=2\u0026proposalID=1\u0026reason=\u0026vote=)\n//\n// ---\n// [«](?page=1) | page 2 of 2 | \\-\n// # Foo: Proposals\n// [Go to DAO](/r/nt/commondao/v0:2)\n//\n// ---\n// View: [finished](/r/nt/commondao/v0:2/proposals?finished=\u0026order=asc) • Sort by: [newest](/r/nt/commondao/v0:2/proposals?order=desc)\n//\n// **[#1 Proposal 1](/r/nt/commondao/v0:2/proposals/1)**\n// Created by g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n// Voting ends on Fri, 20 Feb 2009 11:31pm UTC\n// Votes: **0** • Status: **active** • [Vote](/r/nt/commondao/v0$help\u0026func=Vote\u0026daoID=2\u0026proposalID=1\u0026reason=\u0026vote=)\n//\n// **[#2 Proposal 2](/r/nt/commondao/v0:2/proposals/2)**\n// Created by g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n// Voting ends on Fri, 20 Feb 2009 11:31pm UTC\n// Votes: **0** • Status: **active** • [Vote](/r/nt/commondao/v0$help\u0026func=Vote\u0026daoID=2\u0026proposalID=2\u0026reason=\u0026vote=)\n//\n// **[#3 Proposal 3](/r/nt/commondao/v0:2/proposals/3)**\n// Created by g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n// Voting ends on Fri, 20 Feb 2009 11:31pm UTC\n// Votes: **0** • Status: **active** • [Vote](/r/nt/commondao/v0$help\u0026func=Vote\u0026daoID=2\u0026proposalID=3\u0026reason=\u0026vote=)\n//\n// **[#4 Proposal 4](/r/nt/commondao/v0:2/proposals/4)**\n// Created by g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n// Voting ends on Fri, 20 Feb 2009 11:31pm UTC\n// Votes: **0** • Status: **active** • [Vote](/r/nt/commondao/v0$help\u0026func=Vote\u0026daoID=2\u0026proposalID=4\u0026reason=\u0026vote=)\n//\n// **[#5 Proposal 5](/r/nt/commondao/v0:2/proposals/5)**\n// Created by g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n// Voting ends on Fri, 20 Feb 2009 11:31pm UTC\n// Votes: **0** • Status: **active** • [Vote](/r/nt/commondao/v0$help\u0026func=Vote\u0026daoID=2\u0026proposalID=5\u0026reason=\u0026vote=)\n//\n// **[#6 Proposal 6](/r/nt/commondao/v0:2/proposals/6)**\n// Created by g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n// Voting ends on Fri, 20 Feb 2009 11:31pm UTC\n// Votes: **0** • Status: **active** • [Vote](/r/nt/commondao/v0$help\u0026func=Vote\u0026daoID=2\u0026proposalID=6\u0026reason=\u0026vote=)\n//\n// **[#7 Proposal 7](/r/nt/commondao/v0:2/proposals/7)**\n// Created by g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n// Voting ends on Fri, 20 Feb 2009 11:31pm UTC\n// Votes: **0** • Status: **active** • [Vote](/r/nt/commondao/v0$help\u0026func=Vote\u0026daoID=2\u0026proposalID=7\u0026reason=\u0026vote=)\n//\n// **[#8 Proposal 8](/r/nt/commondao/v0:2/proposals/8)**\n// Created by g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n// Voting ends on Fri, 20 Feb 2009 11:31pm UTC\n// Votes: **0** • Status: **active** • [Vote](/r/nt/commondao/v0$help\u0026func=Vote\u0026daoID=2\u0026proposalID=8\u0026reason=\u0026vote=)\n//\n// ---\n// \\- | page 1 of 2 | [»](?order=asc\u0026page=2)\n// # Foo: Proposals\n// [Go to DAO](/r/nt/commondao/v0:2)\n//\n// ---\n// View: [active](/r/nt/commondao/v0:2/proposals) • Sort by: [oldest](/r/nt/commondao/v0:2/proposals?finished=\u0026order=asc)\n//\n// Currently there are no finished proposals\n"},{"name":"z_23_b_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_23_b_filetest\n\n// The home index paginates over the LISTED set, not over every DAO.\n//\n// Listing is opt-in and off by default, so the newest DAOs are usually\n// unlisted. Paginating over all DAOs and filtering the window afterwards\n// drops listed DAOs off page 1 entirely (and, when a whole window is\n// unlisted, renders an empty page with no pager at all — stranding them\n// on pages the reader can no longer reach).\npackage z_23_b_filetest\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Twenty DAOs (IDs 2..21). List the ten oldest; the rest stay\n\t// unlisted, so a window taken over all DAOs would start on entries\n\t// that never render.\n\t//\n\t// The counts are deliberately far apart: 11 listed is 2 pages, 21\n\t// total is 3. Were the pager sized on daos.Size() the extra page\n\t// would exist and render empty, so ?page=3 below discriminates the\n\t// pagination total — not just the iteration source.\n\tfor i := 0; i \u003c 20; i++ {\n\t\tid := commondao.New(cross(cur), \"DAO \"+strconv.Itoa(i+1), \"Purpose\", \"\", \"\")\n\t\tif i \u003c 10 {\n\t\t\tcommondao.SetListed(cross(cur), id, true)\n\t\t}\n\t}\n}\n\nfunc main() {\n\t// Eleven listed DAOs (ten above plus the genesis DAO) over a page\n\t// size of ten: page 1 is full and page 2 holds the remainder.\n\tprintln(commondao.Render(\"\"))\n\tprintln(commondao.Render(\"?page=2\"))\n\n\t// Past the end of the LISTED set: there is no third page. Sized on\n\t// every DAO there would be, and it would render empty.\n\tprintln(commondao.Render(\"?page=3\"))\n}\n\n// Output:\n// # Common DAO\n// ---\n// This realm can be used to create CommonDAO instances based on [commondao](/p/nt/commondao/v0/) package.\n//\n// Here is a list of some of the DAOs that were created:\n//\n// - [DAO 10](/r/nt/commondao/v0:11)\n// - [DAO 9](/r/nt/commondao/v0:10)\n// - [DAO 8](/r/nt/commondao/v0:9)\n// - [DAO 7](/r/nt/commondao/v0:8)\n// - [DAO 6](/r/nt/commondao/v0:7)\n// - [DAO 5](/r/nt/commondao/v0:6)\n// - [DAO 4](/r/nt/commondao/v0:5)\n// - [DAO 3](/r/nt/commondao/v0:4)\n// - [DAO 2](/r/nt/commondao/v0:3)\n// - [DAO 1](/r/nt/commondao/v0:2)\n//\n//\n// \\- | page 1 of 2 | [»](?page=2)\n//\n//\n// # Common DAO\n// ---\n// This realm can be used to create CommonDAO instances based on [commondao](/p/nt/commondao/v0/) package.\n//\n// Here is a list of some of the DAOs that were created:\n//\n// - [Common DAO](/r/nt/commondao/v0:1)\n//\n//\n// [«](?page=1) | page 2 of 2 | \\-\n//\n//\n// # Common DAO\n// ---\n// This realm can be used to create CommonDAO instances based on [commondao](/p/nt/commondao/v0/) package.\n//\n// invalid page number\n"},{"name":"z_23_c_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_23_c_filetest\n\n// The single-vote page's failure branches.\n//\n// Each is a navigable page (header + links back), not a bare dead-end\n// string, and a missing vote distinguishes a council member who has not\n// voted yet — an ordinary state — from an account outside the electorate.\npackage z_23_c_filetest\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner     = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser      = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n\tsilent    = address(\"g1manfred47kzduec920z88wfr64ylksmdcedlf5\") // in the electorate, never votes\n\tlatecomer = address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\") // joined the council AFTER the proposal\n\toutsider  = address(\"g1qjnm0dpkcze233rljaqffny3eel3wt6wn4lql2\") // never on the council\n)\n\nvar proposalPath string\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID := commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", silent.String())\n\n\tpID := commondao.CreateTextProposal(cross(cur), daoID, \"Decision\", \"Body\", 7)\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"yes from the proposer\")\n\tproposalPath = strconv.FormatUint(daoID, 10) + \"/proposals/\" + strconv.FormatUint(pID, 10)\n\n\t// Grow the council AFTER the proposal exists, so the live council and\n\t// the proposal's frozen electorate are different sets. Both current\n\t// members must approve (two-member supermajority).\n\taddID := commondao.CreateCouncilUpdateProposal(cross(cur), daoID, latecomer.String(), \"\")\n\tcommondao.Vote(cross(cur), daoID, addID, pdao.ChoiceYes, \"\")\n\ttesting.SetRealm(testing.NewUserRealm(silent))\n\tcommondao.Vote(cross(cur), daoID, addID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), daoID, addID)\n}\n\nfunc main() {\n\t// A recorded vote.\n\tprintln(commondao.Render(proposalPath + \"/vote/\" + string(user)))\n\n\t// A council member who has not voted yet.\n\tprintln(commondao.Render(proposalPath + \"/vote/\" + string(silent)))\n\n\t// A sitting council member who joined after the snapshot: not in the\n\t// electorate, so not entitled to vote on this proposal. Testing the\n\t// live council instead of the snapshot would wrongly report this one\n\t// as merely \"has not voted yet\".\n\tprintln(commondao.Render(proposalPath + \"/vote/\" + string(latecomer)))\n\n\t// An account that was never in the electorate.\n\tprintln(commondao.Render(proposalPath + \"/vote/\" + string(outsider)))\n\n\t// A malformed address.\n\tprintln(commondao.Render(proposalPath + \"/vote/not-an-address\"))\n}\n\n// Output:\n// # Vote: Proposal #1\n// [Go to DAO](/r/nt/commondao/v0:2) • [Go to Proposal](/r/nt/commondao/v0:2/proposals/1)\n//\n// ---\n// ## Details\n// - User: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n// - Vote: YES\n// ## Reason\n// yes from the proposer\n//\n//\n// # Vote: Proposal #1\n// [Go to DAO](/r/nt/commondao/v0:2) • [Go to Proposal](/r/nt/commondao/v0:2/proposals/1)\n//\n// ---\n// This council member has not voted on this proposal yet.\n//\n//\n// # Vote: Proposal #1\n// [Go to DAO](/r/nt/commondao/v0:2) • [Go to Proposal](/r/nt/commondao/v0:2/proposals/1)\n//\n// ---\n// This account is not a member of the proposal's electorate.\n//\n//\n// # Vote: Proposal #1\n// [Go to DAO](/r/nt/commondao/v0:2) • [Go to Proposal](/r/nt/commondao/v0:2/proposals/1)\n//\n// ---\n// This account is not a member of the proposal's electorate.\n//\n//\n// # Vote: Proposal #1\n// [Go to DAO](/r/nt/commondao/v0:2) • [Go to Proposal](/r/nt/commondao/v0:2/proposals/1)\n//\n// ---\n// Invalid address.\n"},{"name":"z_23_d_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_23_d_filetest\n\n// The home index with nothing listed.\n//\n// pager.New treats a total of zero as having no valid page — including\n// page 1, which its own Picker links to — so the home route skips the\n// pager entirely when the listed set is empty. Without that, unlisting\n// every DAO turns \"?page=1\" into an error page.\npackage z_23_d_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig, genesis council\n)\n\nfunc init(cur realm) {\n\t// The genesis DAO lists itself at creation; unlist it so nothing is\n\t// listed. Only its own council member may toggle the flag.\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.SetListed(cross(cur), 1, false)\n}\n\nfunc main(cur realm) {\n\tprintln(\"listed:\", commondao.IsListed(1))\n\n\t// Both render the header alone — no list, and no \"invalid page number\".\n\tprintln(commondao.Render(\"\"))\n\tprintln(commondao.Render(\"?page=1\"))\n}\n\n// Output:\n// listed: false\n// # Common DAO\n// ---\n// This realm can be used to create CommonDAO instances based on [commondao](/p/nt/commondao/v0/) package.\n//\n//\n// # Common DAO\n// ---\n// This realm can be used to create CommonDAO instances based on [commondao](/p/nt/commondao/v0/) package.\n"},{"name":"z_23_e_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_23_e_filetest\n\n// A pager error raised inside a helper must be its own markdown block.\n//\n// renderCouncil is called mid-page by renderDAO, so its early return\n// resumes the caller, which goes straight on to write the Treasury\n// heading. Written as a bare string the error has no trailing newline\n// and the two glue together (\"invalid page number## Treasury\"),\n// destroying the H2 and taking the council table with it. This is the\n// one pager site where that is observable — everywhere else the error\n// is the last thing on the page, where the missing newline is trimmed.\npackage z_23_e_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tcommondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n}\n\nfunc main(cur realm) {\n\t// A council page far past the end.\n\tout := commondao.Render(\"2?members=99\")\n\n\t// Slice around the boundary: the error and the heading that follows\n\t// must be separate blocks.\n\ti := strings.Index(out, \"invalid page number\")\n\tprintln(out[i : i+len(\"invalid page number\\n\\n## Treasury\")])\n}\n\n// Output:\n// invalid page number\n//\n// ## Treasury\n"},{"name":"z_2_b_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_2_b_filetest\n\npackage z_2_b_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")) // @test1\n\tcommondao.New(cross(cur), \"\", \"Purpose\", \"\", \"\")\n}\n\n// Error:\n// DAO name is empty\n"},{"name":"z_2_c_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_2_c_filetest\n\npackage z_2_c_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst user = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n\nfunc main(cur realm) {\n\t// Calling with a user that was not invited\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tcommondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n}\n\n// Error:\n// unauthorized\n"},{"name":"z_2_d_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_2_d_filetest\n\npackage z_2_d_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n}\n\nfunc main(cur realm) {\n\t// A realm relaying for the invited user is rejected: it would redeem\n\t// `user`'s invite and take the council seat for itself.\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/realm_a\"))\n\n\tcommondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", string(owner))\n}\n\n// Error:\n// only a user can create a DAO\n"},{"name":"z_4_a_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_4_a_filetest\n\npackage z_4_a_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n\tname  = \"B\"\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\t// Invite a user to be able to start creating DAOs\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\t// Create a couple of DAOs\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tcommondao.New(cross(cur), \"A\", \"Purpose\", \"\", string(user))\n\tdaoID = commondao.New(cross(cur), name, \"Purpose\", \"\", \"\")\n\tcommondao.New(cross(cur), \"C\", \"Purpose\", \"\", \"\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/test\"))\n\n\tview := commondao.GetView(daoID)\n\n\tprintln(view.Name() == name)\n\tprintln(view.ID() == daoID)\n}\n\n// Output:\n// true\n// true\n"},{"name":"z_5_a_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_5_a_filetest\n\npackage z_5_a_filetest\n\nimport (\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\tdaoID uint64\n\tpID   uint64\n\tvote  pdao.VoteChoice = pdao.ChoiceYes\n)\n\nfunc init(cur realm) {\n\t// Invite a user to be able to start creating DAOs\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\t// Create a new DAO and add its direct user caller to the council\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", string(user))\n\n\t// Create a new proposal\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tpID = commondao.CreateTextProposal(cross(cur), daoID, \"Title\", \"Body\", 0)\n}\n\nfunc main(cur realm) {\n\t// User must be the caller to Vote()\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tcommondao.Vote(cross(cur), daoID, pID, vote, \"\")\n\n\tp, _ := commondao.GetView(daoID).GetProposal(pID)\n\trecord := p.VotingRecord()\n\tif record.Size() != 1 {\n\t\tpanic(\"expected a single vote\")\n\t}\n\n\tprintln(record.HasVoted(user))\n\trecord.Iterate(0, record.Size(), false, func(v pdao.Vote) bool {\n\t\tprintln(v.Choice() == vote)\n\t\treturn false\n\t})\n}\n\n// Output:\n// true\n// true\n"},{"name":"z_5_b_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_5_b_filetest\n\npackage z_5_b_filetest\n\nimport (\n\t\"testing\"\n\n\tpcommondao \"gno.land/p/nt/commondao/v0\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst user = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tcommondao.Vote(cross(cur), 404, 1, pcommondao.ChoiceYes, \"\")\n}\n\n// Error:\n// DAO not found\n"},{"name":"z_5_c_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_5_c_filetest\n\npackage z_5_c_filetest\n\nimport (\n\t\"testing\"\n\n\tpcommondao \"gno.land/p/nt/commondao/v0\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\tdaoID uint64\n\tpID   uint64\n)\n\nfunc init(cur realm) {\n\t// Invite a user to be able to start creating DAOs\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\t// Create a new DAO and add its direct user caller to the council\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", string(user))\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tpID = commondao.CreateTextProposal(cross(cur), daoID, \"Title\", \"Body\", 0)\n}\n\nfunc main(cur realm) {\n\t// The caller is not a member of the proposal's electorate\n\tcommondao.Vote(cross(cur), daoID, pID, pcommondao.ChoiceYes, \"\")\n}\n\n// Error:\n// account is not a member of the proposal's electorate\n"},{"name":"z_5_d_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_5_d_filetest\n\npackage z_5_d_filetest\n\nimport (\n\t\"testing\"\n\n\tpcommondao \"gno.land/p/nt/commondao/v0\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\t// Invite a user to be able to start creating DAOs\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\t// Create a new DAO and add its direct user caller to the council\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", string(user))\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tcommondao.Vote(cross(cur), daoID, 404, pcommondao.ChoiceYes, \"\")\n}\n\n// Error:\n// proposal not found\n"},{"name":"z_5_e_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_5_e_filetest\n\npackage z_5_e_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\tdaoID uint64\n\tpID   uint64\n)\n\nfunc init(cur realm) {\n\t// Invite a user to be able to start creating DAOs\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\t// Create a new DAO and add its direct user caller to the council\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", string(user))\n\n\t// Create a new proposal\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tpID = commondao.CreateTextProposal(cross(cur), daoID, \"Title\", \"Body\", 0)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tcommondao.Vote(cross(cur), daoID, pID, \"invalid\", \"\")\n}\n\n// Error:\n// invalid vote choice\n"},{"name":"z_6_a_filetest.gno","body":"// PKGPATH: gno.land/r/demo/test\npackage test\n\nimport (\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/p/nt/testutils/v0\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst owner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\nvar (\n\tdaoID uint64\n\tpID   uint64\n\tuser1 = testutils.TestAddress(\"user1\")\n)\n\nfunc init(cur realm) {\n\t// Invite a user to be able to start creating DAOs\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user1)\n\n\t// Create a new DAO owned by user1, with user1 as the only council member\n\ttesting.SetRealm(testing.NewUserRealm(user1))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n\n\t// Create a new proposal\n\tpID = commondao.CreateTextProposal(cross(cur), daoID, \"Title\", \"Body\", 0)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user1))\n\n\t// A single YES is a supermajority of the one member electorate,\n\t// so the proposal passes immediately and can be executed early\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), daoID, pID)\n\n\tp, found := commondao.GetView(daoID).GetProposal(pID)\n\tif !found {\n\t\tpanic(\"expected proposal to be finished\")\n\t}\n\n\tprintln(string(p.Status()))\n\tprintln(commondao.GetView(daoID).FinishedProposalsSize())\n}\n\n// Output:\n// executed\n// 1\n"},{"name":"z_6_b_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_6_b_filetest\n\npackage z_6_b_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst owner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tcommondao.Execute(cross(cur), 404, 1)\n}\n\n// Error:\n// DAO not found\n"},{"name":"z_6_c_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_6_c_filetest\n\npackage z_6_c_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\tdaoID uint64\n\tpID   uint64\n)\n\nfunc init(cur realm) {\n\t// Invite a user to be able to start creating DAOs\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\t// Create a new DAO and add its direct user caller to the council\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", string(user))\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tpID = commondao.CreateTextProposal(cross(cur), daoID, \"Title\", \"Body\", 0)\n}\n\nfunc main(cur realm) {\n\t// Executing before the voting deadline requires council membership\n\tcommondao.Execute(cross(cur), daoID, pID)\n}\n\n// Error:\n// caller is not a council member\n"},{"name":"z_6_d_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_6_d_filetest\n\npackage z_6_d_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\t// Invite a user to be able to start creating DAOs\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\t// Create a new DAO and add its direct user caller to the council\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", string(user))\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tcommondao.Execute(cross(cur), daoID, 404)\n}\n\n// Error:\n// proposal not found\n"},{"name":"z_6_f_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_6_f_filetest\n\npackage z_6_f_filetest\n\nimport (\n\t\"testing\"\n\n\tpcommondao \"gno.land/p/nt/commondao/v0\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\tdaoID uint64\n\tpID   uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\t// Council is {user, owner}\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", string(owner))\n\n\t// One day voting period; a 1Y/1N tie leaves the proposal undecided\n\tpID = commondao.CreateTextProposal(cross(cur), daoID, \"Title\", \"Body\", 1)\n\tcommondao.Vote(cross(cur), daoID, pID, pcommondao.ChoiceYes, \"\")\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Vote(cross(cur), daoID, pID, pcommondao.ChoiceNo, \"\")\n}\n\nfunc main(cur realm) {\n\t// Advance past the voting deadline (5 seconds per height)\n\ttesting.SkipHeights(17281)\n\n\t// Finalization after the deadline is permissionless: the caller is\n\t// not a council member, and undecided proposals are dismissed\n\tcommondao.Execute(cross(cur), daoID, pID)\n\n\tp, _ := commondao.GetView(daoID).GetProposal(pID)\n\tprintln(string(p.Status()))\n}\n\n// Output:\n// dismissed\n"},{"name":"z_6_g_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_6_g_filetest\n\npackage z_6_g_filetest\n\nimport (\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\tdaoID uint64\n\tpID   uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n\n\t// A passed proposal that is not executed before its deadline\n\tpID = commondao.CreateTextProposal(cross(cur), daoID, \"Title\", \"Body\", 0)\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n}\n\nfunc main(cur realm) {\n\t// Past the deadline, finalization is permissionless: a non-council\n\t// caller can execute the passed proposal.\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\ttesting.SkipHeights(121000) // 5s blocks: pass the 7 day voting deadline\n\n\tcommondao.Execute(cross(cur), daoID, pID)\n\n\tp, _ := commondao.GetView(daoID).GetProposal(pID)\n\tprintln(\"status:\", string(p.Status()))\n}\n\n// Output:\n// status: executed\n"},{"name":"z_6_h_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_6_h_filetest\n\npackage z_6_h_filetest\n\nimport (\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\tdaoID uint64\n\tpID   uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n\n\tpID = commondao.CreateTextProposal(cross(cur), daoID, \"Title\", \"Body\", 0)\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), daoID, pID)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Executed proposals leave active storage: a second execution attempt\n\t// cannot run them twice\n\tcommondao.Execute(cross(cur), daoID, pID)\n}\n\n// Error:\n// proposal not found\n"},{"name":"z_7_a_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_7_a_filetest\n\npackage z_7_a_filetest\n\nimport (\n\t\"testing\"\n\n\tpcommondao \"gno.land/p/nt/commondao/v0\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n\tname  = \"Foo\"\n)\n\nvar parentID uint64\n\nfunc init(cur realm) {\n\t// Invite a user to be able to start creating DAOs\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\t// The direct user caller must be invited. The invitation is removed after\n\t// the caller creates a DAO for the first time.\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Create the root DAO\n\tparentDAO := commondao.New(cross(cur), \"Parent DAO\", \"Purpose\", \"\", \"\")\n\tparentID = parentDAO\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// SubDAOs are created through a council proposal at simple majority\n\tpID := commondao.CreateSubDAOProposal(cross(cur), parentID, name, \"Purpose\", \"Sub charter\", string(user))\n\tcommondao.Vote(cross(cur), parentID, pID, pcommondao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), parentID, pID)\n\n\tsubID := uint64(3)\n\tview := commondao.GetView(subID)\n\n\tprintln(view.Name() == name)\n\tprintln(view.Description())\n\n\tparent, found := view.Parent()\n\tprintln(found \u0026\u0026 parent.ID() == parentID)\n\n\t// Check that SubDAO is added as a child to the parent DAO\n\tchildFound := false\n\tparent.IterateChildren(func(child pcommondao.ReadonlyCommonDAO) bool {\n\t\tchildFound = child.ID() == subID\n\t\treturn childFound\n\t})\n\tprintln(childFound)\n}\n\n// Output:\n// true\n// Sub charter\n// true\n// true\n"},{"name":"z_7_b_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_7_b_filetest\n\npackage z_7_b_filetest\n\n// Entry points must authenticate the *caller*, not the transaction origin.\n// The council here is a realm, reached by seating it through New's members\n// argument and resigning; the origin driving it is an address that is not on\n// the council. Every assertion below fails if an entry point reads\n// unsafe.OriginCaller() instead of cur.Previous().Address().\n//\n// z_7_a used to carry this coverage implicitly (its DAO was created by a code\n// realm while the origin stayed a user). New now rejects realm callers, so the\n// caller != origin case needs a DAO that a realm reached the supported way.\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\tpcommondao \"gno.land/p/nt/commondao/v0\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner  = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser   = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n\torigin = address(\"g1manfred47kzduec920z88wfr64ylksmdcedlf5\") // @moul\n\tpolicy = \"gno.land/r/demo/policy\"\n)\n\nvar (\n\tdaoID   uint64\n\tcouncil address\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\t// The invited user seats the policy realm next to itself, then resigns:\n\t// the realm is left as the DAO's sole council member.\n\tcouncil = chain.PackageAddress(policy)\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", string(council))\n\tcommondao.Resign(cross(cur), daoID)\n}\n\nfunc main(cur realm) {\n\t// origin is NOT on the council; the policy realm is.\n\ttesting.SetRealm(testing.NewUserRealm(origin))\n\ttesting.SetRealm(testing.NewCodeRealm(policy))\n\n\tprintln(commondao.GetView(daoID).Council().Has(council))\n\tprintln(commondao.GetView(daoID).Council().Has(origin))\n\n\t// Create, vote and execute as the realm. Each of these authorizes the\n\t// caller against the council, and the realm is a one-member council, so\n\t// its single YES decides.\n\tpID := commondao.CreateSubDAOProposal(cross(cur), daoID, \"Bar\", \"Purpose\", \"\", string(council))\n\tcommondao.Vote(cross(cur), daoID, pID, pcommondao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), daoID, pID)\n\n\tp, found := commondao.GetView(daoID).GetProposal(pID)\n\tprintln(found \u0026\u0026 p.Creator() == council)\n\tprintln(string(p.Status()))\n\n\tsubID := uint64(3)\n\tprintln(commondao.GetView(subID).Name())\n\n\t// A text proposal on the same DAO, also as the realm.\n\ttID := commondao.CreateTextProposal(cross(cur), daoID, \"Title\", \"Body\", 1)\n\tt, found := commondao.GetView(daoID).GetProposal(tID)\n\tprintln(found \u0026\u0026 t.Creator() == council)\n}\n\n// Output:\n// true\n// false\n// true\n// executed\n// Bar\n// true\n"},{"name":"z_7_f_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_7_f_filetest\n\npackage z_7_f_filetest\n\nimport (\n\t\"testing\"\n\n\tpcommondao \"gno.land/p/nt/commondao/v0\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar parentID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tparentID = commondao.New(cross(cur), \"Parent\", \"Purpose\", \"\", \"\")\n\n\t// Dissolve the parent (sole council member passes the supermajority)\n\tpID := commondao.CreateDissolutionProposal(cross(cur), parentID, owner)\n\tcommondao.Vote(cross(cur), parentID, pID, pcommondao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), parentID, pID)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// A sub-DAO grafted onto a dissolved parent could never be dissolved\n\t// itself, so creation must be rejected: dissolved DAOs reject proposals.\n\tcommondao.CreateSubDAOProposal(cross(cur), parentID, \"Child\", \"Purpose\", \"\", string(user))\n}\n\n// Error:\n// DAO is deleted\n"},{"name":"z_7_g_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_7_g_filetest\n\npackage z_7_g_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\n// Sub-DAO creation passes by SIMPLE majority (spec :1504), not\n// supermajority. A 5-member council splitting 3 YES / 2 NO clears a simple\n// majority (2*3 \u003e 5) but not a supermajority (3*3 \u003c 2*5), so under the\n// correct Simple threshold the sub-DAO is created; a mutated Super threshold\n// would dismiss it. Mirror of z_11_h (spend = Super).\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n\tmoul  = address(\"g1manfred47kzduec920z88wfr64ylksmdcedlf5\") // @moul\n\tmF    = address(\"g1cyh9qm4y43w38gfk77qxczrhqj2asdr8l7uk54\")\n\tmG    = address(\"g1x95kxqpwvqjf98qpkvsay7tppkh6zs9fr7wwda\")\n)\n\nvar (\n\trootID uint64\n\tpID    uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tmembers := strings.Join([]string{owner.String(), moul.String(), mF.String(), mG.String()}, \"\\n\")\n\trootID = commondao.New(cross(cur), \"Root\", \"Purpose\", \"\", members)\n\n\tpID = commondao.CreateSubDAOProposal(cross(cur), rootID, \"Sub\", \"Purpose\", \"\", string(user))\n\n\t// 3 YES / 2 NO from distinct members; NO first so the simple-majority\n\t// pass lands on the last (3rd YES) vote, never on a decided proposal.\n\ttesting.SetRealm(testing.NewUserRealm(mF))\n\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceNo, \"\")\n\ttesting.SetRealm(testing.NewUserRealm(mG))\n\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceNo, \"\")\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n\ttesting.SetRealm(testing.NewUserRealm(moul))\n\tcommondao.Vote(cross(cur), rootID, pID, pdao.ChoiceYes, \"\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\ttesting.SkipHeights(121000) // pass the 7 day voting deadline\n\n\tcommondao.Execute(cross(cur), rootID, pID)\n\n\tp, _ := commondao.GetView(rootID).GetProposal(pID)\n\tprintln(\"status:\", string(p.Status()))\n\tprintln(\"children:\", commondao.GetView(rootID).ChildrenCount())\n}\n\n// Output:\n// status: executed\n// children: 1\n"},{"name":"z_9_a_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_9_a_filetest\n\npackage z_9_a_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\t// Council is {user, owner}\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", string(owner))\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tcommondao.Resign(cross(cur), daoID)\n\n\tcouncil := commondao.GetView(daoID).Council()\n\tprintln(council.Size())\n\tprintln(council.Has(user))\n\tprintln(council.Has(owner))\n}\n\n// Output:\n// 1\n// false\n// true\n"},{"name":"z_9_b_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_9_b_filetest\n\npackage z_9_b_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\t// Council is {user} only\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tcommondao.Resign(cross(cur), daoID)\n}\n\n// Error:\n// council update would remove every council member\n"},{"name":"z_9_c_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_9_c_filetest\n\npackage z_9_c_filetest\n\nimport (\n\t\"testing\"\n\n\tpcommondao \"gno.land/p/nt/commondao/v0\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", string(owner))\n\n\t// Dissolve the DAO through a proposal\n\tpID := commondao.CreateDissolutionProposal(cross(cur), daoID, owner)\n\tcommondao.Vote(cross(cur), daoID, pID, pcommondao.ChoiceYes, \"\")\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Vote(cross(cur), daoID, pID, pcommondao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), daoID, pID)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tcommondao.Resign(cross(cur), daoID)\n}\n\n// Error:\n// DAO is deleted\n"},{"name":"z_9_d_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_9_d_filetest\n\npackage z_9_d_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\tdaoID uint64\n\tpID   uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", string(owner))\n\n\tpID = commondao.CreateTextProposal(cross(cur), daoID, \"Title\", \"Body\", 0)\n}\n\nfunc main(cur realm) {\n\t// Owner is a council member but not the proposal creator\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tcommondao.Withdraw(cross(cur), daoID, pID)\n}\n\n// Error:\n// only the proposal creator can withdraw it\n"},{"name":"z_9_e_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_9_e_filetest\n\npackage z_9_e_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\tdaoID uint64\n\tpID   uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n\n\tpID = commondao.CreateTextProposal(cross(cur), daoID, \"Title\", \"Body\", 0)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tcommondao.Withdraw(cross(cur), daoID, pID)\n\n\tp, found := commondao.GetView(daoID).GetProposal(pID)\n\tprintln(found)\n\tprintln(string(p.Status()))\n\tprintln(commondao.GetView(daoID).ActiveProposalsSize())\n}\n\n// Output:\n// true\n// withdrawn\n// 0\n"},{"name":"z_9_f_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/z_9_f_filetest\n\npackage z_9_f_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\n// The Execute-deadline re-tally uses the proposal's electorate SNAPSHOT, not\n// the live council. A 3-member proposal with a single YES is undecided\n// (supermajority needs 3*1 \u003e= 2*3). If two members resign after the proposal\n// is created, the live council shrinks to 1 (where 1 YES would pass), but the\n// snapshot electorate stays 3, so at the deadline the proposal is DISMISSED.\n// A mutant tallying the live council at Execute would instead pass it.\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n\tmoul  = address(\"g1manfred47kzduec920z88wfr64ylksmdcedlf5\") // @moul\n)\n\nvar (\n\tdaoID uint64\n\tpID   uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tmembers := strings.Join([]string{owner.String(), moul.String()}, \"\\n\")\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", members)\n\n\t// Snapshot electorate = 3 members; a single YES leaves it undecided.\n\tpID = commondao.CreateTextProposal(cross(cur), daoID, \"Title\", \"Body\", 0)\n\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\n\t// Two members resign: the live council shrinks to {user}, but the\n\t// proposal's electorate snapshot is unchanged.\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Resign(cross(cur), daoID)\n\ttesting.SetRealm(testing.NewUserRealm(moul))\n\tcommondao.Resign(cross(cur), daoID)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\ttesting.SkipHeights(121000) // pass the 7 day deadline\n\n\tcommondao.Execute(cross(cur), daoID, pID)\n\n\tp, _ := commondao.GetView(daoID).GetProposal(pID)\n\tprintln(\"status:\", string(p.Status()))\n\tprintln(\"live council:\", commondao.GetView(daoID).Council().Size())\n\tprintln(\"electorate:\", p.Electorate().Size())\n}\n\n// Output:\n// status: dismissed\n// live council: 1\n// electorate: 3\n"},{"name":"zp_0_a_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/zp_0_a_filetest\n\npackage zp_0_a_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner         = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser          = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n\tnewMembers    = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\\ng147ah9520z0r6jh9mjr6c75rv6l8aypzvcd3f7d\"\n\tremoveMembers = \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", string(user))\n\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tpID := commondao.CreateCouncilUpdateProposal(cross(cur), daoID, newMembers, removeMembers)\n\n\tp, found := commondao.GetView(daoID).GetProposal(pID)\n\tif !found {\n\t\tpanic(\"expected proposal to be created\")\n\t}\n\n\tprintln(string(p.Status()))\n\tprintln(p.Creator() == user)\n\tprintln(p.Title() == \"Council Update\")\n\tprintln(\"\")\n\tprintln(p.Body())\n}\n\n// Output:\n// active\n// true\n// true\n//\n// **Council Members to Add:**\n// - g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\n// - g147ah9520z0r6jh9mjr6c75rv6l8aypzvcd3f7d\n//\n//\n// **Council Members to Remove:**\n// - g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n"},{"name":"zp_0_b_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/zp_0_b_filetest\n\npackage zp_0_b_filetest\n\nimport (\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nfunc main(cur realm) {\n\tcommondao.CreateCouncilUpdateProposal(cross(cur), 404, \"\", \"\")\n}\n\n// Error:\n// DAO not found\n"},{"name":"zp_0_d_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/zp_0_d_filetest\n\npackage zp_0_d_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\t// Invite a user to be able to start creating DAOs\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\t// The direct user caller must be invited. The invitation is removed after\n\t// the caller creates a DAO for the first time.\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Create root DAO with a subDAO\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"))\n\n\tcommondao.CreateCouncilUpdateProposal(cross(cur), daoID, \"\", \"\")\n}\n\n// Error:\n// caller is not a council member\n"},{"name":"zp_0_e_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/zp_0_e_filetest\n\npackage zp_0_e_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\t// Invite a user to be able to start creating DAOs\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\t// The direct user caller must be invited. The invitation is removed after\n\t// the caller creates a DAO for the first time.\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Create root DAO with a subDAO\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", string(user))\n\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tcommondao.CreateCouncilUpdateProposal(cross(cur), daoID, \"not-an-address\", \"\")\n}\n\n// Error:\n// invalid address: not-an-address\n"},{"name":"zp_0_g_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/zp_0_g_filetest\n\npackage zp_0_g_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", string(user))\n\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tcommondao.CreateCouncilUpdateProposal(cross(cur), daoID, \"\", \"\")\n}\n\n// Error:\n// no council members were specified to be added or removed\n"},{"name":"zp_0_h_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/zp_0_h_filetest\n\npackage zp_0_h_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", string(user))\n\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tpID := commondao.CreateCouncilUpdateProposal(cross(cur), daoID, user.String(), \"\")\n\n\t// Adding an existing member is an idempotent no-op, not an error\n\tp, found := commondao.GetView(daoID).GetProposal(pID)\n\tprintln(found)\n\tprintln(string(p.Status()))\n}\n\n// Output:\n// true\n// active\n"},{"name":"zp_0_i_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/zp_0_i_filetest\n\npackage zp_0_i_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", string(user))\n\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tpID := commondao.CreateCouncilUpdateProposal(cross(cur), daoID, \"\", owner.String())\n\n\t// Removing a non-member is an idempotent no-op, not an error\n\tp, found := commondao.GetView(daoID).GetProposal(pID)\n\tprintln(found)\n\tprintln(string(p.Status()))\n}\n\n// Output:\n// true\n// active\n"},{"name":"zp_1_a_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/zp_1_a_filetest\n\npackage zp_1_a_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner      = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser       = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n\ttitle      = \"General Proposal\"\n\tbody       = \"Foo Bar\"\n\tvotingDays = uint8(1)\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", string(user)+\"\\n\"+string(owner))\n\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tpID := commondao.CreateTextProposal(cross(cur), daoID, title, body, votingDays)\n\n\tp, found := commondao.GetView(daoID).GetProposal(pID)\n\tif !found {\n\t\tpanic(\"expected proposal to be created\")\n\t}\n\n\tprintln(string(p.Status()))\n\tprintln(p.Creator() == user)\n\tprintln(p.Title() == title)\n\tprintln(p.VotingDeadline())\n\tprintln(p.Body())\n}\n\n// Output:\n// active\n// true\n// true\n// 2009-02-14 23:31:30 +0000 UTC\n// Foo Bar\n"},{"name":"zp_1_b_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/zp_1_b_filetest\n\npackage zp_1_b_filetest\n\nimport (\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nfunc main(cur realm) {\n\tcommondao.CreateTextProposal(cross(cur), 404, \"\", \"\", 0)\n}\n\n// Error:\n// DAO not found\n"},{"name":"zp_1_d_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/zp_1_d_filetest\n\npackage zp_1_d_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", string(user)+\"\\n\"+string(owner))\n\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tcommondao.CreateTextProposal(cross(cur), daoID, \"\", \"\", 31)\n}\n\n// Error:\n// maximum proposal voting period is 30 days\n"},{"name":"zp_1_e_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/zp_1_e_filetest\n\npackage zp_1_e_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tcommondao.CreateTextProposal(cross(cur), daoID, \"\", \"\", 1)\n}\n\n// Error:\n// caller is not a council member\n"},{"name":"zp_2_a_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/zp_2_a_filetest\n\npackage zp_2_a_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner   = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser    = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n\tname    = \"Foo SubDAO\"\n\tmembers = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\\ng1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", string(user))\n\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tpID := commondao.CreateSubDAOProposal(cross(cur), daoID, name, \"Purpose\", \"\", members)\n\n\tp, found := commondao.GetView(daoID).GetProposal(pID)\n\tif !found {\n\t\tpanic(\"expected proposal to be created\")\n\t}\n\n\tprintln(string(p.Status()))\n\tprintln(p.Creator() == user)\n\tprintln(p.Title() == (\"New SubDAO: \" + name))\n\tprintln(p.VotingDeadline())\n\tprintln(p.Body())\n}\n\n// Output:\n// active\n// true\n// true\n// 2009-02-20 23:31:30 +0000 UTC\n// **Parent DAO:**\n// [Foo](/r/nt/commondao/v0:2)\n//\n// **SubDAO Name:**\n// Foo SubDAO\n//\n// **SubDAO Purpose:**\n// Purpose\n//\n// **Council Members:**\n// - g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\n// - g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n"},{"name":"zp_2_b_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/zp_2_b_filetest\n\npackage zp_2_b_filetest\n\nimport (\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nfunc main(cur realm) {\n\tcommondao.CreateSubDAOProposal(cross(cur), 404, \"\", \"Purpose\", \"\", \"\")\n}\n\n// Error:\n// DAO not found\n"},{"name":"zp_2_d_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/zp_2_d_filetest\n\npackage zp_2_d_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n\n}\n\nfunc main(cur realm) {\n\t// Call with a user that is not a member of the DAO\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tcommondao.CreateSubDAOProposal(cross(cur), daoID, \"\", \"Purpose\", \"\", \"\")\n}\n\n// Error:\n// caller is not a council member\n"},{"name":"zp_2_e_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/zp_2_e_filetest\n\npackage zp_2_e_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", string(user))\n\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tcommondao.CreateSubDAOProposal(cross(cur), daoID, \"Name\", \"Purpose\", \"\", \"INVALID\")\n}\n\n// Error:\n// invalid address: INVALID\n"},{"name":"zp_2_f_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/zp_2_f_filetest\n\npackage zp_2_f_filetest\n\nimport (\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n\tname  = \"Foo\"\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), daoID, name, \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), daoID, pID)\n\t}\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tcommondao.CreateSubDAOProposal(cross(cur), daoID, name, \"Purpose\", \"\", string(user))\n}\n\n// Error:\n// a SubDAO with the same name already exists\n"},{"name":"zp_2_g_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/zp_2_g_filetest\n\npackage zp_2_g_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", string(user))\n\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tcommondao.CreateSubDAOProposal(cross(cur), daoID, \"\", \"Purpose\", \"\", \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n}\n\n// Error:\n// DAO name is empty\n"},{"name":"zp_2_h_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/zp_2_h_filetest\n\npackage zp_2_h_filetest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\tdaoID uint64\n\tname  = strings.Repeat(\"A\", 61)\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", string(user))\n\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tcommondao.CreateSubDAOProposal(cross(cur), daoID, name, \"Purpose\", \"\", \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n}\n\n// Error:\n// DAO name is too long, max length is 60 characters\n"},{"name":"zp_3_a_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/zp_3_a_filetest\n\npackage zp_3_a_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", string(user))\n\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tpID := commondao.CreateDissolutionProposal(cross(cur), daoID, owner)\n\n\tp, found := commondao.GetView(daoID).GetProposal(pID)\n\tif !found {\n\t\tpanic(\"expected proposal to be created\")\n\t}\n\n\tprintln(string(p.Status()))\n\tprintln(p.Creator() == user)\n\tprintln(p.Title() == (\"Dissolve DAO: \" + commondao.GetView(daoID).Name()))\n\tprintln(p.VotingDeadline())\n\tprintln(p.Body())\n}\n\n// Output:\n// active\n// true\n// true\n// 2009-02-20 23:31:30 +0000 UTC\n// **DAO:**\n// [Foo](/r/nt/commondao/v0:2)\n//\n// **Sweep destination:**\n// g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\n"},{"name":"zp_3_b_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/zp_3_b_filetest\n\npackage zp_3_b_filetest\n\nimport (\n\t\"testing\"\n\n\tpdao \"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar (\n\tdaoID uint64\n\tsubID uint64\n)\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n\n\t// Create a SubDAO to be dissolved\n\t{\n\t\tpID := commondao.CreateSubDAOProposal(cross(cur), daoID, \"Bar\", \"Purpose\", \"\", string(user))\n\t\tcommondao.Vote(cross(cur), daoID, pID, pdao.ChoiceYes, \"\")\n\t\tcommondao.Execute(cross(cur), daoID, pID)\n\t}\n\tsubID = 3\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tpID := commondao.CreateDissolutionProposal(cross(cur), subID, \"\")\n\n\tp, found := commondao.GetView(daoID).GetProposal(pID)\n\tif !found {\n\t\tpanic(\"expected proposal to be created in the parent DAO\")\n\t}\n\n\t// Make sure proposal doesn't exists in the SubDAO\n\t_, foundInSub := commondao.GetView(subID).GetProposal(pID)\n\tprintln(!foundInSub)\n\n\tprintln(string(p.Status()))\n\tprintln(p.Creator() == user)\n\tprintln(p.Title() == (\"Dissolve DAO: \" + commondao.GetView(subID).Name()))\n\tprintln(p.VotingDeadline())\n\tprintln(p.Body())\n}\n\n// Output:\n// true\n// active\n// true\n// true\n// 2009-02-20 23:31:30 +0000 UTC\n// **DAO:**\n// [Bar](/r/nt/commondao/v0:3)\n"},{"name":"zp_3_c_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/zp_3_c_filetest\n\npackage zp_3_c_filetest\n\nimport (\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nfunc main(cur realm) {\n\tcommondao.CreateDissolutionProposal(cross(cur), 404, \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\")\n}\n\n// Error:\n// DAO not found\n"},{"name":"zp_3_e_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/zp_3_e_filetest\n\npackage zp_3_e_filetest\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tcommondao.CreateDissolutionProposal(cross(cur), daoID, owner)\n}\n\n// Error:\n// caller is not a council member\n"},{"name":"zp_3_f_filetest.gno","body":"// PKGPATH: gno.land/r/nt/commondao/v0/filetests/zp_3_f_filetest\n\npackage zp_3_f_filetest\n\nimport (\n\t\"testing\"\n\n\tpcommondao \"gno.land/p/nt/commondao/v0\"\n\n\t\"gno.land/r/nt/commondao/v0\"\n)\n\nconst (\n\towner = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tuser  = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // @test1\n)\n\nvar daoID uint64\n\nfunc init(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tcommondao.Invite(cross(cur), user)\n\n\t// Create a DAO owned by user, with user as the only council member\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tdaoID = commondao.New(cross(cur), \"Foo\", \"Purpose\", \"\", \"\")\n\n\t// Dissolve the DAO through a proposal decided by supermajority\n\tpID := commondao.CreateDissolutionProposal(cross(cur), daoID, owner)\n\tcommondao.Vote(cross(cur), daoID, pID, pcommondao.ChoiceYes, \"\")\n\tcommondao.Execute(cross(cur), daoID, pID)\n}\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// A dissolved DAO rejects new proposals\n\tcommondao.CreateDissolutionProposal(cross(cur), daoID, owner)\n}\n\n// Error:\n// DAO is deleted\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"Sp3WHsTgtYFsfLqcB2VNCRVg/K1g+8f1IXkvJLpFRxUkshIiZoddI5uUEDytMLkXjCpX3AwsGZN6lPhCU6X+iw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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/v0\"\n\t\"gno.land/p/moul/helplink/v0\"\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/v0\"\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":"cla_test.gno","body":"package cla\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/moul/addrset/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\n// cur is a zero-value realm used as a placeholder when forwarding to\n// uassert/urequire dispatch helpers that gained an `rlm realm` param.\n// These tests pass `func()` callbacks (no crossing inside the callback),\n// so rlm is ignored — a nil realm here is safe.\nvar cur realm\n\nconst (\n\ttestHash1 = \"abc123def456\"\n\ttestHash2 = \"xyz789uvw012\"\n\ttestUser1 = \"g1user1address1234567890\"\n\ttestUser2 = \"g1user2address0987654321\"\n)\n\nfunc resetState() {\n\tsignatures = addrset.Set{}\n\trequiredHash = \"\"\n\tclaURL = \"\"\n}\n\nfunc TestSign(cur realm, t *testing.T) {\n\tresetState()\n\n\tsetRequiredHash(testHash1)\n\n\ttesting.SetRealm(testing.NewUserRealm(testUser1))\n\tSign(cross(cur), testHash1)\n\n\tuassert.True(t, HasValidSignature(address(testUser1)))\n}\n\nfunc TestSign_WrongHash(cur realm, t *testing.T) {\n\tresetState()\n\n\tsetRequiredHash(testHash1)\n\n\ttesting.SetRealm(testing.NewUserRealm(testUser1))\n\tuassert.AbortsWithMessage(t, cur, \"hash does not match required CLA hash\", func() {\n\t\tSign(cross(cur), testHash2)\n\t})\n\n\tuassert.False(t, HasValidSignature(address(testUser1)))\n}\n\nfunc TestHasValidSignature_Disabled(t *testing.T) {\n\tresetState()\n\n\tuassert.Equal(t, \"\", requiredHash)\n\tuassert.True(t, HasValidSignature(address(testUser1)))\n\tuassert.True(t, HasValidSignature(address(testUser2)))\n}\n\nfunc TestHasValidSignature_Valid(cur realm, t *testing.T) {\n\tresetState()\n\n\tsetRequiredHash(testHash1)\n\n\ttesting.SetRealm(testing.NewUserRealm(testUser1))\n\tSign(cross(cur), testHash1)\n\n\tuassert.True(t, HasValidSignature(address(testUser1)))\n}\n\nfunc TestHasValidSignature_NotSigned(t *testing.T) {\n\tresetState()\n\n\tsetRequiredHash(testHash1)\n\n\tuassert.False(t, HasValidSignature(address(testUser1)))\n}\n\nfunc TestSetRequiredHash_ResetsSignatures(cur realm, t *testing.T) {\n\tresetState()\n\n\tsetRequiredHash(testHash1)\n\n\ttesting.SetRealm(testing.NewUserRealm(testUser1))\n\tSign(cross(cur), testHash1)\n\tuassert.True(t, HasValidSignature(address(testUser1)))\n\tuassert.Equal(t, 1, signatures.Size())\n\n\t// Update hash - should reset signatures\n\tsetRequiredHash(testHash2)\n\n\tuassert.False(t, HasValidSignature(address(testUser1)))\n\tuassert.Equal(t, 0, signatures.Size())\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/v0\"\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/moul/mdtable/v0\"\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"GB/72/4xjMC5t0pghKx3mA5x7CBGCVYki1u6qC4vWLlv3C1DcB2zl5cRb7K6A+Y+CZxD7WCGMJmbmilCppJcVA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l","package":{"name":"namereg","path":"gno.land/r/sys/namereg/v0","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 V0: Pause\", \"\", e)\n\t}\n\n\treturn dao.NewProposalRequest(\"User Registry V0: 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 V0: 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 V0: 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 V0: 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":"blacklist_test.gno","body":"package namereg\n\nimport \"testing\"\n\n// TestReservedNamesSorted enforces lexicographic ordering on reservedNames.\n// The list is intended to be auditable at a glance and to grow over time;\n// keeping it sorted makes diffs minimal and makes manual scanning for a\n// given entry feasible. If this fails after a maintainer adds an entry,\n// re-sort the slice rather than weakening the test.\nfunc TestReservedNamesSorted(cur realm, t *testing.T) {\n\tfor i := 1; i \u003c len(reservedNames); i++ {\n\t\tif reservedNames[i-1] \u003e= reservedNames[i] {\n\t\t\tt.Errorf(\n\t\t\t\t\"reservedNames not sorted at index %d: %q should come after %q\",\n\t\t\t\ti, reservedNames[i-1], reservedNames[i],\n\t\t\t)\n\t\t}\n\t}\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/v0-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/v0 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/v0 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":"canonical_test.gno","body":"package namereg\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\n\tsusers \"gno.land/r/sys/users\"\n)\n\n// Canonicalize is now a delegating shim to susers.Canonicalize; verify\n// the local export and the upstream produce the same output for the\n// rules namereg/v0 cares about.\nfunc TestCanonicalize_DelegatesToSusers(cur realm, t *testing.T) {\n\tcases := []string{\n\t\t\"\",\n\t\t\"a\",\n\t\t\"alice\",\n\t\t\"vitalik\",\n\t\t\"vital1k\",\n\t\t\"balloon\",\n\t\t\"already-canonical\",\n\t\t\"xyz123\",\n\t\t\"nym-foolbar000\",\n\t\t\"nym-foolbar001\",\n\t}\n\tfor _, in := range cases {\n\t\tuassert.Equal(t, susers.Canonicalize(in), Canonicalize(in),\n\t\t\t\"namereg.Canonicalize must match susers.Canonicalize\")\n\t}\n}\n\nfunc TestReservedSet_BuiltFromBlacklist(cur realm, t *testing.T) {\n\t// Sanity: every entry from reservedNames AND its `+s` form must be\n\t// in the canonicalized lookup.\n\tfor _, n := range reservedNames {\n\t\t_, gotBare := reservedSet[Canonicalize(n)]\n\t\tuassert.True(t, gotBare,\n\t\t\t\"reservedSet missing canonical(%q)\", n)\n\n\t\t_, gotPlural := reservedSet[Canonicalize(n+\"s\")]\n\t\tuassert.True(t, gotPlural,\n\t\t\t\"reservedSet missing canonical(%q+s)\", n)\n\t}\n}\n\nfunc TestIsReserved_LiteralAndCanonical(cur realm, t *testing.T) {\n\t// Literal entry must be reserved.\n\tuassert.True(t, IsReserved(\"admin\"), \"admin should be reserved\")\n\n\t// `blogs` covered via `blog`+s rule (blog is in reservedNames).\n\tuassert.True(t, IsReserved(\"blogs\"), \"blogs covered by blog+s\")\n\n\t// l-substituted: `blog` canonicalizes to `biog`. Both forms blocked\n\t// because we canonicalize the candidate before lookup.\n\tuassert.True(t, IsReserved(\"biog\"), \"canonical(blog)=biog also blocked\")\n\n\t// 1-substituted under the new susers.Canonicalize rules: `1` → `i`.\n\t// `b1og` canonicalizes to `biog`, same as `blog`. Confirms the shim\n\t// picks up the broader rule set.\n\tuassert.True(t, IsReserved(\"b1og\"), \"1→i variant also blocked\")\n\n\t// Non-reserved stem should not be flagged.\n\tuassert.False(t, IsReserved(\"zulufoxtrot\"), \"zulufoxtrot is not reserved\")\n\n\t// Identity passthrough.\n\tuassert.True(t, IsReserved(\"tendermint\"))\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/v0\"\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/v0\"))\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/v0\"\n\t\"gno.land/p/moul/realmpath/v0\"\n\t\"gno.land/p/moul/txlink/v0\"\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 (v0). 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 V0, 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/v0\"\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"},{"name":"users_test.gno","body":"package namereg\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n\n\tsusers \"gno.land/r/sys/users\"\n)\n\n// cur is a zero-value realm used as a placeholder when forwarding to\n// uassert/urequire dispatch helpers that gained an `rlm realm` param.\n// These tests pass `func()` callbacks (no crossing inside the callback),\n// so rlm is ignored — a nil realm here is safe.\nvar cur realm\n\nfunc init(cur realm) {\n\t// Unit tests run with DefaultHeight=123, so the production init() in init.gno\n\t// (guarded on height==0) is a no-op in tests. Temporarily reset height so we\n\t// can whitelist this realm as a controller for testing.\n\ttesting.SetHeight(0)\n\tsusers.AddControllerAtGenesis(cross(cur), chain.PackageAddress(\"gno.land/r/sys/namereg/v0\"))\n\ttesting.SetHeight(123)\n}\n\nfunc TestRegister_Valid(cur realm, t *testing.T) {\n\t// Stems chosen with no `l` characters to keep canonical forms identical\n\t// to the originals — avoids accidental cross-subtest collisions with\n\t// other tests that may register l-bearing names.\n\tvalidUsernames := []string{\n\t\t\"nym-bravo123\",         // 5-char stem (minimum)\n\t\t\"nym-tango456\",         // 5-char stem\n\t\t\"nym-victor789\",        // 6-char stem\n\t\t\"nym-romeoecho012\",     // 10-char stem\n\t\t\"nym-mikefoxhote345\",   // 11-char stem\n\t\t\"nym-mikefoxhotenp678\", // 13-char stem (maximum)\n\t}\n\n\tfor _, username := range validUsernames {\n\t\taddr := testutils.TestAddress(username)\n\n\t\ttesting.SetRealm(testing.NewUserRealm(addr))\n\t\ttesting.SetOriginCaller(addr)\n\t\ttesting.SetOriginSend(chain.NewCoins(chain.NewCoin(\"ugnot\", registerPrice)))\n\n\t\turequire.NotPanics(t, cur, func() {\n\t\t\tRegister(cross(cur), username)\n\t\t})\n\t}\n}\n\nfunc TestRegister_Free(cur realm, t *testing.T) {\n\toldPrice := registerPrice\n\tdefer func() { registerPrice = oldPrice }()\n\tregisterPrice = 0\n\n\taddr := testutils.TestAddress(\"free-payer\")\n\n\ttesting.SetRealm(testing.NewUserRealm(addr))\n\ttesting.SetOriginCaller(addr)\n\n\turequire.NotPanics(t, cur, func() {\n\t\tRegister(cross(cur), \"nym-foxtrot456\")\n\t})\n}\n\nfunc TestRegister_InvalidFormat(cur realm, t *testing.T) {\n\ttesting.SetOriginSend(chain.NewCoins(chain.NewCoin(\"ugnot\", registerPrice)))\n\ttesting.SetRealm(testing.NewUserRealm(testutils.TestAddress(\"fmt-payer\")))\n\n\tcases := []string{\n\t\t\"\",                      // empty\n\t\t\"    \",                  // whitespace\n\t\t\"alice123\",              // missing nym- prefix\n\t\t\"usr-alice123\",          // wrong prefix\n\t\t\"nym-\",                  // prefix only\n\t\t\"nym-abc123\",            // stem too short (3 chars, need ≥5)\n\t\t\"nym-abcd123\",           // stem too short (4 chars)\n\t\t\"nym-abcdefghijklmn123\", // stem too long (14 chars, need ≤13)\n\t\t\"nym-Alice123\",          // uppercase in stem\n\t\t\"nym-al1ce123\",          // digit in stem\n\t\t\"nym-alice12\",           // only 2 trailing digits\n\t\t\"nym-alice1234\",         // 4 trailing digits (extra digit in stem too long)\n\t\t\"nym-alice\u0026#($)123\",     // special chars in stem\n\t}\n\n\tfor _, username := range cases {\n\t\tuassert.AbortsWithMessage(t, cur, ErrInvalidFormat.Error(), func() {\n\t\t\tRegister(cross(cur), username)\n\t\t})\n\t}\n}\n\nfunc TestRegister_ReservedPrefix(cur realm, t *testing.T) {\n\ttesting.SetOriginSend(chain.NewCoins(chain.NewCoin(\"ugnot\", registerPrice)))\n\ttesting.SetRealm(testing.NewUserRealm(testutils.TestAddress(\"prefix-payer\")))\n\n\tcases := []string{\n\t\t\"nym-gnoblah123\",    // gno prefix\n\t\t\"nym-gnomeland456\",  // gno prefix\n\t\t\"nym-glasgow012\",    // gl prefix (most-confusable with bech32 g1...)\n\t\t\"nym-atomic123\",     // atom prefix\n\t\t\"nym-atonex123\",     // atone prefix\n\t\t\"nym-photons456\",    // photon prefix\n\t\t\"nym-cosmoswide789\", // cosmos prefix\n\t}\n\n\tfor _, username := range cases {\n\t\tuassert.AbortsWithMessage(t, cur, ErrReservedPrefix.Error(), func() {\n\t\t\tRegister(cross(cur), username)\n\t\t})\n\t}\n}\n\n// gi is intentionally NOT in reservedPrefixes (i is visually distinct\n// enough from 1/l that legitimate gi* names should be registerable).\n// Phishing protection for the gi/gl visual class is enforced by\n// canonical-collision detection in r/sys/users — see\n// TestRegister_CanonicalCollision.\nfunc TestRegister_GiPrefix_Allowed(cur realm, t *testing.T) {\n\taddr := testutils.TestAddress(\"gi-prefix-allowed\")\n\ttesting.SetRealm(testing.NewUserRealm(addr))\n\ttesting.SetOriginCaller(addr)\n\ttesting.SetOriginSend(chain.NewCoins(chain.NewCoin(\"ugnot\", registerPrice)))\n\n\turequire.NotPanics(t, cur, func() {\n\t\tRegister(cross(cur), \"nym-gillette789\")\n\t})\n}\n\nfunc TestRegister_Blacklisted(cur realm, t *testing.T) {\n\ttesting.SetOriginSend(chain.NewCoins(chain.NewCoin(\"ugnot\", registerPrice)))\n\ttesting.SetRealm(testing.NewUserRealm(testutils.TestAddress(\"blk-payer\")))\n\n\tcases := []string{\n\t\t// `admin` is in reservedNames; with stem 5 chars it now matches the regex.\n\t\t\"nym-admin123\",\n\t\t// `support` is reserved (7 chars).\n\t\t\"nym-support456\",\n\t\t// `bitcoin` is reserved (7 chars).\n\t\t\"nym-bitcoin789\",\n\t\t// Implicit s-suffix expansion: `blog` is reserved, so `blogs` (stem\n\t\t// = blog+s) must also be rejected.\n\t\t\"nym-blogs012\",\n\t\t// Canonical match: `setting` is reserved → canonical \"setting\" (no l).\n\t\t// Try `setting` directly via 7-char stem.\n\t\t\"nym-setting345\",\n\t\t// Canonical match: `news` is reserved via `new+s` rule; canonical\n\t\t// of `news` is `news`. With a 5-char stem this won't reach here\n\t\t// (stem must be ≥5). Use a longer reserved name with l→i interaction:\n\t\t// `tendermint` (10 chars, no l) → canonical = tendermint → blacklisted.\n\t\t\"nym-tendermint678\",\n\t}\n\n\tfor _, username := range cases {\n\t\tuassert.AbortsWithMessage(t, cur, ErrBlacklisted.Error(), func() {\n\t\t\tRegister(cross(cur), username)\n\t\t})\n\t}\n}\n\nfunc TestRegister_CanonicalCollision(cur realm, t *testing.T) {\n\t// First register a name. Same-digits confusable variant must collide\n\t// in the unified susers canonical store.\n\taddr1 := testutils.TestAddress(\"collision-1\")\n\ttesting.SetRealm(testing.NewUserRealm(addr1))\n\ttesting.SetOriginCaller(addr1)\n\ttesting.SetOriginSend(chain.NewCoins(chain.NewCoin(\"ugnot\", registerPrice)))\n\turequire.NotPanics(t, cur, func() {\n\t\tRegister(cross(cur), \"nym-balloon123\")\n\t})\n\n\t// Then attempt to register a canonical-equivalent variant — same\n\t// canonical full name (after l→i, etc.). Must reject from susers.\n\taddr2 := testutils.TestAddress(\"collision-2\")\n\ttesting.SetRealm(testing.NewUserRealm(addr2))\n\ttesting.SetOriginCaller(addr2)\n\ttesting.SetOriginSend(chain.NewCoins(chain.NewCoin(\"ugnot\", registerPrice)))\n\tuassert.AbortsWithMessage(t, cur, susers.ErrCanonicalCollision.Error(), func() {\n\t\tRegister(cross(cur), \"nym-baiioon123\")\n\t})\n\n\t// Reverse direction also blocked.\n\taddr3 := testutils.TestAddress(\"collision-3\")\n\ttesting.SetRealm(testing.NewUserRealm(addr3))\n\ttesting.SetOriginCaller(addr3)\n\ttesting.SetOriginSend(chain.NewCoins(chain.NewCoin(\"ugnot\", registerPrice)))\n\turequire.NotPanics(t, cur, func() {\n\t\tRegister(cross(cur), \"nym-papaiio789\")\n\t})\n\n\taddr4 := testutils.TestAddress(\"collision-4\")\n\ttesting.SetRealm(testing.NewUserRealm(addr4))\n\ttesting.SetOriginCaller(addr4)\n\ttesting.SetOriginSend(chain.NewCoins(chain.NewCoin(\"ugnot\", registerPrice)))\n\tuassert.AbortsWithMessage(t, cur, susers.ErrCanonicalCollision.Error(), func() {\n\t\tRegister(cross(cur), \"nym-papallo789\")\n\t})\n}\n\n// Decision #2 in Option B: store key is full canonical username (not\n// stem). Same alpha stem with different digit suffixes coexists — does\n// not collide.\nfunc TestRegister_SameStemDifferentDigits_Coexist(cur realm, t *testing.T) {\n\taddrA := testutils.TestAddress(\"stem-coexist-A\")\n\ttesting.SetRealm(testing.NewUserRealm(addrA))\n\ttesting.SetOriginCaller(addrA)\n\ttesting.SetOriginSend(chain.NewCoins(chain.NewCoin(\"ugnot\", registerPrice)))\n\turequire.NotPanics(t, cur, func() {\n\t\tRegister(cross(cur), \"nym-foolbar000\")\n\t})\n\n\taddrB := testutils.TestAddress(\"stem-coexist-B\")\n\ttesting.SetRealm(testing.NewUserRealm(addrB))\n\ttesting.SetOriginCaller(addrB)\n\ttesting.SetOriginSend(chain.NewCoins(chain.NewCoin(\"ugnot\", registerPrice)))\n\turequire.NotPanics(t, cur, func() {\n\t\tRegister(cross(cur), \"nym-foolbar999\")\n\t})\n}\n\nfunc TestRegister_TakenUsername(cur realm, t *testing.T) {\n\ttesting.SetOriginSend(chain.NewCoins(chain.NewCoin(\"ugnot\", registerPrice)))\n\ttesting.SetRealm(testing.NewUserRealm(testutils.TestAddress(\"taken-payer\")))\n\n\tusername := \"nym-zulutwo567\"\n\n\turequire.NotPanics(t, cur, func() {\n\t\tRegister(cross(cur), username)\n\t})\n\n\t// Re-registration of the same exact name is rejected by susers via\n\t// ErrNameTaken (the nameStore.Has check precedes the canonical check).\n\tuassert.AbortsWithMessage(t, cur, susers.ErrNameTaken.Error(), func() {\n\t\tRegister(cross(cur), username)\n\t})\n}\n\nfunc TestRegister_InvalidPayment(cur realm, t *testing.T) {\n\taddr := testutils.TestAddress(\"payment-payer\")\n\n\ttesting.SetRealm(testing.NewUserRealm(addr))\n\ttesting.SetOriginCaller(addr)\n\ttesting.SetOriginSend(chain.NewCoins(chain.NewCoin(\"ugnot\", 12))) // invalid\n\n\tuassert.AbortsContains(t, cur, ErrInvalidPayment.Error(), func() {\n\t\tRegister(cross(cur), \"nym-yankee345\")\n\t})\n}\n\n// Audit finding #11: the OriginSend() payment check is only trustworthy\n// when the direct caller is a pure EOA (IsUserCall). Any intermediate\n// code realm can attach -send to the tx, keep the coins, and call\n// Register; OriginSend() would still describe the envelope but namereg\n// would receive nothing. The EOA-only guard must reject all such callers.\nfunc TestRegister_IntermediateCodeRealmRejected(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/evil/wrapper\"))\n\ttesting.SetOriginSend(chain.NewCoins(chain.NewCoin(\"ugnot\", registerPrice)))\n\n\tuassert.AbortsWithMessage(t, cur, ErrNonUserCall.Error(), func() {\n\t\tRegister(cross(cur), \"nym-whisky345\")\n\t})\n}\n\n// Audit finding #13: the OriginSend mismatch error must report the\n// CURRENT registerPrice, not the value frozen at package init time.\nfunc TestRegister_PaymentErrorReflectsCurrentPrice(cur realm, t *testing.T) {\n\toldPrice := registerPrice\n\tdefer func() { registerPrice = oldPrice }()\n\n\tregisterPrice = 5_000_000\n\n\taddr := testutils.TestAddress(\"price-payer\")\n\ttesting.SetRealm(testing.NewUserRealm(addr))\n\ttesting.SetOriginCaller(addr)\n\ttesting.SetOriginSend(chain.NewCoins(chain.NewCoin(\"ugnot\", 1_000_000))) // wrong\n\n\tuassert.AbortsContains(t, cur, \"5000000\", func() {\n\t\tRegister(cross(cur), \"nym-xrayfox678\")\n\t})\n\n\tuassert.AbortsContains(t, cur, ErrInvalidPayment.Error(), func() {\n\t\tRegister(cross(cur), \"nym-xrayfox678\")\n\t})\n}\n\n// Audit finding #14: ProposeNewRegisterPrice originally rejected only\n// negative prices. A passed proposal to set a negative price would\n// have been arithmetically nonsense; reject below MinRegisterPrice\n// (currently 0) at proposal-creation time so governance can never\n// underflow the floor.\nfunc TestProposeNewRegisterPrice_floor(cur realm, t *testing.T) {\n\tt.Run(\"zero is accepted (free registration is the default)\", func(t *testing.T) {\n\t\turequire.NotPanics(t, cur, func() { ProposeNewRegisterPrice(cur, 0) })\n\t})\n\n\tt.Run(\"negative is rejected\", func(t *testing.T) {\n\t\turequire.PanicsWithMessage(t, cur,\n\t\t\t\"price below floor: -1 ugnot \u003c 0 ugnot (MinRegisterPrice)\",\n\t\t\tfunc() { ProposeNewRegisterPrice(cur, -1) })\n\t})\n\n\tt.Run(\"at floor is accepted\", func(t *testing.T) {\n\t\turequire.NotPanics(t, cur, func() { ProposeNewRegisterPrice(cur, MinRegisterPrice) })\n\t})\n\n\tt.Run(\"above floor is accepted\", func(t *testing.T) {\n\t\turequire.NotPanics(t, cur, func() { ProposeNewRegisterPrice(cur, 1_000_000_000) })\n\t})\n}\n"},{"name":"z_0_prop1_filetest.gno","body":"// PKGPATH: gno.land/r/sys/namereg/v0/filetests/z_0_prop1_filetest\n\npackage z_0_prop1_filetest\n\nimport (\n\t\"chain\"\n\t\"chain/runtime/unsafe\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/r/gov/dao\"\n\tdaov3init \"gno.land/r/gov/dao/init/v0\"\n\tusers \"gno.land/r/sys/namereg/v0\"\n\tsusers \"gno.land/r/sys/users\"\n)\n\n// Test updating a name via GovDAO\nvar c address = unsafe.OriginCaller()\n\nfunc init(cur realm) {\n\t// Whitelist this realm as a controller so its Register() can reach r/sys/users.\n\ttesting.SetHeight(0)\n\tsusers.AddControllerAtGenesis(cross(cur), chain.PackageAddress(\"gno.land/r/sys/namereg/v0\"))\n\ttesting.SetHeight(123)\n\n\tdaov3init.InitWithUsers(cross(cur), c)\n\n\talice := testutils.TestAddress(\"alice\")\n\n\t// Register alice\n\ttesting.SetOriginCaller(alice)\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\tusers.Register(cross(cur), \"nym-alice123\")\n\n\t// Prop to change name\n\ttesting.SetOriginCaller(c)\n\ttesting.SetRealm(testing.NewUserRealm(c))\n\tpr := users.ProposeNewName(cross(cur), alice, \"alice_new123\")\n\tdao.MustCreateProposal(cross(cur), pr)\n}\n\nfunc main(cur realm) {\n\ttesting.SetOriginCaller(c)\n\n\tprintln(\"--\")\n\tprintln(dao.Render(cross(cur), \"\"))\n\tprintln(\"--\")\n\tprintln(dao.Render(cross(cur), \"0\"))\n\tprintln(\"--\")\n\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(0)))\n\n\tprintln(\"--\")\n\tprintln(dao.Render(cross(cur), \"0\"))\n\tprintln(\"--\")\n\n\tdao.ExecuteProposal(cross(cur), dao.ProposalID(0))\n\n\tprintln(\"--\")\n\tprintln(dao.Render(cross(cur), \"0\"))\n\n\tdata, _ := susers.ResolveName(\"alice_new123\")\n\tprintln(data.Addr())\n}\n\n// Output:\n// --\n// # GovDAO\n// ## Members\n// [\u003e Go to Memberstore \u003c](/r/gov/dao/memberstore/v0)\n// ## Proposals\n// ### [Prop #0 - User Registry V0: Rename user \\`nym\\-alice123\\` to \\`alice\\_new123\\`](/r/gov/dao:0)\n// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n//\n// Status: ACTIVE\n//\n// Tiers eligible to vote: T1, T2, T3\n//\n// ---\n//\n//\n// --\n// ## Prop #0 - User Registry V0: Rename user \\`nym\\-alice123\\` to \\`alice\\_new123\\`\n// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n//\n//\n//\n// Executor created in: `gno.land/r/sys/namereg/v0`\n//\n//\n//\n//\n// ---\n//\n// ### Stats\n// - **Proposal is open for votes**\n// - Tiers eligible to vote: T1, T2, T3\n// - YES PERCENT: 0%\n// - NO PERCENT: 0%\n// - ABSTAIN PERCENT: 0%\n//\n// [Detailed voting list](/r/gov/dao:0/votes)\n//\n// ---\n//\n// ### Actions\n// [Vote YES](/r/gov/dao$help\u0026func=MustVoteOnProposalSimple\u0026option=YES\u0026pid=0) | [Vote NO](/r/gov/dao$help\u0026func=MustVoteOnProposalSimple\u0026option=NO\u0026pid=0) | [Vote ABSTAIN](/r/gov/dao$help\u0026func=MustVoteOnProposalSimple\u0026option=ABSTAIN\u0026pid=0)\n//\n// WARNING: Please double check transaction data before voting.\n// --\n// --\n// ## Prop #0 - User Registry V0: Rename user \\`nym\\-alice123\\` to \\`alice\\_new123\\`\n// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n//\n//\n//\n// Executor created in: `gno.land/r/sys/namereg/v0`\n//\n//\n//\n//\n// ---\n//\n// ### Stats\n// - **Proposal is open for votes**\n// - Tiers eligible to vote: T1, T2, T3\n// - YES PERCENT: 100%\n// - NO PERCENT: 0%\n// - ABSTAIN PERCENT: 0%\n//\n// [Detailed voting list](/r/gov/dao:0/votes)\n//\n// ---\n//\n// ### Actions\n// [Vote YES](/r/gov/dao$help\u0026func=MustVoteOnProposalSimple\u0026option=YES\u0026pid=0) | [Vote NO](/r/gov/dao$help\u0026func=MustVoteOnProposalSimple\u0026option=NO\u0026pid=0) | [Vote ABSTAIN](/r/gov/dao$help\u0026func=MustVoteOnProposalSimple\u0026option=ABSTAIN\u0026pid=0)\n//\n// WARNING: Please double check transaction data before voting.\n// --\n// --\n// ## Prop #0 - User Registry V0: Rename user \\`nym\\-alice123\\` to \\`alice\\_new123\\`\n// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n//\n//\n//\n// Executor created in: `gno.land/r/sys/namereg/v0`\n//\n//\n//\n//\n// ---\n//\n// ### Stats\n// - **PROPOSAL HAS BEEN ACCEPTED**\n// - Tiers eligible to vote: T1, T2, T3\n// - YES PERCENT: 100%\n// - NO PERCENT: 0%\n// - ABSTAIN PERCENT: 0%\n//\n// [Detailed voting list](/r/gov/dao:0/votes)\n//\n// ---\n//\n// ### Actions\n// [Vote YES](/r/gov/dao$help\u0026func=MustVoteOnProposalSimple\u0026option=YES\u0026pid=0) | [Vote NO](/r/gov/dao$help\u0026func=MustVoteOnProposalSimple\u0026option=NO\u0026pid=0) | [Vote ABSTAIN](/r/gov/dao$help\u0026func=MustVoteOnProposalSimple\u0026option=ABSTAIN\u0026pid=0)\n//\n// WARNING: Please double check transaction data before voting.\n// g1v9kxjcm9ta047h6lta047h6lta047h6lzd40gh\n"},{"name":"z_1_prop2_filetest.gno","body":"// PKGPATH: gno.land/r/sys/namereg/v0/filetests/z_1_prop2_filetest\n\npackage z_1_prop2_filetest\n\nimport (\n\t\"chain\"\n\t\"chain/runtime/unsafe\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/r/gov/dao\"\n\tdaov3init \"gno.land/r/gov/dao/init/v0\"\n\tusers \"gno.land/r/sys/namereg/v0\"\n\tsusers \"gno.land/r/sys/users\"\n)\n\n// Test updating a name via GovDAO\nvar c address = unsafe.OriginCaller()\n\nfunc init(cur realm) {\n\t// Whitelist this realm as a controller so its Register() can reach r/sys/users.\n\ttesting.SetHeight(0)\n\tsusers.AddControllerAtGenesis(cross(cur), chain.PackageAddress(\"gno.land/r/sys/namereg/v0\"))\n\ttesting.SetHeight(123)\n\n\tdaov3init.InitWithUsers(cross(cur), c)\n\n\talice := testutils.TestAddress(\"alice\")\n\n\t// Register alice\n\ttesting.SetOriginCaller(alice)\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\tusers.Register(cross(cur), \"nym-alice123\")\n\n\t// Prop to delete user\n\ttesting.SetOriginCaller(c)\n\ttesting.SetRealm(testing.NewUserRealm(c))\n\tpr := users.ProposeDeleteUser(cross(cur), alice, \"delete user test\")\n\tdao.MustCreateProposal(cross(cur), pr)\n}\n\nfunc main(cur realm) {\n\ttesting.SetOriginCaller(c)\n\n\tprintln(\"--\")\n\tprintln(dao.Render(cross(cur), \"\"))\n\tprintln(\"--\")\n\tprintln(dao.Render(cross(cur), \"0\"))\n\tprintln(\"--\")\n\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(0)))\n\n\tprintln(\"--\")\n\tprintln(dao.Render(cross(cur), \"0\"))\n\tprintln(\"--\")\n\n\tdao.ExecuteProposal(cross(cur), dao.ProposalID(0))\n\n\tprintln(\"--\")\n\tprintln(dao.Render(cross(cur), \"0\"))\n\n\tdata, _ := susers.ResolveName(\"nym-alice123\")\n\tif data == nil {\n\t\tprintln(\"Successfully deleted alice\")\n\t}\n}\n\n// Output:\n// --\n// # GovDAO\n// ## Members\n// [\u003e Go to Memberstore \u003c](/r/gov/dao/memberstore/v0)\n// ## Proposals\n// ### [Prop #0 - User Registry V0: Delete user \\`nym\\-alice123\\`](/r/gov/dao:0)\n// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n//\n// Status: ACTIVE\n//\n// Tiers eligible to vote: T1, T2, T3\n//\n// ---\n//\n//\n// --\n// ## Prop #0 - User Registry V0: Delete user \\`nym\\-alice123\\`\n// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n//\n// delete user test\n//\n// Executor created in: `gno.land/r/sys/namereg/v0`\n//\n//\n//\n//\n// ---\n//\n// ### Stats\n// - **Proposal is open for votes**\n// - Tiers eligible to vote: T1, T2, T3\n// - YES PERCENT: 0%\n// - NO PERCENT: 0%\n// - ABSTAIN PERCENT: 0%\n//\n// [Detailed voting list](/r/gov/dao:0/votes)\n//\n// ---\n//\n// ### Actions\n// [Vote YES](/r/gov/dao$help\u0026func=MustVoteOnProposalSimple\u0026option=YES\u0026pid=0) | [Vote NO](/r/gov/dao$help\u0026func=MustVoteOnProposalSimple\u0026option=NO\u0026pid=0) | [Vote ABSTAIN](/r/gov/dao$help\u0026func=MustVoteOnProposalSimple\u0026option=ABSTAIN\u0026pid=0)\n//\n// WARNING: Please double check transaction data before voting.\n// --\n// --\n// ## Prop #0 - User Registry V0: Delete user \\`nym\\-alice123\\`\n// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n//\n// delete user test\n//\n// Executor created in: `gno.land/r/sys/namereg/v0`\n//\n//\n//\n//\n// ---\n//\n// ### Stats\n// - **Proposal is open for votes**\n// - Tiers eligible to vote: T1, T2, T3\n// - YES PERCENT: 100%\n// - NO PERCENT: 0%\n// - ABSTAIN PERCENT: 0%\n//\n// [Detailed voting list](/r/gov/dao:0/votes)\n//\n// ---\n//\n// ### Actions\n// [Vote YES](/r/gov/dao$help\u0026func=MustVoteOnProposalSimple\u0026option=YES\u0026pid=0) | [Vote NO](/r/gov/dao$help\u0026func=MustVoteOnProposalSimple\u0026option=NO\u0026pid=0) | [Vote ABSTAIN](/r/gov/dao$help\u0026func=MustVoteOnProposalSimple\u0026option=ABSTAIN\u0026pid=0)\n//\n// WARNING: Please double check transaction data before voting.\n// --\n// --\n// ## Prop #0 - User Registry V0: Delete user \\`nym\\-alice123\\`\n// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n//\n// delete user test\n//\n// Executor created in: `gno.land/r/sys/namereg/v0`\n//\n//\n//\n//\n// ---\n//\n// ### Stats\n// - **PROPOSAL HAS BEEN ACCEPTED**\n// - Tiers eligible to vote: T1, T2, T3\n// - YES PERCENT: 100%\n// - NO PERCENT: 0%\n// - ABSTAIN PERCENT: 0%\n//\n// [Detailed voting list](/r/gov/dao:0/votes)\n//\n// ---\n//\n// ### Actions\n// [Vote YES](/r/gov/dao$help\u0026func=MustVoteOnProposalSimple\u0026option=YES\u0026pid=0) | [Vote NO](/r/gov/dao$help\u0026func=MustVoteOnProposalSimple\u0026option=NO\u0026pid=0) | [Vote ABSTAIN](/r/gov/dao$help\u0026func=MustVoteOnProposalSimple\u0026option=ABSTAIN\u0026pid=0)\n//\n// WARNING: Please double check transaction data before voting.\n// Successfully deleted alice\n"},{"name":"z_2_prop3_filetest.gno","body":"// PKGPATH: gno.land/r/sys/namereg/v0/filetests/z_2_prop3_filetest\n\npackage z_2_prop3_filetest\n\nimport (\n\t\"chain\"\n\t\"chain/runtime/unsafe\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/r/gov/dao\"\n\tdaov3init \"gno.land/r/gov/dao/init/v0\"\n\tusers \"gno.land/r/sys/namereg/v0\"\n\tsusers \"gno.land/r/sys/users\"\n)\n\n// Test the full governance-driven registerPrice flow:\n//  1. propose a new price via users.ProposeNewRegisterPrice\n//  2. DAO accepts the proposal (vote YES, execute)\n//  3. verify the executed callback updated registerPrice by exercising\n//     Register with the new amount and watching it succeed (registration\n//     under the old default of 0 ugnot would now panic with the new\n//     price in the error message).\nvar c address = unsafe.OriginCaller()\n\nfunc init(cur realm) {\n\t// Whitelist this realm as a controller so its Register() can reach r/sys/users.\n\ttesting.SetHeight(0)\n\tsusers.AddControllerAtGenesis(cross(cur), chain.PackageAddress(\"gno.land/r/sys/namereg/v0\"))\n\ttesting.SetHeight(123)\n\n\tdaov3init.InitWithUsers(cross(cur), c)\n\n\t// Propose raising the price from 0 to 20 GNOT.\n\ttesting.SetOriginCaller(c)\n\ttesting.SetRealm(testing.NewUserRealm(c))\n\tpr := users.ProposeNewRegisterPrice(cross(cur), 20_000_000)\n\tdao.MustCreateProposal(cross(cur), pr)\n}\n\nfunc main(cur realm) {\n\ttesting.SetOriginCaller(c)\n\n\t// Vote and execute.\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(0)))\n\tdao.ExecuteProposal(cross(cur), dao.ProposalID(0))\n\n\t// New price should be active. Register a user with the new amount;\n\t// registration with the old default (0 ugnot) would panic.\n\talice := testutils.TestAddress(\"alice\")\n\ttesting.SetOriginCaller(alice)\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\ttesting.SetOriginSend(chain.Coins{{Denom: \"ugnot\", Amount: 20_000_000}})\n\tusers.Register(cross(cur), \"nym-alice123\")\n\n\tdata, _ := susers.ResolveName(\"nym-alice123\")\n\tprintln(\"registered alice at:\", data.Addr())\n}\n\n// Output:\n// registered alice at: g1v9kxjcm9ta047h6lta047h6lta047h6lzd40gh\n"},{"name":"z_3_prop4_filetest.gno","body":"// PKGPATH: gno.land/r/sys/namereg/v0/filetests/z_3_prop4_filetest\n\npackage z_3_prop4_filetest\n\nimport (\n\t\"chain\"\n\t\"chain/runtime/unsafe\"\n\t\"testing\"\n\n\t\"gno.land/r/gov/dao\"\n\tdaov3init \"gno.land/r/gov/dao/init/v0\"\n\tusers \"gno.land/r/sys/namereg/v0\"\n\tsusers \"gno.land/r/sys/users\"\n)\n\n// Test the full governance-driven pause/unpause flow:\n//  1. propose pause via NewSetPausedExecutor(true), vote YES, execute,\n//     observe IsPaused() flip to true.\n//  2. propose unpause via NewSetPausedExecutor(false), vote YES, execute,\n//     observe IsPaused() flip back to false.\n//  3. Register works again post-unpause.\nvar c address = unsafe.OriginCaller()\n\nfunc init(cur realm) {\n\t// Whitelist this realm as a controller so its Register() can reach r/sys/users.\n\ttesting.SetHeight(0)\n\tsusers.AddControllerAtGenesis(cross(cur), chain.PackageAddress(\"gno.land/r/sys/namereg/v0\"))\n\ttesting.SetHeight(123)\n\n\tdaov3init.InitWithUsers(cross(cur), c)\n}\n\nfunc main(cur realm) {\n\ttesting.SetOriginCaller(c)\n\ttesting.SetRealm(testing.NewUserRealm(c))\n\n\tprintln(\"paused before:\", users.IsPaused())\n\n\t// Propose, vote, execute: pause.\n\tdao.MustCreateProposal(cross(cur), users.NewSetPausedExecutor(cross(cur), true))\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(0)))\n\tdao.ExecuteProposal(cross(cur), dao.ProposalID(0))\n\tprintln(\"paused after pause prop:\", users.IsPaused())\n\n\t// Propose, vote, execute: unpause.\n\tdao.MustCreateProposal(cross(cur), users.NewSetPausedExecutor(cross(cur), false))\n\tdao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(1)))\n\tdao.ExecuteProposal(cross(cur), dao.ProposalID(1))\n\tprintln(\"paused after unpause prop:\", users.IsPaused())\n\n\t// Register works again (the pause check inside Register would have\n\t// panicked with ErrPaused had the unpause executor not run).\n\tusers.Register(cross(cur), \"nym-alice123\")\n\tdata, _ := susers.ResolveName(\"nym-alice123\")\n\tprintln(\"registered alice at:\", data.Addr())\n}\n\n// Output:\n// paused before: false\n// paused after pause prop: true\n// paused after unpause prop: false\n// registered alice at: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"/rXA74MNiO2KXt4ng+RjocRgIZDUeAByR9K0evMOeFF5DeGcU8cBVTo9HS37iOxk8ymtZrTl9pJy/QXbW+xOnQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l","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/v0 (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/impl/v0\"\n\tmemberstore \"gno.land/r/gov/dao/memberstore/v0\"\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(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\")\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.IsCurrent() {\n\t\tpanic(\"unauthorized: cur is not the caller's live realm\")\n\t}\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"},{"name":"verifier_test.gno","body":"package names\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\n// cur is a zero-value realm used as a placeholder when forwarding to\n// uassert/urequire dispatch helpers that gained an `rlm realm` param.\n// These tests pass `func()` callbacks (no crossing inside the callback),\n// so rlm is ignored — a nil realm here is safe.\nvar cur realm\nvar (\n\talice = testutils.TestAddress(\"alice\")\n\tbob   = testutils.TestAddress(\"bob\")\n)\n\nfunc TestDefaultVerifier(t *testing.T) {\n\t// Disabled: any case is true (regardless of paused or lookup).\n\tuassert.True(t, verifier(false, false, alice, alice.String(), nil))\n\tuassert.True(t, verifier(false, false, \"\", alice.String(), nil))\n\tuassert.True(t, verifier(false, false, alice, \"somerandomusername\", nil))\n\n\t// Pre-Enable bypass takes priority over paused — even when paused=true,\n\t// !isEnabled returns true. This is the documented \"stage pause before\n\t// Enable\" behavior.\n\tuassert.True(t, verifier(false, true, alice, alice.String(), nil))\n\tuassert.True(t, verifier(false, true, alice, \"anything\", nil))\n\n\t// Enabled: PA namespace check.\n\tuassert.True(t, verifier(true, false, alice, alice.String(), nil))\n\n\t// Enabled: non-PA namespaces denied when no registered-name lookup.\n\tuassert.False(t, verifier(true, false, alice, \"notregistered\", nil))\n\tuassert.False(t, verifier(true, false, alice, \"alice\", nil))\n\n\t// Enabled: empty name/address.\n\tuassert.False(t, verifier(true, false, address(\"\"), \"\", nil))\n\tuassert.False(t, verifier(true, false, alice, \"\", nil))\n\tuassert.False(t, verifier(true, false, address(\"\"), \"something\", nil))\n}\n\n// TestRegisteredNameVerifier exercises the second authorization path —\n// the registered-name lookup. Uses a fake lookup so this test doesn't\n// depend on r/sys/users state.\nfunc TestRegisteredNameVerifier(t *testing.T) {\n\t// Lookup that says \"gnobody\" is registered to alice.\n\taliceOwnsGnobody := func(name string) (address, bool) {\n\t\tif name == \"gnobody\" {\n\t\t\treturn alice, true\n\t\t}\n\t\treturn \"\", false\n\t}\n\n\t// alice can deploy under \"gnobody\" (her registered name).\n\tuassert.True(t, verifier(true, false, alice, \"gnobody\", aliceOwnsGnobody))\n\n\t// bob cannot deploy under \"gnobody\" (registered to alice, not bob).\n\tuassert.False(t, verifier(true, false, bob, \"gnobody\", aliceOwnsGnobody))\n\n\t// Unknown name still rejects (lookup returns ok=false).\n\tuassert.False(t, verifier(true, false, alice, \"notregistered\", aliceOwnsGnobody))\n\n\t// PA path still works alongside registered-name lookup. Tried first.\n\tuassert.True(t, verifier(true, false, alice, alice.String(), aliceOwnsGnobody))\n\n\t// Disabled bypasses both paths entirely (returns true regardless).\n\tuassert.True(t, verifier(false, false, bob, \"gnobody\", aliceOwnsGnobody))\n\n\t// Empty namespace + non-nil lookup: still rejects (early-out before lookup).\n\tuassert.False(t, verifier(true, false, alice, \"\", aliceOwnsGnobody))\n\n\t// Lookup returns ok=true for current-but-mismatched address.\n\tbobOwnsGnobody := func(name string) (address, bool) {\n\t\tif name == \"gnobody\" {\n\t\t\treturn bob, true\n\t\t}\n\t\treturn \"\", false\n\t}\n\tuassert.False(t, verifier(true, false, alice, \"gnobody\", bobOwnsGnobody))\n}\n\n// TestPausedVerifier covers the emergency-halt semantic. When paused\n// AND enabled, the verifier rejects EVERY namespace check including PA.\nfunc TestPausedVerifier(t *testing.T) {\n\t// Lookup that says \"gnobody\" is registered to alice. Used to confirm\n\t// the registered-name path is also halted by pause.\n\taliceOwnsGnobody := func(name string) (address, bool) {\n\t\tif name == \"gnobody\" {\n\t\t\treturn alice, true\n\t\t}\n\t\treturn \"\", false\n\t}\n\n\tt.Run(\"paused blocks PA namespaces\", func(t *testing.T) {\n\t\t// PA would normally succeed (addr.String() == namespace), but\n\t\t// pause short-circuits before the PA check.\n\t\tuassert.False(t, verifier(true, true, alice, alice.String(), nil))\n\t\tuassert.False(t, verifier(true, true, alice, alice.String(), aliceOwnsGnobody))\n\t})\n\n\tt.Run(\"paused blocks registered-name namespaces\", func(t *testing.T) {\n\t\t// Registered-name path would normally succeed, but pause\n\t\t// short-circuits before the lookup is consulted.\n\t\tuassert.False(t, verifier(true, true, alice, \"gnobody\", aliceOwnsGnobody))\n\t})\n\n\tt.Run(\"paused blocks any other namespace too\", func(t *testing.T) {\n\t\tuassert.False(t, verifier(true, true, alice, \"anything\", aliceOwnsGnobody))\n\t\tuassert.False(t, verifier(true, true, alice, \"\", aliceOwnsGnobody))\n\t\tuassert.False(t, verifier(true, true, address(\"\"), \"anything\", aliceOwnsGnobody))\n\t})\n\n\tt.Run(\"pre-Enable bypass takes priority over paused\", func(t *testing.T) {\n\t\t// !enabled is checked BEFORE paused in the verifier — the\n\t\t// pre-Enable shortcut returns true regardless of pause state.\n\t\t// This is the \"pause is staged, applies on Enable\" semantic.\n\t\tuassert.True(t, verifier(false, true, alice, alice.String(), nil))\n\t\tuassert.True(t, verifier(false, true, alice, \"gnobody\", aliceOwnsGnobody))\n\t\tuassert.True(t, verifier(false, true, alice, \"anything\", nil))\n\t})\n\n\tt.Run(\"paused=false with enabled=true behaves normally\", func(t *testing.T) {\n\t\t// Sanity: just confirms the false case doesn't accidentally do\n\t\t// something different from the no-pause baseline.\n\t\tuassert.True(t, verifier(true, false, alice, alice.String(), nil))\n\t\tuassert.True(t, verifier(true, false, alice, \"gnobody\", aliceOwnsGnobody))\n\t\tuassert.False(t, verifier(true, false, bob, \"gnobody\", aliceOwnsGnobody))\n\t})\n}\n\n// TestProposeSetPaused_idempotency verifies the proposal-creation guard:\n// requesting a pause state that already matches the current state panics\n// at proposal-creation time, so voters never see a no-op proposal.\n//\n// NOTE: this test mutates the package-level `paused` var directly to\n// avoid driving the full DAO flow. Restores the value via defer.\nfunc TestProposeSetPaused_idempotency(cur realm, t *testing.T) {\n\tsaved := paused\n\tdefer func() { paused = saved }()\n\n\tt.Run(\"propose pause when already paused panics\", func(t *testing.T) {\n\t\tpaused = true\n\t\turequire.PanicsWithMessage(t, cur,\n\t\t\t\"paused state already matches requested value; no-op proposal rejected\",\n\t\t\tfunc() { ProposeSetPaused(cur, true) })\n\t})\n\n\tt.Run(\"propose unpause when already unpaused panics\", func(t *testing.T) {\n\t\tpaused = false\n\t\turequire.PanicsWithMessage(t, cur,\n\t\t\t\"paused state already matches requested value; no-op proposal rejected\",\n\t\t\tfunc() { ProposeSetPaused(cur, false) })\n\t})\n\n\tt.Run(\"propose pause when unpaused succeeds\", func(t *testing.T) {\n\t\tpaused = false\n\t\turequire.NotPanics(t, cur, func() { ProposeSetPaused(cur, true) })\n\t\t// The proposal-create call doesn't actually mutate paused —\n\t\t// only its execution would. Confirm.\n\t\tuassert.False(t, paused)\n\t})\n\n\tt.Run(\"propose unpause when paused succeeds\", func(t *testing.T) {\n\t\tpaused = true\n\t\turequire.NotPanics(t, cur, func() { ProposeSetPaused(cur, false) })\n\t\t// Same: proposal-create does not mutate.\n\t\tuassert.True(t, paused)\n\t})\n}\n\n// TestSetPaused_internal verifies the private setPaused actuator\n// directly. Bypasses the proposal flow to confirm the toggle and the\n// event-emission path independently.\nfunc TestSetPaused_internal(cur realm, t *testing.T) {\n\tsaved := paused\n\tdefer func() { paused = saved }()\n\n\tpaused = false\n\tsetPaused(0, cur, true)\n\tuassert.True(t, paused)\n\n\tsetPaused(0, cur, false)\n\tuassert.False(t, paused)\n}\n\nfunc TestIsPaused(t *testing.T) {\n\tsaved := paused\n\tdefer func() { paused = saved }()\n\n\tpaused = false\n\tuassert.False(t, IsPaused())\n\n\tpaused = true\n\tuassert.True(t, IsPaused())\n}\n\nfunc TestEnable(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(testutils.TestAddress(\"random\")))\n\tuassert.AbortsWithMessage(t, cur, \"caller is not admin\", func() {\n\t\tEnable(cross(cur))\n\t})\n\tuassert.False(t, IsEnabled())\n\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\tuassert.NotPanics(t, cur, func() {\n\t\tEnable(cross(cur))\n\t})\n\tuassert.True(t, IsEnabled())\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"lfFT4xsIPp/iFE4CygD8RA++jXLyjRSsMSXPYAr3fHtlmpHeiEiX2a6TnrLIiEheebrTD60rXPNevsSO9UnsVw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l","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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"Jk4YTTGFEl0B+WRCKzL818PqhlJCvvI58tuQme3tVFxxoNNBQFNz7/iQboKfhpETYe8UCmc3AeESx1GTsUPQyQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l","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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"9kSqG3Ke4gwXrD+BoCW5+G/6nosu26inVoBef4jX3/tBmgoRUc0DgkESH0G+OwhF378zM05aXeN2OLRJEWrtDg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l","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/v0\"\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/v0\"\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/v0\"\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"},{"name":"validators_test.gno","body":"package validators\n\nimport (\n\t\"chain/runtime\"\n\t\"math\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/poa/v0\"\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/sys/validators/v0\"\n)\n\n// cur is a zero-value realm used as a placeholder when forwarding to\n// uassert/urequire dispatch helpers that gained an `rlm realm` param.\n// These tests pass `func()` callbacks (no crossing inside the callback),\n// so rlm is ignored — a nil realm here is safe.\nvar cur realm\n\n// generateTestValidators generates a dummy validator set\nfunc generateTestValidators(count int) []validators.Validator {\n\tvals := make([]validators.Validator, 0, count)\n\n\tfor i := 0; i \u003c count; i++ {\n\t\tval := validators.Validator{\n\t\t\tAddress:     testutils.TestAddress(ufmt.Sprintf(\"%d\", i)),\n\t\t\tPubKey:      \"public-key\",\n\t\t\tVotingPower: 10,\n\t\t}\n\n\t\tvals = append(vals, val)\n\t}\n\n\treturn vals\n}\n\nfunc TestValidators_AddRemove(t *testing.T) {\n\t// Clear any changes\n\tchanges = bptree.NewBPTree32()\n\n\tvar (\n\t\tvals          = generateTestValidators(100)\n\t\tinitialHeight = int64(123)\n\t)\n\n\t// Add in the validators\n\tfor _, val := range vals {\n\t\taddValidator(val)\n\n\t\t// Make sure the validator is added\n\t\tuassert.True(t, vp.IsValidator(val.Address))\n\n\t\ttesting.SkipHeights(1)\n\t}\n\n\tfor i := initialHeight; i \u003c initialHeight+int64(len(vals)); i++ {\n\t\t// Make sure the changes are saved\n\t\tchs := GetChanges(i, initialHeight+int64(len(vals)))\n\n\t\t// We use the funky index calculation to make sure\n\t\t// changes are properly handled for each block span\n\t\tuassert.Equal(t, initialHeight+int64(len(vals))-i, int64(len(chs)))\n\n\t\tfor index, val := range vals[i-initialHeight:] {\n\t\t\t// Make sure the changes are equal to the additions\n\t\t\tch := chs[index]\n\n\t\t\tuassert.Equal(t, val.Address, ch.Address)\n\t\t\tuassert.Equal(t, val.PubKey, ch.PubKey)\n\t\t\tuassert.Equal(t, val.VotingPower, ch.VotingPower)\n\t\t}\n\t}\n\n\t// Save the beginning height for the removal\n\tinitialRemoveHeight := runtime.ChainHeight()\n\n\t// Clear any changes\n\tchanges = bptree.NewBPTree32()\n\n\t// Remove the validators\n\tfor _, val := range vals {\n\t\tremoveValidator(val.Address)\n\n\t\t// Make sure the validator is removed\n\t\tuassert.False(t, vp.IsValidator(val.Address))\n\n\t\ttesting.SkipHeights(1)\n\t}\n\n\tfor i := initialRemoveHeight; i \u003c initialRemoveHeight+int64(len(vals)); i++ {\n\t\t// Make sure the changes are saved\n\t\tchs := GetChanges(i, initialRemoveHeight+int64(len(vals)))\n\n\t\t// We use the funky index calculation to make sure\n\t\t// changes are properly handled for each block span\n\t\tuassert.Equal(t, initialRemoveHeight+int64(len(vals))-i, int64(len(chs)))\n\n\t\tfor index, val := range vals[i-initialRemoveHeight:] {\n\t\t\t// Make sure the changes are equal to the additions\n\t\t\tch := chs[index]\n\n\t\t\tuassert.Equal(t, val.Address, ch.Address)\n\t\t\tuassert.Equal(t, val.PubKey, ch.PubKey)\n\t\t\tuassert.Equal(t, uint64(0), ch.VotingPower)\n\t\t}\n\t}\n}\n\n// TestGetChanges_BoundedRange verifies that GetChanges(from, to) correctly\n// returns only changes within the [from, to] block range.\nfunc TestGetChanges_BoundedRange(t *testing.T) {\n\tchanges = bptree.NewBPTree32()\n\tvp = poa.NewPoA()\n\n\tvals := generateTestValidators(3)\n\n\t// Store additions at block h1\n\th1 := runtime.ChainHeight()\n\tfor _, val := range vals {\n\t\taddValidator(val)\n\t}\n\ttesting.SkipHeights(1)\n\n\t// Store removals at block h2\n\th2 := runtime.ChainHeight()\n\tfor _, val := range vals {\n\t\tremoveValidator(val.Address)\n\t}\n\ttesting.SkipHeights(1)\n\n\t// Query spanning both blocks returns all changes\n\tall := GetChanges(h1, h2)\n\tuassert.Equal(t, 6, len(all))\n\n\t// Query for h1 only returns additions\n\tatH1 := GetChanges(h1, h1)\n\tuassert.Equal(t, 3, len(atH1))\n\tfor i, ch := range atH1 {\n\t\tuassert.Equal(t, vals[i].Address, ch.Address)\n\t\tuassert.True(t, ch.VotingPower \u003e 0)\n\t}\n\n\t// Query for h2 only returns removals\n\tatH2 := GetChanges(h2, h2)\n\tuassert.Equal(t, 3, len(atH2))\n\tfor i, ch := range atH2 {\n\t\tuassert.Equal(t, vals[i].Address, ch.Address)\n\t\tuassert.Equal(t, uint64(0), ch.VotingPower)\n\t}\n\n\t// Query beyond stored range returns empty\n\tuassert.Equal(t, 0, len(GetChanges(h2+1, h2+1)))\n}\n\n// TestRender_ShowsNewestWhenOverLimit verifies that Render displays the newest\n// maxDisplay buckets when there are more change buckets than maxDisplay.\nfunc TestRender_ShowsNewestWhenOverLimit(t *testing.T) {\n\tchanges = bptree.NewBPTree32()\n\tvp = poa.NewPoA()\n\n\tconst total = 13\n\tconst maxDisplay = 10\n\n\tvals := generateTestValidators(total)\n\tbase := runtime.ChainHeight()\n\n\tfor i := 0; i \u003c total; i++ {\n\t\th := base + int64(i)\n\t\tchanges.Set(getBlockID(h), []change{\n\t\t\t{blockNum: h, validator: vals[i]},\n\t\t})\n\t}\n\n\toutput := Render(\"\")\n\n\t// Newest maxDisplay buckets must appear.\n\tfor i := total - maxDisplay; i \u003c total; i++ {\n\t\th := base + int64(i)\n\t\tuassert.True(t, strings.Contains(output, ufmt.Sprintf(\"#%d:\", h)),\n\t\t\tufmt.Sprintf(\"expected block #%d in output\", h))\n\t}\n\n\t// Oldest (total - maxDisplay) buckets must NOT appear.\n\tfor i := 0; i \u003c total-maxDisplay; i++ {\n\t\th := base + int64(i)\n\t\tuassert.False(t, strings.Contains(output, ufmt.Sprintf(\"#%d:\", h)),\n\t\t\tufmt.Sprintf(\"block #%d should be absent from output\", h))\n\t}\n}\n\n// TestRender_ShowsAllWhenUnderLimit verifies that all buckets are displayed when\n// there are fewer than maxDisplay, and that no panic occurs from a negative offset.\nfunc TestRender_ShowsAllWhenUnderLimit(t *testing.T) {\n\tchanges = bptree.NewBPTree32()\n\tvp = poa.NewPoA()\n\n\tconst total = 5 // less than maxDisplay=10\n\n\tvals := generateTestValidators(total)\n\tbase := runtime.ChainHeight()\n\n\tfor i := 0; i \u003c total; i++ {\n\t\th := base + int64(i)\n\t\tchanges.Set(getBlockID(h), []change{\n\t\t\t{blockNum: h, validator: vals[i]},\n\t\t})\n\t}\n\n\toutput := Render(\"\")\n\n\tfor i := 0; i \u003c total; i++ {\n\t\th := base + int64(i)\n\t\tuassert.True(t, strings.Contains(output, ufmt.Sprintf(\"#%d:\", h)),\n\t\t\tufmt.Sprintf(\"expected block #%d in output\", h))\n\t}\n}\n\nfunc TestGetChanges_PanicsOnInvalidRange(cur realm, t *testing.T) {\n\tuassert.PanicsWithMessage(t, cur, \"invalid range: from must be \u003c= to\", func() {\n\t\tGetChanges(10, 5)\n\t})\n}\n\nfunc TestGetChanges_ClampsMaxInt64(t *testing.T) {\n\tchanges = bptree.NewBPTree32()\n\n\tvals := generateTestValidators(1)\n\n\t// Simulate a validator change at block math.MaxInt64-1 (the boundary value).\n\tchanges.Set(getBlockID(math.MaxInt64-1), []change{\n\t\t{blockNum: math.MaxInt64 - 1, validator: vals[0]},\n\t})\n\n\t// Passing math.MaxInt64 as \"to\" means \"get all updates from here onwards\".\n\t// The clamp (to = MaxInt64-1) must still include the boundary block.\n\tresult := GetChanges(math.MaxInt64-1, math.MaxInt64)\n\tuassert.Equal(t, 1, len(result))\n\tuassert.Equal(t, vals[0].Address, result[0].Address)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"ucKvrgL2s/oGsrUtESEZk6gDdmwNJwg7e+vWH4XO0142HTAmg04LdFULupiTzE0MQNmJb7N1dpt4qYpL13h1yA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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/v0\"\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"SVx7rTrUBnFovy+p0yfmZpAq/OV8ioRiPS70ts20AC0F+dFIMX80lm+02MbNpqMGggverviFkYjSuNq1BGzYeQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"qm1v95uj5qstXrtSr8aRkqDAyaTodMEtex2ssWnxTaAgFw5bqKD7PLKL+9DZ+PXkq3xK3G0MZSDL6nlhTTiVxQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"VJDiHmw/NTJt97Z7jUaBZvfHU0745sCA70Lk79oW2Wd1PINzwVkWYpYsjB2iQ0w5ba2CyIx1FNhr/N8O6IPWhQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"Se5VJC29BG8d9l5xCSdRR0XJSDzuBIl0F8luYfAcKWR7+YNXoz78BNnGPmlOzrRdBMTPipOezRngwNk3U72VPg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"IPp78BuOs1syEeM77PoWdlSzpNKx/qlx2m4heop5mucuz78iTAgq9lZgwWmSqS0xCgmUiVOQOTyG0XRHNfcz1g=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"SzpG+IV35MU11/O9wEBL06EtOrmCrfth8oWndzuNsrUpreopXo5d2PvrY2ue1LsbNcGpvnCFEv/VKV2C1bJqBQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"H5NXoz7ifPF4iJ+lSomgqeU5tQ+1F9bvOAVnPCYS5Jloytnb5ODAsiTWCJBdkPBVCUn70MXMmG8goK0Jxqll5w=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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/v0\"\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"ff/VzGUBhsB8bnR8GzdoE4VPrYeISCOnchrlfiystcs187ElqiTiWx4vWccypaqfqM3BvfWo2SfWaijEbp/lww=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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/v0\"\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"62TJTEuTBIfGqX4Xjs2E9p1YIgF+d7BOzaUqwG9KcFJkISL48VT3XsoQdMnCXd4XNiU9i4XlkPNJKr2ztwy8Bw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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/v0\"\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"+uY/lfjQgO4C1zw5bby+GOi/CcCdTDk72qHbRoN4IMke9F6jhCqOdwwvfbfb1x7eFzshuA+tDQlCo9I5XlHZjg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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/v0\"\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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"LbfGP5HkLG9bcIW6FotSjS6JBmr+OfiOsVL2QAnqceYS/E/YGJZDPiO0KfGiVF+XuKdVT9/22rUCXAFxiQm4WA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"zvSqZpLMqTIsDh5Uzn4WAVANObYxTxAuHaLkRghtqQ0rU0Pk2NVqOEijMSWjfWGaHvM5BLIZTu1uG7c0SGZDxw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"6ifzujdgIPpGsQ4IGbQUIw/R9FHWX3vl2wx48nXvlDhhHYWIJx69XQ36B6IDN9NYLv1VMflj3gZqwAFdrIDgxw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","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":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"FdnM3+pq1qQLf6wZ2Zl/Vj/P9F782tgUT5b9S21TElobxxxmdRkoehoFez2olykfYMUqniPFrPx9T5+svWP8xA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"forms","path":"gno.land/p/agherasie/forms","files":[{"name":"create.gno","body":"package forms\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/onbloc/json/v0\"\n)\n\nconst dateFormat = \"2006-01-02T15:04:05Z\"\n\nfunc CreateField(label string, fieldType string, required bool) Field {\n\treturn Field{\n\t\tLabel:     label,\n\t\tFieldType: fieldType,\n\t\tRequired:  required,\n\t}\n}\n\n// CreateForm creates a new form. rlm must be the caller's own captured\n// cur (asserted via rlm.IsCurrent()); the form Owner is set to\n// rlm.Previous().Address() — unforgeable, vs. the prior runtime walk\n// which a non-crossing wrapper could spoof.\nfunc (db *FormDB) CreateForm(_ int, rlm realm, title string, description string, openAt string, closeAt string, data string) (string, error) {\n\tif !rlm.IsCurrent() {\n\t\treturn \"\", errSpoofedRealm\n\t}\n\t// Parsing the dates\n\tvar parsedOpenTime, parsedCloseTime time.Time\n\n\tif openAt != \"\" {\n\t\tvar err error\n\t\tparsedOpenTime, err = time.Parse(dateFormat, openAt)\n\t\tif err != nil {\n\t\t\treturn \"\", errInvalidDate\n\t\t}\n\t}\n\n\tif closeAt != \"\" {\n\t\tvar err error\n\t\tparsedCloseTime, err = time.Parse(dateFormat, closeAt)\n\t\tif err != nil {\n\t\t\treturn \"\", errInvalidDate\n\t\t}\n\t}\n\n\t// Parsing the json submission\n\tnode, err := json.Unmarshal([]byte(data))\n\tif err != nil {\n\t\treturn \"\", errInvalidJson\n\t}\n\n\tfieldsCount := node.Size()\n\tfields := make([]Field, fieldsCount)\n\n\t// Parsing the json submission to create the gno data structures\n\tfor i := 0; i \u003c fieldsCount; i++ {\n\t\tfield := node.MustIndex(i)\n\n\t\tfields[i] = CreateField(\n\t\t\tfield.MustKey(\"label\").MustString(),\n\t\t\tfield.MustKey(\"fieldType\").MustString(),\n\t\t\tfield.MustKey(\"required\").MustBool(),\n\t\t)\n\t}\n\n\t// Generating the form ID\n\tid := db.IDCounter.Next().String()\n\n\t// Creating the form\n\tform := Form{\n\t\tID:          id,\n\t\tOwner:       rlm.Previous().Address(),\n\t\tTitle:       title,\n\t\tDescription: description,\n\t\tCreatedAt:   time.Now(),\n\t\topenAt:      parsedOpenTime,\n\t\tcloseAt:     parsedCloseTime,\n\t\tFields:      fields,\n\t}\n\n\t// Adding the form to the database\n\tdb.Forms = append(db.Forms, \u0026form)\n\n\treturn id, nil\n}\n"},{"name":"create_test.gno","body":"package forms\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\n// cur is a zero-value realm used as a placeholder when forwarding to\n// uassert/urequire dispatch helpers that gained an `rlm realm` param.\n// These tests pass `func()` callbacks (no crossing inside the callback),\n// so rlm is ignored — a nil realm here is safe.\nvar cur realm\n\nfunc TestCreateForm(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\ttesting.SetOriginCaller(alice)\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\n\tdb := NewDB()\n\ttitle := \"Simple Form\"\n\tdescription := \"This is a form\"\n\topenAt := \"2021-01-01T00:00:00Z\"\n\tcloseAt := \"2021-01-02T00:00:00Z\"\n\tdata := `[\n\t\t{\n\t\t\t\"label\": \"Name\",\n\t\t\t\"fieldType\": \"string\",\n\t\t\t\"required\": true\n\t\t},\n\t\t{\n\t\t\t\"label\": \"Age\",\n\t\t\t\"fieldType\": \"number\",\n\t\t\t\"required\": false\n\t\t},\n\t\t{\n\t\t\t\"label\": \"Is this a test?\",\n\t\t\t\"fieldType\": \"boolean\",\n\t\t\t\"required\": false\n\t\t},\n\t\t{\n\t\t\t\"label\": \"Favorite Food\",\n\t\t\t\"fieldType\": \"['Pizza', 'Schnitzel', 'Burger']\",\n\t\t\t\"required\": true\n\t\t},\n\t\t{\n\t\t\t\"label\": \"Favorite Foods\",\n\t\t\t\"fieldType\": \"{'Pizza', 'Schnitzel', 'Burger'}\",\n\t\t\t\"required\": true\n\t\t}\n\t]`\n\n\turequire.NotPanics(t, cur, func() {\n\t\tvar id string\n\t\tvar err error\n\t\tfunc(cur realm) {\n\t\t\tid, err = db.CreateForm(0, cur, title, description, openAt, closeAt, data)\n\t\t}(cross(cur))\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\turequire.True(t, id != \"\", \"Form ID is empty\")\n\n\t\tform, err := db.GetForm(id)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\turequire.True(t, form.ID == id, \"Form ID is not correct\")\n\t\turequire.Equal(t, form.Owner, alice, \"Owner is not correct\")\n\t\turequire.True(t, form.Title == title, \"Title is not correct\")\n\t\turequire.True(t, form.Description == description, \"Description is not correct\")\n\t\turequire.True(t, len(form.Fields) == 5, \"Not enough fields were provided\")\n\t\turequire.True(t, form.Fields[0].Label == \"Name\", \"Field 0 label is not correct\")\n\t\turequire.True(t, form.Fields[0].FieldType == \"string\", \"Field 0 type is not correct\")\n\t\turequire.True(t, form.Fields[0].Required == true, \"Field 0 required is not correct\")\n\t\turequire.True(t, form.Fields[1].Label == \"Age\", \"Field 1 label is not correct\")\n\t\turequire.True(t, form.Fields[1].FieldType == \"number\", \"Field 1 type is not correct\")\n\t})\n}\n"},{"name":"doc.gno","body":"// # Gno forms\n\n// gno-forms is a package which demonstrates a form editing and sharing application in gno\n\n// ## Features\n// - **Form Creation**: Create new forms with specified titles, descriptions, and fields.\n// - **Form Submission**: Submit answers to forms.\n// - **Form Retrieval**: Retrieve existing forms and their submissions.\n// - **Form Deadline**: Set a precise time range during which a form can be interacted with.\n\n// ## Field Types\n// The system supports the following field types:\n\n// | type         | example                                                                                         |\n// |--------------|-------------------------------------------------------------------------------------------------|\n// | string       | `{\"label\": \"Name\", \"fieldType\": \"string\", \"required\": true}`                                    |\n// | number       | `{\"label\": \"Age\", \"fieldType\": \"number\", \"required\": true}`                                     |\n// | boolean      | `{\"label\": \"Is Student?\", \"fieldType\": \"boolean\", \"required\": false}`                           |\n// | choice       | `{\"label\": \"Favorite Food\", \"fieldType\": \"['Pizza', 'Schnitzel', 'Burger']\", \"required\": true}` |\n// | multi-choice | `{\"label\": \"Hobbies\", \"fieldType\": \"{'Reading', 'Swimming', 'Gaming'}\", \"required\": false}`     |\n\n// ## Web-app\n\n// The external repo where the initial development took place and where you can find the frontend is [here](https://github.com/agherasie/gno-forms).\npackage forms\n"},{"name":"errors.gno","body":"package forms\n\nimport \"errors\"\n\nvar (\n\terrNoOpenDate       = errors.New(\"Form has no open date\")\n\terrNoCloseDate      = errors.New(\"Form has no close date\")\n\terrInvalidJson      = errors.New(\"Invalid JSON\")\n\terrInvalidDate      = errors.New(\"Invalid date\")\n\terrFormNotFound     = errors.New(\"Form not found\")\n\terrAnswerNotFound   = errors.New(\"Answer not found\")\n\terrAlreadySubmitted = errors.New(\"You already submitted this form\")\n\terrFormClosed       = errors.New(\"Form is closed\")\n\terrInvalidAnswers   = errors.New(\"Invalid answers\")\n\terrSpoofedRealm     = errors.New(\"rlm does not match the current crossing frame\")\n)\n"},{"name":"forms.gno","body":"package forms\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/nt/seqid/v0\"\n)\n\n// FieldType examples :\n// - string: \"string\";\n// - number: \"number\";\n// - boolean: \"boolean\";\n// - choice: \"['Pizza', 'Schnitzel', 'Burger']\";\n// - multi-choice: \"{'Pizza', 'Schnitzel', 'Burger'}\";\ntype Field struct {\n\tLabel     string\n\tFieldType string\n\tRequired  bool\n}\n\ntype Form struct {\n\tID          string\n\tOwner       address\n\tTitle       string\n\tDescription string\n\tFields      []Field\n\tCreatedAt   time.Time\n\topenAt      time.Time\n\tcloseAt     time.Time\n}\n\n// Answers example :\n// - [\"Alex\", 21, true, 0, [0, 1]]\ntype Submission struct {\n\tFormID      string\n\tAuthor      address\n\tAnswers     string // json\n\tSubmittedAt time.Time\n}\n\ntype FormDB struct {\n\tForms     []*Form\n\tAnswers   []*Submission\n\tIDCounter seqid.ID\n}\n\nfunc NewDB() *FormDB {\n\treturn \u0026FormDB{\n\t\tForms:   make([]*Form, 0),\n\t\tAnswers: make([]*Submission, 0),\n\t}\n}\n\n// This function checks if the form is open by verifying the given dates\n// - If a form doesn't have any dates, it's considered open\n// - If a form has only an open date, it's considered open if the open date is in the past\n// - If a form has only a close date, it's considered open if the close date is in the future\n// - If a form has both open and close dates, it's considered open if the current date is between the open and close dates\nfunc (form *Form) IsOpen() bool {\n\topenAt, errOpen := form.OpenAt()\n\tclosedAt, errClose := form.CloseAt()\n\n\tnoOpenDate := errOpen != nil\n\tnoCloseDate := errClose != nil\n\n\tif noOpenDate \u0026\u0026 noCloseDate {\n\t\treturn true\n\t}\n\n\tif noOpenDate \u0026\u0026 !noCloseDate {\n\t\treturn time.Now().Before(closedAt)\n\t}\n\n\tif !noOpenDate \u0026\u0026 noCloseDate {\n\t\treturn time.Now().After(openAt)\n\t}\n\n\tnow := time.Now()\n\treturn now.After(openAt) \u0026\u0026 now.Before(closedAt)\n}\n\n// OpenAt returns the open date of the form if it exists\nfunc (form *Form) OpenAt() (time.Time, error) {\n\tif form.openAt.IsZero() {\n\t\treturn time.Time{}, errNoOpenDate\n\t}\n\n\treturn form.openAt, nil\n}\n\n// CloseAt returns the close date of the form if it exists\nfunc (form *Form) CloseAt() (time.Time, error) {\n\tif form.closeAt.IsZero() {\n\t\treturn time.Time{}, errNoCloseDate\n\t}\n\n\treturn form.closeAt, nil\n}\n\n// GetForm returns a form by its ID if it exists\nfunc (db *FormDB) GetForm(id string) (*Form, error) {\n\tfor _, form := range db.Forms {\n\t\tif form.ID == id {\n\t\t\treturn form, nil\n\t\t}\n\t}\n\treturn nil, errFormNotFound\n}\n\n// GetAnswer returns an answer by its form - and author ids if it exists\nfunc (db *FormDB) GetAnswer(formID string, author address) (*Submission, error) {\n\tfor _, answer := range db.Answers {\n\t\tif answer.FormID == formID \u0026\u0026 answer.Author.String() == author.String() {\n\t\t\treturn answer, nil\n\t\t}\n\t}\n\treturn nil, errAnswerNotFound\n}\n\n// GetSubmissionsByFormID returns a list containing the existing form submissions by the form ID\nfunc (db *FormDB) GetSubmissionsByFormID(formID string) []*Submission {\n\tsubmissions := make([]*Submission, 0)\n\n\tfor _, answer := range db.Answers {\n\t\tif answer.FormID == formID {\n\t\t\tsubmissions = append(submissions, answer)\n\t\t}\n\t}\n\n\treturn submissions\n}\n"},{"name":"forms_json.gno","body":"package forms\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/onbloc/json/v0\"\n)\n\ntype FormNodeBuilder struct {\n\t*json.NodeBuilder\n}\n\ntype FormArrayBuilder struct {\n\t*json.ArrayBuilder\n}\n\nfunc (b *FormNodeBuilder) WriteArray(key string, fn func(*FormArrayBuilder)) *FormNodeBuilder {\n\tb.NodeBuilder.WriteArray(key, func(ab *json.ArrayBuilder) {\n\t\tfn(\u0026FormArrayBuilder{ab})\n\t})\n\treturn b\n}\n\nfunc (b *FormNodeBuilder) WriteObject(key string, fn func(*FormNodeBuilder)) *FormNodeBuilder {\n\tb.NodeBuilder.WriteObject(key, func(nb *json.NodeBuilder) {\n\t\tfn(\u0026FormNodeBuilder{nb})\n\t})\n\treturn b\n}\n\nfunc (b *FormArrayBuilder) WriteObject(fn func(*FormNodeBuilder)) *FormArrayBuilder {\n\tb.ArrayBuilder.WriteObject(func(nb *json.NodeBuilder) {\n\t\tfn(\u0026FormNodeBuilder{nb})\n\t})\n\treturn b\n}\n\nfunc (b *FormNodeBuilder) WriteFormFields(key string, fields []Field) *FormNodeBuilder {\n\tb.WriteArray(key, func(builder *FormArrayBuilder) {\n\t\tfor _, field := range fields {\n\t\t\tbuilder.WriteObject(func(builder *FormNodeBuilder) {\n\t\t\t\tbuilder.WriteString(\"label\", field.Label).\n\t\t\t\t\tWriteString(\"fieldType\", field.FieldType).\n\t\t\t\t\tWriteBool(\"required\", field.Required)\n\t\t\t})\n\t\t}\n\t})\n\treturn b\n}\n\nfunc (b *FormNodeBuilder) WriteFormSubmission(key string, submission *Submission) *FormNodeBuilder {\n\tb.WriteObject(key, func(builder *FormNodeBuilder) {\n\t\tbuilder.WriteString(\"submittedAt\", submission.SubmittedAt.Format(\"2006-01-02 15:04:05\")).\n\t\t\tWriteString(\"answers\", strings.ReplaceAll(submission.Answers, \"\\\"\", \"'\"))\n\t})\n\treturn b\n}\n\nfunc (b *FormNodeBuilder) WriteForm(key string, value *Form) *FormNodeBuilder {\n\tb.WriteString(\"id\", value.ID).\n\t\tWriteString(\"owner\", value.Owner.String()).\n\t\tWriteString(\"title\", value.Title).\n\t\tWriteString(\"description\", value.Description).\n\t\tWriteString(\"createdAt\", value.CreatedAt.Format(\"2006-01-02 15:04:05\"))\n\tb.WriteFormFields(\"fields\", value.Fields)\n\treturn b\n}\n\nfunc (b *FormArrayBuilder) WriteForm(key string, value *Form) *FormArrayBuilder {\n\tb.WriteObject(func(builder *FormNodeBuilder) {\n\t\tbuilder.WriteString(\"id\", value.ID).\n\t\t\tWriteString(\"owner\", value.Owner.String()).\n\t\t\tWriteString(\"title\", value.Title).\n\t\t\tWriteString(\"description\", value.Description).\n\t\t\tWriteString(\"createdAt\", value.CreatedAt.Format(\"2006-01-02 15:04:05\"))\n\t\tbuilder.WriteFormFields(\"fields\", value.Fields)\n\t})\n\treturn b\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/agherasie/forms\"\ngno = \"0.9\"\n"},{"name":"submit.gno","body":"package forms\n\nimport (\n\t\"time\"\n)\n\n// SubmitForm records an answer to a form. rlm must be the caller's own\n// captured cur; the Author is set to rlm.Previous().Address() —\n// unforgeable, vs. the prior runtime walk a non-crossing wrapper could\n// spoof.\nfunc (db *FormDB) SubmitForm(_ int, rlm realm, formID string, answers string) {\n\t// Check if form exists\n\tform, err := db.GetForm(formID)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tif !rlm.IsCurrent() {\n\t\tpanic(errSpoofedRealm)\n\t}\n\tauthor := rlm.Previous().Address()\n\n\t// Check if form was already submitted by this user\n\tpreviousAnswer, _ := db.GetAnswer(formID, author)\n\tif previousAnswer != nil {\n\t\tpanic(errAlreadySubmitted)\n\t}\n\n\t// Check time restrictions\n\tif !form.IsOpen() {\n\t\tpanic(errFormClosed)\n\t}\n\n\t// Check if answers are formatted correctly\n\tif ValidateAnswers(answers, form.Fields) == false {\n\t\tpanic(errInvalidAnswers)\n\t}\n\n\t// Save answers\n\tanswer := Submission{\n\t\tFormID:      formID,\n\t\tAnswers:     answers,\n\t\tAuthor:      author,\n\t\tSubmittedAt: time.Now(),\n\t}\n\tdb.Answers = append(db.Answers, \u0026answer)\n}\n"},{"name":"submit_test.gno","body":"package forms\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestAnswerForm(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1user\"))\n\n\tdb := NewDB()\n\n\tdata := `[\n\t\t{\n\t\t\t\"label\": \"Name\",\n\t\t\t\"fieldType\": \"string\",\n\t\t\t\"required\": true\n\t\t},\n\t\t{\n\t\t\t\"label\": \"Age\",\n\t\t\t\"fieldType\": \"number\",\n\t\t\t\"required\": false\n\t\t},\n\t\t{\n\t\t\t\"label\": \"Is this a test?\",\n\t\t\t\"fieldType\": \"boolean\",\n\t\t\t\"required\": false\n\t\t},\n\t\t{\n\t\t\t\"label\": \"Favorite Food\",\n\t\t\t\"fieldType\": \"[Pizza|Schnitzel|Burger]\",\n\t\t\t\"required\": true\n\t\t},\n\t\t{\n\t\t\t\"label\": \"Favorite Foods\",\n\t\t\t\"fieldType\": \"{Pizza|Schnitzel|Burger}\",\n\t\t\t\"required\": true\n\t\t}\n\t]`\n\n\tvar formID string\n\tvar err error\n\tfunc(cur realm) {\n\t\tformID, err = db.CreateForm(0, cur, \"Test Form\", \"Test Description\", \"\", \"\", data)\n\t}(cross(cur))\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\tanswers := `[\"Alex\", 21, true, 0, [0, 1]]`\n\tfunc(cur realm) { db.SubmitForm(0, cur, formID, answers) }(cross(cur))\n\n\turequire.True(t, len(db.Answers) == 1, \"Expected 1 answer, got\", string(len(db.Answers)))\n\turequire.True(t, db.Answers[0].FormID == formID, \"Expected form ID\", formID, \"got\", db.Answers[0].FormID)\n\turequire.True(t, db.Answers[0].Answers == answers, \"Expected answers\", answers, \"got\", db.Answers[0].Answers)\n\turequire.True(t, err == nil, \"Submit should not return an error\")\n}\n\nfunc TestAnswerFormDates(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1user\"))\n\tdb := NewDB()\n\n\tnow := time.Now()\n\ttomorrow := now.AddDate(0, 0, 1).Format(\"2006-01-02T15:04:05Z\")\n\tyesterday := now.AddDate(0, 0, -1).Format(\"2006-01-02T15:04:05Z\")\n\n\tdata := `[\n\t\t{\n\t\t\t\"label\": \"Name\",\n\t\t\t\"fieldType\": \"string\",\n\t\t\t\"required\": true\n\t\t}\n\t]`\n\tanswers := `[\"Test\"]`\n\n\turequire.AbortsWithMessage(t, cur, \"Form is closed\", func() {\n\t\tvar formID string\n\t\tvar err error\n\t\tfunc(cur realm) {\n\t\t\tformID, err = db.CreateForm(0, cur, \"Test Form\", \"Test Description\", tomorrow, \"\", data)\n\t\t}(cross(cur))\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tfunc(cur realm) { db.SubmitForm(0, cur, formID, answers) }(cross(cur))\n\t})\n\n\turequire.AbortsWithMessage(t, cur, \"Form is closed\", func() {\n\t\tvar formID string\n\t\tvar err error\n\t\tfunc(cur realm) {\n\t\t\tformID, err = db.CreateForm(0, cur, \"Test Form\", \"Test Description\", \"\", yesterday, data)\n\t\t}(cross(cur))\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tfunc(cur realm) { db.SubmitForm(0, cur, formID, answers) }(cross(cur))\n\t})\n\n\turequire.NotAborts(t, cur, func() {\n\t\tvar formID string\n\t\tvar err error\n\t\tfunc(cur realm) {\n\t\t\tformID, err = db.CreateForm(0, cur, \"Test Form\", \"Test Description\", yesterday, tomorrow, data)\n\t\t}(cross(cur))\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tfunc(cur realm) { db.SubmitForm(0, cur, formID, answers) }(cross(cur))\n\t})\n}\n"},{"name":"validate.gno","body":"package forms\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/onbloc/json/v0\"\n)\n\nfunc validateBooleanField(node *json.Node, field Field) bool {\n\tif node.IsBool() == false {\n\t\treturn false\n\t}\n\n\tanswer, err := node.GetBool()\n\tif err != nil {\n\t\treturn false\n\t}\n\n\t// If the field is required, checkbox must be checked\n\tif field.Required == true \u0026\u0026 answer == false {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\nfunc validateStringField(node *json.Node, field Field) bool {\n\tif node.IsString() == false {\n\t\treturn false\n\t}\n\n\tanswer, err := node.GetString()\n\tif err != nil {\n\t\treturn false\n\t}\n\n\t// If the field is required, the answer must not be empty\n\tif field.Required == true \u0026\u0026 answer == \"\" {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\nfunc validateNumberField(node *json.Node, field Field) bool {\n\tif node.IsNumber() == false {\n\t\treturn false\n\t}\n\n\t_, err := node.GetNumeric()\n\tif err != nil {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\nfunc validateMultiChoiceField(node *json.Node, field Field) bool {\n\tchoices := strings.Split(field.FieldType[1:len(field.FieldType)-1], \"|\")\n\n\tif node.IsArray() == false {\n\t\treturn false\n\t}\n\n\tif field.Required == true \u0026\u0026 node.Size() == 0 {\n\t\treturn false\n\t}\n\n\tif node.Size() \u003e len(choices) {\n\t\treturn false\n\t}\n\n\tfor i := 0; i \u003c node.Size(); i++ {\n\t\tchoiceNode, err := node.GetIndex(i)\n\t\tif err != nil {\n\t\t\treturn false\n\t\t}\n\n\t\tchoiceIdx := choiceNode.MustNumeric()\n\t\tif choiceIdx \u003c 0 || int(choiceIdx) \u003e= len(choices) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\nfunc validateChoiceField(node *json.Node, field Field) bool {\n\tchoices := strings.Split(field.FieldType[1:len(field.FieldType)-1], \"|\")\n\n\tif node.IsNumber() == false {\n\t\treturn false\n\t}\n\n\tchoiceIdx := node.MustNumeric()\n\tif choiceIdx \u003c 0 || int(choiceIdx) \u003e= len(choices) {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\nfunc ValidateAnswer(answer *json.Node, field Field) bool {\n\tif field.FieldType == \"boolean\" {\n\t\treturn validateBooleanField(answer, field)\n\t} else if field.FieldType == \"string\" {\n\t\treturn validateStringField(answer, field)\n\t} else if field.FieldType == \"number\" {\n\t\treturn validateNumberField(answer, field)\n\t} else if strings.HasPrefix(field.FieldType, \"{\") \u0026\u0026 strings.HasSuffix(field.FieldType, \"}\") {\n\t\treturn validateMultiChoiceField(answer, field)\n\t} else if strings.HasPrefix(field.FieldType, \"[\") \u0026\u0026 strings.HasSuffix(field.FieldType, \"]\") {\n\t\treturn validateChoiceField(answer, field)\n\t}\n\n\treturn false\n}\n\n// ValidateAnswers checks if the given answers are valid for the given fields\nfunc ValidateAnswers(answers string, fields []Field) bool {\n\tunmarshalled, err := json.Unmarshal([]byte(answers))\n\tif err != nil {\n\t\treturn false\n\t}\n\n\t// If the number of answers is different from the number of fields, it's invalid\n\tif len(fields) != unmarshalled.Size() {\n\t\treturn false\n\t}\n\n\tfor i, field := range fields {\n\t\tanswer, err := unmarshalled.GetIndex(i)\n\t\tif err != nil {\n\t\t\treturn false\n\t\t}\n\n\t\t// If the answer is empty and the field is not required, it's valid\n\t\tif answer.IsNull() \u0026\u0026 !field.Required {\n\t\t\treturn true\n\t\t}\n\n\t\tif !ValidateAnswer(answer, field) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n"},{"name":"validate_test.gno","body":"package forms\n\nimport (\n\t\"testing\"\n)\n\nfunc TestAnswerFormInvalidForm(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1user\"))\n\tdb := NewDB()\n\n\tdataAllTypes := `[\n\t\t{\n\t\t\t\"label\": \"Name\",\n\t\t\t\"fieldType\": \"string\",\n\t\t\t\"required\": true\n\t\t},\n\t\t{\n\t\t\t\"label\": \"Age\",\n\t\t\t\"fieldType\": \"number\",\n\t\t\t\"required\": false\n\t\t},\n\t\t{\n\t\t\t\"label\": \"Is this a test?\",\n\t\t\t\"fieldType\": \"boolean\",\n\t\t\t\"required\": false\n\t\t},\n\t\t{\n\t\t\t\"label\": \"Favorite Food\",\n\t\t\t\"fieldType\": \"[Pizza|Schnitzel|Burger]\",\n\t\t\t\"required\": true\n\t\t},\n\t\t{\n\t\t\t\"label\": \"Favorite Foods\",\n\t\t\t\"fieldType\": \"{Pizza|Schnitzel|Burger}\",\n\t\t\t\"required\": true\n\t\t}\n\t]`\n\tdataOneRequiredText := `[\n\t\t{\n\t\t\t\"label\": \"Name\",\n\t\t\t\"fieldType\": \"string\",\n\t\t\t\"required\": true\n\t\t}\n\t]`\n\n\ttests := []struct {\n\t\tname        string\n\t\tanswer      string\n\t\texpectPanic bool\n\t\tdata        string\n\t}{\n\t\t{\n\t\t\tname:        \"correct\",\n\t\t\tanswer:      `[\"Alex\", 21, true, 0, [0, 1]]`,\n\t\t\texpectPanic: false,\n\t\t\tdata:        dataAllTypes,\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid string\",\n\t\t\tanswer:      `[0, 21, true, 0, [0, 1]`,\n\t\t\texpectPanic: true,\n\t\t\tdata:        dataAllTypes,\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid number\",\n\t\t\tanswer:      `[\"Alex\", \"21\", true, 0, [0, 1]]`,\n\t\t\texpectPanic: true,\n\t\t\tdata:        dataAllTypes,\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid boolean\",\n\t\t\tanswer:      `[\"Alex\", 21, 1, 0, [0, 1]]`,\n\t\t\texpectPanic: true,\n\t\t\tdata:        dataAllTypes,\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid choice\",\n\t\t\tanswer:      `[\"Alex\", 21, true, 10, [0, 1]]`,\n\t\t\texpectPanic: true,\n\t\t\tdata:        dataAllTypes,\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid multi-choice 1\",\n\t\t\tanswer:      `[\"Alex\", 21, true, 0, [0, 1, 2, 3, 4, 5]]`,\n\t\t\texpectPanic: true,\n\t\t\tdata:        dataAllTypes,\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid multi-choice 2\",\n\t\t\tanswer:      `[\"Alex\", 21, true, 0, [5]]`,\n\t\t\texpectPanic: true,\n\t\t\tdata:        dataAllTypes,\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid multi-choice 3\",\n\t\t\tanswer:      `[\"Alex\", 21, true, 0, 0]`,\n\t\t\texpectPanic: true,\n\t\t\tdata:        dataAllTypes,\n\t\t},\n\t\t{\n\t\t\tname:        \"required string\",\n\t\t\tanswer:      `[\"\", 21, true, 0, [0, 1]]`,\n\t\t\texpectPanic: true,\n\t\t\tdata:        dataAllTypes,\n\t\t},\n\t\t{\n\t\t\tname:        \"unrequired number\",\n\t\t\tanswer:      `[\"Alex\", null, true, 0, [0, 1]]`,\n\t\t\texpectPanic: false,\n\t\t\tdata:        dataAllTypes,\n\t\t},\n\t\t{\n\t\t\tname:        \"correct one field\",\n\t\t\tanswer:      `[\"Alex\"]`,\n\t\t\texpectPanic: false,\n\t\t\tdata:        dataOneRequiredText,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar formID string\n\t\t\tvar err error\n\t\t\tfunc(cur realm) {\n\t\t\t\tformID, err = db.CreateForm(0, cur, \"Test Form\", \"Test Description\", \"\", \"\", tt.data)\n\t\t\t}(cross(cur))\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\n\t\t\t// SubmitForm crosses a realm boundary (cross(cur)), so a\n\t\t\t// panic from it becomes a cross-realm \"abort\". A plain\n\t\t\t// defer-recover won't see it; use revive() instead.\n\t\t\tr := revive(func() {\n\t\t\t\tfunc(cur realm) { db.SubmitForm(0, cur, formID, tt.answer) }(cross(cur))\n\t\t\t})\n\t\t\tif r != nil \u0026\u0026 !tt.expectPanic {\n\t\t\t\tt.Errorf(\"%s panic occurred when not expected: %v\", tt.name, r)\n\t\t\t} else if r == nil \u0026\u0026 tt.expectPanic {\n\t\t\t\tt.Errorf(\"%s expected panic but didn't occur\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"67h9K9M4WNovx6yPWD76zTIj1tmnwfcCCsGYKLXSziFbJN+Qc1xciYNoTJzPl+txM8DE/hgfy4/Srx+BL7dVZw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"acl","path":"gno.land/p/archive/acl","files":[{"name":"acl.gno","body":"package acl\n\nimport \"gno.land/p/nt/avl/v0\"\n\nfunc New() *Directory {\n\treturn \u0026Directory{\n\t\tuserGroups:  avl.Tree{},\n\t\tpermBuckets: avl.Tree{},\n\t}\n}\n\ntype Directory struct {\n\tpermBuckets avl.Tree // identifier -\u003e perms\n\tuserGroups  avl.Tree // chain.Address -\u003e []string\n}\n\nfunc (d *Directory) HasPerm(addr address, verb, resource string) bool {\n\t// FIXME: consider memoize.\n\n\t// user perms\n\tif d.getBucketPerms(\"u:\"+addr.String()).hasPerm(verb, resource) {\n\t\treturn true\n\t}\n\n\t// everyone's perms.\n\tif d.getBucketPerms(\"g:\"+Everyone).hasPerm(verb, resource) {\n\t\treturn true\n\t}\n\n\t// user groups' perms.\n\tgroups := d.userGroups.Get(addr.String())\n\tif groups != nil {\n\t\tfor _, group := range groups.([]string) {\n\t\t\tif d.getBucketPerms(\"g:\"+group).hasPerm(verb, resource) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc (d *Directory) getBucketPerms(bucket string) perms {\n\tres := d.permBuckets.Get(bucket)\n\tif res != nil {\n\t\treturn res.(perms)\n\t}\n\treturn perms{}\n}\n\nfunc (d *Directory) HasRole(addr address, role string) bool {\n\treturn d.HasPerm(addr, \"role\", role)\n}\n\nfunc (d *Directory) AddUserPerm(addr address, verb, resource string) {\n\tbucket := \"u:\" + addr.String()\n\tp := perm{\n\t\tverbs:     []string{verb},\n\t\tresources: []string{resource},\n\t}\n\td.addPermToBucket(bucket, p)\n}\n\nfunc (d *Directory) AddGroupPerm(name string, verb, resource string) {\n\tbucket := \"g:\" + name\n\tp := perm{\n\t\tverbs:     []string{verb},\n\t\tresources: []string{resource},\n\t}\n\td.addPermToBucket(bucket, p)\n}\n\nfunc (d *Directory) addPermToBucket(bucket string, p perm) {\n\tvar ps perms\n\n\texisting := d.permBuckets.Get(bucket)\n\tif existing != nil {\n\t\tps = existing.(perms)\n\t}\n\tps = append(ps, p)\n\n\td.permBuckets.Set(bucket, ps)\n}\n\nfunc (d *Directory) AddUserToGroup(user address, group string) {\n\texisting := d.userGroups.Get(user.String())\n\tvar groups []string\n\tif existing != nil {\n\t\tgroups = existing.([]string)\n\t}\n\tgroups = append(groups, group)\n\td.userGroups.Set(user.String(), groups)\n}\n\n// TODO: helpers to remove permissions.\n// TODO: helpers to adds multiple permissions at once -\u003e {verbs: []string{\"read\",\"write\"}}.\n// TODO: helpers to delete users from gorups.\n// TODO: helpers to quickly reset states.\n"},{"name":"acl_test.gno","body":"package acl\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nfunc Test(t *testing.T) {\n\tadm := testutils.TestAddress(\"admin\")\n\tmod := testutils.TestAddress(\"mod\")\n\tusr := testutils.TestAddress(\"user\")\n\tcst := testutils.TestAddress(\"custom\")\n\n\tdir := New()\n\n\t// by default, no one has perm.\n\tshouldNotHasRole(t, dir, adm, \"foo\")\n\tshouldNotHasRole(t, dir, mod, \"foo\")\n\tshouldNotHasRole(t, dir, usr, \"foo\")\n\tshouldNotHasRole(t, dir, cst, \"foo\")\n\tshouldNotHasPerm(t, dir, adm, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, mod, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, cst, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, adm, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, mod, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, cst, \"read\", \"r/demo/boards:gnolang/1\")\n\n\t// adding all the rights to admin.\n\tdir.AddUserPerm(adm, \".*\", \".*\")\n\tshouldHasRole(t, dir, adm, \"foo\")\n\tshouldNotHasRole(t, dir, mod, \"foo\")\n\tshouldNotHasRole(t, dir, usr, \"foo\")\n\tshouldNotHasRole(t, dir, cst, \"foo\")\n\tshouldHasPerm(t, dir, adm, \"write\", \"r/demo/boards:gnolang/1\") // new\n\tshouldNotHasPerm(t, dir, mod, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, cst, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, adm, \"read\", \"r/demo/boards:gnolang/1\") // new\n\tshouldNotHasPerm(t, dir, mod, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, cst, \"read\", \"r/demo/boards:gnolang/1\")\n\n\t// adding custom regexp rule for user \"cst\".\n\tdir.AddUserPerm(cst, \"write\", \"r/demo/boards:gnolang/.*\")\n\tshouldHasRole(t, dir, adm, \"foo\")\n\tshouldNotHasRole(t, dir, mod, \"foo\")\n\tshouldNotHasRole(t, dir, usr, \"foo\")\n\tshouldNotHasRole(t, dir, cst, \"foo\")\n\tshouldHasPerm(t, dir, adm, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, mod, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, cst, \"write\", \"r/demo/boards:gnolang/1\") // new\n\tshouldHasPerm(t, dir, adm, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, mod, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, cst, \"read\", \"r/demo/boards:gnolang/1\")\n\n\t// adding a group perm for a new group.\n\t// no changes expected.\n\tdir.AddGroupPerm(\"mods\", \"role\", \"moderator\")\n\tdir.AddGroupPerm(\"mods\", \"write\", \".*\")\n\tshouldHasRole(t, dir, adm, \"foo\")\n\tshouldNotHasRole(t, dir, mod, \"foo\")\n\tshouldNotHasRole(t, dir, usr, \"foo\")\n\tshouldNotHasRole(t, dir, cst, \"foo\")\n\tshouldHasPerm(t, dir, adm, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, mod, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, cst, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, adm, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, mod, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, cst, \"read\", \"r/demo/boards:gnolang/1\")\n\n\t// assigning the user \"mod\" to the \"mods\" group.\n\tdir.AddUserToGroup(mod, \"mods\")\n\tshouldHasRole(t, dir, adm, \"foo\")\n\tshouldNotHasRole(t, dir, mod, \"foo\")\n\tshouldNotHasRole(t, dir, usr, \"foo\")\n\tshouldNotHasRole(t, dir, cst, \"foo\")\n\tshouldHasPerm(t, dir, adm, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, mod, \"write\", \"r/demo/boards:gnolang/1\") // new\n\tshouldNotHasPerm(t, dir, usr, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, cst, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, adm, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, mod, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, cst, \"read\", \"r/demo/boards:gnolang/1\")\n\n\t// adding \"read\" permission for everyone.\n\tdir.AddGroupPerm(Everyone, \"read\", \".*\")\n\tshouldHasRole(t, dir, adm, \"foo\")\n\tshouldNotHasRole(t, dir, mod, \"foo\")\n\tshouldNotHasRole(t, dir, usr, \"foo\")\n\tshouldNotHasRole(t, dir, cst, \"foo\")\n\tshouldHasPerm(t, dir, adm, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, mod, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, cst, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, adm, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, mod, \"read\", \"r/demo/boards:gnolang/1\") // new\n\tshouldHasPerm(t, dir, usr, \"read\", \"r/demo/boards:gnolang/1\") // new\n\tshouldHasPerm(t, dir, cst, \"read\", \"r/demo/boards:gnolang/1\") // new\n}\n\nfunc shouldHasRole(t *testing.T, dir *Directory, addr address, role string) {\n\tt.Helper()\n\tcheck := dir.HasRole(addr, role)\n\tuassert.Equal(t, true, check, ufmt.Sprintf(\"%s should has role %s\", addr.String(), role))\n}\n\nfunc shouldNotHasRole(t *testing.T, dir *Directory, addr address, role string) {\n\tt.Helper()\n\tcheck := dir.HasRole(addr, role)\n\tuassert.Equal(t, false, check, ufmt.Sprintf(\"%s should not has role %s\", addr.String(), role))\n}\n\nfunc shouldHasPerm(t *testing.T, dir *Directory, addr address, verb string, resource string) {\n\tt.Helper()\n\tcheck := dir.HasPerm(addr, verb, resource)\n\tuassert.Equal(t, true, check, ufmt.Sprintf(\"%s should has perm for %s - %s\", addr.String(), verb, resource))\n}\n\nfunc shouldNotHasPerm(t *testing.T, dir *Directory, addr address, verb string, resource string) {\n\tt.Helper()\n\tcheck := dir.HasPerm(addr, verb, resource)\n\tuassert.Equal(t, false, check, ufmt.Sprintf(\"%s should not has perm for %s - %s\", addr.String(), verb, resource))\n}\n"},{"name":"const.gno","body":"package acl\n\nconst Everyone string = \"everyone\"\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/archive/acl\"\ngno = \"0.9\"\n"},{"name":"perm.gno","body":"package acl\n\nimport \"regexp\"\n\ntype perm struct {\n\tverbs     []string\n\tresources []string\n}\n\nfunc (perm perm) hasPerm(verb, resource string) bool {\n\t// check verb\n\tverbOK := false\n\tfor _, pattern := range perm.verbs {\n\t\tif match(pattern, verb) {\n\t\t\tverbOK = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !verbOK {\n\t\treturn false\n\t}\n\n\t// check resource\n\tfor _, pattern := range perm.resources {\n\t\tif match(pattern, resource) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc match(pattern, target string) bool {\n\tif pattern == \".*\" {\n\t\treturn true\n\t}\n\n\tif pattern == target {\n\t\treturn true\n\t}\n\n\t// regexp handling\n\tmatch, _ := regexp.MatchString(pattern, target)\n\treturn match\n}\n"},{"name":"perms.gno","body":"package acl\n\ntype perms []perm\n\nfunc (perms perms) hasPerm(verb, resource string) bool {\n\tfor _, perm := range perms {\n\t\tif perm.hasPerm(verb, resource) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"wr5olLRF7fn00Jch28hSlvERx6CdM+mzE+i/FpnA9xUWsB+vSThMEMkH8nnPT0dxsJt+uyaeHBf2s/F48m2PJg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"bank","path":"gno.land/p/archive/bank","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/archive/bank\"\ngno = \"0.9\"\n"},{"name":"types.gno","body":"// TODO: this is an example, and needs to be fixed up and tested.\n\npackage bank\n\n// NOTE: unexposed struct for security.\ntype order struct {\n\tfrom      Address\n\tto        Address\n\tamount    Coins\n\tprocessed bool\n}\n\n// NOTE: unexposed methods for security.\nfunc (ch *order) string() string {\n\treturn \"TODO\"\n}\n\n// Wraps the internal *order for external use.\ntype Order struct {\n\t*order\n}\n\n// XXX only exposed for demonstration. TODO unexpose, make full demo.\nfunc NewOrder(from Address, to Address, amount Coins) Order {\n\treturn Order{\n\t\torder: \u0026order{\n\t\t\tfrom:   from,\n\t\t\tto:     to,\n\t\t\tamount: amount,\n\t\t},\n\t}\n}\n\n// Panics if error, or already processed.\nfunc (o Order) Execute() {\n\tif o.order.processed {\n\t\tpanic(\"order already processed\")\n\t}\n\to.order.processed = true\n\t// TODO implemement.\n}\n\nfunc (o Order) IsZero() bool {\n\treturn o.order == nil\n}\n\nfunc (o Order) From() Address {\n\treturn o.order.from\n}\n\nfunc (o Order) To() Address {\n\treturn o.order.to\n}\n\nfunc (o Order) Amount() Coins {\n\treturn o.order.amount\n}\n\nfunc (o Order) Processed() bool {\n\treturn o.order.processed\n}\n\n//----------------------------------------\n// Escrow\n\ntype EscrowTerms struct {\n\tPartyA  Address\n\tPartyB  Address\n\tAmountA Coins\n\tAmountB Coins\n}\n\ntype EscrowContract struct {\n\tEscrowTerms\n\tOrderA Order\n\tOrderB Order\n}\n\nfunc CreateEscrow(terms EscrowTerms) *EscrowContract {\n\treturn \u0026EscrowContract{\n\t\tEscrowTerms: terms,\n\t}\n}\n\nfunc (esc *EscrowContract) SetOrderA(order Order) {\n\tif !esc.OrderA.IsZero() {\n\t\tpanic(\"order-a already set\")\n\t}\n\tif esc.EscrowTerms.PartyA != order.From() {\n\t\tpanic(\"invalid order-a:from mismatch\")\n\t}\n\tif esc.EscrowTerms.PartyB != order.To() {\n\t\tpanic(\"invalid order-a:to mismatch\")\n\t}\n\tif !esc.EscrowTerms.AmountA.Equal(order.Amount()) {\n\t\tpanic(\"invalid order-a amount\")\n\t}\n\tesc.OrderA = order\n}\n\nfunc (esc *EscrowContract) SetOrderB(order Order) {\n\tif !esc.OrderB.IsZero() {\n\t\tpanic(\"order-b already set\")\n\t}\n\tif esc.EscrowTerms.PartyB != order.From() {\n\t\tpanic(\"invalid order-b:from mismatch\")\n\t}\n\tif esc.EscrowTerms.PartyA != order.To() {\n\t\tpanic(\"invalid order-b:to mismatch\")\n\t}\n\tif !esc.EscrowTerms.AmountB.Equal(order.Amount()) {\n\t\tpanic(\"invalid order-b amount\")\n\t}\n\tesc.OrderA = order\n}\n\nfunc (esc *EscrowContract) Execute() {\n\tif esc.OrderA.IsZero() {\n\t\tpanic(\"order-a not yet set\")\n\t}\n\tif esc.OrderB.IsZero() {\n\t\tpanic(\"order-b not yet set\")\n\t}\n\t// NOTE: succeeds atomically.\n\tesc.OrderA.Execute()\n\tesc.OrderB.Execute()\n}\n\n//----------------------------------------\n// TODO: actually implement these in std package.\n\ntype (\n\tAddress string\n\tCoins   []Coin\n\tCoin    struct {\n\t\tDenom  bool\n\t\tAmount int64\n\t}\n)\n\nfunc (a Coins) Equal(b Coins) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i, v := range a {\n\t\tif v != b[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"b8WaLVjbVtAVJho9Z6wZyZ4e2xxwplh/zEJmEwVvQr0GTh8T1Yae2nvZaRIxwJ1vsvMwR8f8/FkujMP41St+sw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"gnode","path":"gno.land/p/archive/gnode","files":[{"name":"gnode.gno","body":"package gnode\n\n// XXX what about Gnodes signing on behalf of others?\n// XXX like a multi-sig of Gnodes?\n\ntype Name string\n\ntype Gnode interface {\n\t//----------------------------------------\n\t// Basic properties\n\tGetName() Name\n\n\t//----------------------------------------\n\t// Affiliate Gnodes\n\tNumAffiliates() int\n\tGetAffiliates(Name) Affiliate\n\tAddAffiliate(Affiliate) error // must be affiliated\n\tRemAffiliate(Name) error      // must have become unaffiliated\n\n\t//----------------------------------------\n\t// Signing\n\tNumSignedDocuments() int\n\tGetSignedDocument(idx int) Document\n\tSignDocument(doc Document) (int, error) // index relative to signer\n\n\t//----------------------------------------\n\t// Rendering\n\tRenderLines() []string\n}\n\ntype Affiliate struct {\n\tType  string\n\tGnode Gnode\n\tTags  []string\n}\n\ntype MyGnode struct {\n\tName\n\t// Owners     // voting set, something that gives authority of action.\n\t// Treasury   //\n\t// Affiliates //\n\t// Board      // discussions\n\t// Data       // XXX ?\n}\n\ntype Affiliates []*Affiliate\n\n// Documents are equal if they compare equal.\n// NOTE: requires all fields to be comparable.\ntype Document struct {\n\tAuthors string\n\t// Timestamp\n\t// Body\n\t// Attachments\n}\n\n// ACTIONS\n\n// * Lend tokens\n// * Pay tokens\n// * Administrate transferrable and non-transferrable tokens\n// * Sum tokens\n// * Passthrough dependencies\n// * Code\n// * ...\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/archive/gnode\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"U/9J04CqAmzX/CD1bZau91BW+pJHmbJ5Ldt29Rdm2WIPe1Jo2sx+2S8Amhp+LkpaUTE+cQ59zMyf1ZwwMRv6pQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"rat","path":"gno.land/p/archive/rat","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/archive/rat\"\ngno = \"0.9\"\n"},{"name":"maths.gno","body":"package rat\n\nconst (\n\tintSize = 32 \u003c\u003c (^uint(0) \u003e\u003e 63) // 32 or 64\n\n\tMaxInt    = 1\u003c\u003c(intSize-1) - 1\n\tMinInt    = -1 \u003c\u003c (intSize - 1)\n\tMaxInt8   = 1\u003c\u003c7 - 1\n\tMinInt8   = -1 \u003c\u003c 7\n\tMaxInt16  = 1\u003c\u003c15 - 1\n\tMinInt16  = -1 \u003c\u003c 15\n\tMaxInt32  = 1\u003c\u003c31 - 1\n\tMinInt32  = -1 \u003c\u003c 31\n\tMaxInt64  = 1\u003c\u003c63 - 1\n\tMinInt64  = -1 \u003c\u003c 63\n\tMaxUint   = 1\u003c\u003cintSize - 1\n\tMaxUint8  = 1\u003c\u003c8 - 1\n\tMaxUint16 = 1\u003c\u003c16 - 1\n\tMaxUint32 = 1\u003c\u003c32 - 1\n\tMaxUint64 = 1\u003c\u003c64 - 1\n)\n"},{"name":"rat.gno","body":"package rat\n\n//----------------------------------------\n// Rat fractions\n\n// represents a fraction.\ntype Rat struct {\n\tX int32\n\tY int32 // must be positive\n}\n\nfunc NewRat(x, y int32) Rat {\n\tif y \u003c= 0 {\n\t\tpanic(\"invalid std.Rat denominator\")\n\t}\n\treturn Rat{X: x, Y: y}\n}\n\nfunc (r1 Rat) IsValid() bool {\n\tif r1.Y \u003c= 0 {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (r1 Rat) Cmp(r2 Rat) int {\n\tif !r1.IsValid() {\n\t\tpanic(\"invalid std.Rat left operand\")\n\t}\n\tif !r2.IsValid() {\n\t\tpanic(\"invalid std.Rat right operand\")\n\t}\n\tvar p1, p2 int64\n\tp1 = int64(r1.X) * int64(r2.Y)\n\tp2 = int64(r1.Y) * int64(r2.X)\n\tif p1 \u003c p2 {\n\t\treturn -1\n\t} else if p1 == p2 {\n\t\treturn 0\n\t} else {\n\t\treturn 1\n\t}\n}\n\n//func (r1 Rat) Plus(r2 Rat) Rat {\n// XXX\n//}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"od64ZvIR21HUqXW4gjwcaGRjplMtfs7XFfEm4wUfEW8wf2s5XHhz1tk4dsCV5NjyRpT3iMKBRT570gryfgfIJw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"groups","path":"gno.land/p/archive/groups","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/archive/groups\"\ngno = \"0.9\"\n"},{"name":"vote_set.gno","body":"package groups\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/archive/rat\"\n)\n\n//----------------------------------------\n// VoteSet\n\ntype VoteSet interface {\n\t// number of present votes in set.\n\tSize() int\n\t// add or update vote for voter.\n\tSetVote(voter address, value string) error\n\t// count the number of votes for value.\n\tCountVotes(value string) int\n}\n\n//----------------------------------------\n// VoteList\n\ntype Vote struct {\n\tVoter address\n\tValue string\n}\n\ntype VoteList []Vote\n\nfunc NewVoteList() *VoteList {\n\treturn \u0026VoteList{}\n}\n\nfunc (vlist *VoteList) Size() int {\n\treturn len(*vlist)\n}\n\nfunc (vlist *VoteList) SetVote(voter address, value string) error {\n\t// TODO optimize with binary algorithm\n\tfor i, vote := range *vlist {\n\t\tif vote.Voter == voter {\n\t\t\t// update vote\n\t\t\t(*vlist)[i] = Vote{\n\t\t\t\tVoter: voter,\n\t\t\t\tValue: value,\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t}\n\t*vlist = append(*vlist, Vote{\n\t\tVoter: voter,\n\t\tValue: value,\n\t})\n\treturn nil\n}\n\nfunc (vlist *VoteList) CountVotes(target string) int {\n\t// TODO optimize with binary algorithm\n\tvar count int\n\tfor _, vote := range *vlist {\n\t\tif vote.Value == target {\n\t\t\tcount++\n\t\t}\n\t}\n\treturn count\n}\n\n//----------------------------------------\n// Committee\n\ntype Committee struct {\n\tQuorum    rat.Rat\n\tThreshold rat.Rat\n\tAddresses []address\n}\n\n//----------------------------------------\n// VoteSession\n// NOTE: this seems a bit too formal and\n// complicated vs what might be possible;\n// something simpler, more informal.\n\ntype SessionStatus int\n\nconst (\n\tSessionNew SessionStatus = iota\n\tSessionStarted\n\tSessionCompleted\n\tSessionCanceled\n)\n\ntype VoteSession struct {\n\tName      string\n\tCreator   address\n\tBody      string\n\tStart     time.Time\n\tDeadline  time.Time\n\tStatus    SessionStatus\n\tCommittee *Committee\n\tVotes     VoteSet\n\tChoices   []string\n\tResult    string\n}\n"},{"name":"z_1_filetest.gno","body":"// PKGPATH: gno.land/p/archive/groups_test\npackage groups_test\n\nimport (\n\t\"gno.land/p/archive/groups\"\n\t\"gno.land/p/nt/testutils/v0\"\n)\n\nvar vset groups.VoteSet\n\nfunc init() {\n\taddr1 := testutils.TestAddress(\"test1\")\n\taddr2 := testutils.TestAddress(\"test2\")\n\tvset = groups.NewVoteList()\n\tvset.SetVote(addr1, \"yes\")\n\tvset.SetVote(addr2, \"yes\")\n}\n\nfunc main() {\n\tprintln(vset.Size())\n\tprintln(\"yes:\", vset.CountVotes(\"yes\"))\n\tprintln(\"no:\", vset.CountVotes(\"no\"))\n}\n\n// Output:\n// 2\n// yes: 2\n// no: 0\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"AtupT+fQuSUj8Gsq6nKJgepaKCQ9gkegLAcdVFYR6Y4+5yxYaxvjE3TV842RE+rRBBLFkXXHmxQynBDjLkxDGA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"ui","path":"gno.land/p/archive/ui","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/archive/ui\"\ngno = \"0.9\"\n"},{"name":"ui.gno","body":"package ui\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n)\n\ntype DOM struct {\n\t// metadata\n\tPrefix       string\n\tTitle        string\n\tWithComments bool\n\tClasses      []string\n\n\t// elements\n\tHeader Element\n\tBody   Element\n\tFooter Element\n}\n\nfunc (dom DOM) String() string {\n\tclasses := strings.Join(dom.Classes, \" \")\n\n\toutput := \"\"\n\n\tif classes != \"\" {\n\t\toutput += \"\u003cmain class='\" + classes + \"'\u003e\" + \"\\n\\n\"\n\t}\n\n\tif dom.Title != \"\" {\n\t\toutput += H1(dom.Title).String(dom) + \"\\n\"\n\t}\n\n\tif header := dom.Header.String(dom); header != \"\" {\n\t\tif dom.WithComments {\n\t\t\toutput += \"\u003c!-- header --\u003e\"\n\t\t}\n\t\toutput += header + \"\\n\"\n\t\tif dom.WithComments {\n\t\t\toutput += \"\u003c!-- /header --\u003e\"\n\t\t}\n\t}\n\n\tif body := dom.Body.String(dom); body != \"\" {\n\t\tif dom.WithComments {\n\t\t\toutput += \"\u003c!-- body --\u003e\"\n\t\t}\n\t\toutput += body + \"\\n\"\n\t\tif dom.WithComments {\n\t\t\toutput += \"\u003c!-- /body --\u003e\"\n\t\t}\n\t}\n\n\tif footer := dom.Footer.String(dom); footer != \"\" {\n\t\tif dom.WithComments {\n\t\t\toutput += \"\u003c!-- footer --\u003e\"\n\t\t}\n\t\toutput += footer + \"\\n\"\n\t\tif dom.WithComments {\n\t\t\toutput += \"\u003c!-- /footer --\u003e\"\n\t\t}\n\t}\n\n\tif classes != \"\" {\n\t\toutput += \"\u003c/main\u003e\"\n\t}\n\n\t// TODO: cleanup double new-lines.\n\n\treturn output\n}\n\ntype Jumbotron []DomStringer\n\nfunc (j Jumbotron) String(dom DOM) string {\n\toutput := `\u003cdiv class=\"jumbotron\"\u003e` + \"\\n\\n\"\n\tfor _, elem := range j {\n\t\toutput += elem.String(dom) + \"\\n\"\n\t}\n\toutput += `\u003c/div\u003e\u003c!-- /jumbotron --\u003e` + \"\\n\"\n\treturn output\n}\n\n// XXX: rename Element to Div?\ntype Element []DomStringer\n\nfunc (e *Element) Append(elems ...DomStringer) {\n\t*e = append(*e, elems...)\n}\n\nfunc (e *Element) String(dom DOM) string {\n\toutput := \"\"\n\tfor _, elem := range *e {\n\t\toutput += elem.String(dom) + \"\\n\"\n\t}\n\treturn output\n}\n\ntype Breadcrumb []DomStringer\n\nfunc (b *Breadcrumb) Append(elems ...DomStringer) {\n\t*b = append(*b, elems...)\n}\n\nfunc (b Breadcrumb) String(dom DOM) string {\n\toutput := \"\"\n\tfor idx, entry := range b {\n\t\tif idx \u003e 0 {\n\t\t\toutput += \" / \"\n\t\t}\n\t\toutput += entry.String(dom)\n\t}\n\treturn output\n}\n\ntype Columns struct {\n\tMaxWidth int\n\tColumns  []Element\n}\n\nfunc (c *Columns) Append(elems ...Element) {\n\tc.Columns = append(c.Columns, elems...)\n}\n\nfunc (c Columns) String(dom DOM) string {\n\toutput := `\u003cdiv class=\"columns-` + strconv.Itoa(c.MaxWidth) + `\"\u003e` + \"\\n\"\n\tfor _, entry := range c.Columns {\n\t\toutput += `\u003cdiv class=\"column\"\u003e` + \"\\n\\n\"\n\t\toutput += entry.String(dom)\n\t\toutput += \"\u003c/div\u003e\u003c!-- /column--\u003e\\n\"\n\t}\n\toutput += \"\u003c/div\u003e\u003c!-- /columns-\" + strconv.Itoa(c.MaxWidth) + \" --\u003e\\n\"\n\treturn output\n}\n\ntype Link struct {\n\tText string\n\tPath string\n\tURL  string\n}\n\n// TODO: image\n\n// TODO: pager\n\nfunc (l Link) String(dom DOM) string {\n\turl := \"\"\n\tswitch {\n\tcase l.Path != \"\" \u0026\u0026 l.URL != \"\":\n\t\tpanic(\"a link should have a path or a URL, not both.\")\n\tcase l.Path != \"\":\n\t\tif l.Text == \"\" {\n\t\t\tl.Text = l.Path\n\t\t}\n\t\turl = dom.Prefix + l.Path\n\tcase l.URL != \"\":\n\t\tif l.Text == \"\" {\n\t\t\tl.Text = l.URL\n\t\t}\n\t\turl = l.URL\n\t}\n\n\treturn \"[\" + l.Text + \"](\" + url + \")\"\n}\n\ntype BulletList []DomStringer\n\nfunc (bl BulletList) String(dom DOM) string {\n\toutput := \"\"\n\n\tfor _, entry := range bl {\n\t\toutput += \"- \" + entry.String(dom) + \"\\n\"\n\t}\n\n\treturn output\n}\n\nfunc Text(s string) DomStringer {\n\treturn Raw{Content: s}\n}\n\ntype DomStringer interface {\n\tString(dom DOM) string\n}\n\ntype Raw struct {\n\tContent string\n}\n\nfunc (r Raw) String(_ DOM) string {\n\treturn r.Content\n}\n\ntype (\n\tH1        string\n\tH2        string\n\tH3        string\n\tH4        string\n\tH5        string\n\tH6        string\n\tBold      string\n\tItalic    string\n\tCode      string\n\tParagraph string\n\tQuote     string\n\tHR        struct{}\n)\n\nfunc (text H1) String(_ DOM) string        { return \"# \" + string(text) + \"\\n\" }\nfunc (text H2) String(_ DOM) string        { return \"## \" + string(text) + \"\\n\" }\nfunc (text H3) String(_ DOM) string        { return \"### \" + string(text) + \"\\n\" }\nfunc (text H4) String(_ DOM) string        { return \"#### \" + string(text) + \"\\n\" }\nfunc (text H5) String(_ DOM) string        { return \"##### \" + string(text) + \"\\n\" }\nfunc (text H6) String(_ DOM) string        { return \"###### \" + string(text) + \"\\n\" }\nfunc (text Quote) String(_ DOM) string     { return \"\u003e \" + string(text) + \"\\n\" }\nfunc (text Bold) String(_ DOM) string      { return \"**\" + string(text) + \"**\" }\nfunc (text Italic) String(_ DOM) string    { return \"_\" + string(text) + \"_\" }\nfunc (text Paragraph) String(_ DOM) string { return \"\\n\" + string(text) + \"\\n\" }\nfunc (_ HR) String(_ DOM) string           { return \"\\n---\\n\" }\n\nfunc (text Code) String(_ DOM) string {\n\t// multiline\n\tif strings.Contains(string(text), \"\\n\") {\n\t\treturn \"\\n```\\n\" + string(text) + \"\\n```\\n\"\n\t}\n\n\t// single line\n\treturn \"`\" + string(text) + \"`\"\n}\n"},{"name":"ui_test.gno","body":"package ui\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"C8ycccOlc6bvSUj1L+TPqgYpj1ho7Me2dk6zt+HV3TlMNxFHutq3T52Gx6SkZ8HidfgASQEt5AoHhYRfmHYjAA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"bf","path":"gno.land/p/demo/bf","files":[{"name":"bf.gno","body":"package bf\n\nimport (\n\t\"strings\"\n)\n\nconst maxlen = 30000\n\nfunc Execute(code string) string {\n\tvar (\n\t\tmemory  = make([]byte, maxlen) // memory tape\n\t\tpointer = 0                    // initial memory pointer\n\t\tbuf     strings.Builder\n\t)\n\n\t// Loop through each character in the code\n\tfor i := 0; i \u003c len(code); i++ {\n\t\tswitch code[i] {\n\t\tcase '\u003e':\n\t\t\t// Increment memory pointer\n\t\t\tpointer++\n\t\t\tif pointer \u003e= maxlen {\n\t\t\t\tpointer = 0\n\t\t\t}\n\t\tcase '\u003c':\n\t\t\t// Decrement memory pointer\n\t\t\tpointer--\n\t\t\tif pointer \u003c 0 {\n\t\t\t\tpointer = maxlen - 1\n\t\t\t}\n\t\tcase '+':\n\t\t\t// Increment the byte at the memory pointer\n\t\t\tmemory[pointer]++\n\t\tcase '-':\n\t\t\t// Decrement the byte at the memory pointer\n\t\t\tmemory[pointer]--\n\t\tcase '.':\n\t\t\t// Output the byte at the memory pointer\n\t\t\tbuf.WriteByte(memory[pointer])\n\t\tcase ',':\n\t\t\t// Input a byte and store it in the memory\n\t\t\tpanic(\"unsupported\")\n\t\t\t// fmt.Scan(\u0026memory[pointer])\n\t\tcase '[':\n\t\t\t// Jump forward past the matching ']' if the byte at the memory pointer is zero\n\t\t\tif memory[pointer] == 0 {\n\t\t\t\tbraceCount := 1\n\t\t\t\tfor braceCount \u003e 0 {\n\t\t\t\t\ti++\n\t\t\t\t\tif code[i] == '[' {\n\t\t\t\t\t\tbraceCount++\n\t\t\t\t\t} else if code[i] == ']' {\n\t\t\t\t\t\tbraceCount--\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tcase ']':\n\t\t\t// Jump backward to the matching '[' if the byte at the memory pointer is nonzero\n\t\t\tif memory[pointer] != 0 {\n\t\t\t\tbraceCount := 1\n\t\t\t\tfor braceCount \u003e 0 {\n\t\t\t\t\ti--\n\t\t\t\t\tif code[i] == ']' {\n\t\t\t\t\t\tbraceCount++\n\t\t\t\t\t} else if code[i] == '[' {\n\t\t\t\t\t\tbraceCount--\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\ti-- // Move back one more to compensate for the upcoming increment in the loop\n\t\t\t}\n\t\t}\n\t}\n\treturn buf.String()\n}\n"},{"name":"bf_test.gno","body":"package bf\n\nimport \"testing\"\n\nfunc TestExecuteBrainfuck(t *testing.T) {\n\ttestCases := []struct {\n\t\tname     string\n\t\tcode     string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"hello\",\n\t\t\tcode:     \"++++++++++[\u003e+++++++\u003e++++++++++\u003e+++\u003e+\u003c\u003c\u003c\u003c-]\u003e++.\u003e+.+++++++..+++.\u003e++.\u003c\u003c+++++++++++++++.\u003e.+++.------.--------.\",\n\t\t\texpected: \"Hello World\",\n\t\t},\n\t\t{\n\t\t\tname:     \"increment\",\n\t\t\tcode:     \"+++++ +++++ [ \u003e +++++ ++ \u003c - ] \u003e +++++ .\",\n\t\t\texpected: \"K\",\n\t\t},\n\t\t// Add more test cases as needed\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult := Execute(tc.code)\n\t\t\tif result != tc.expected {\n\t\t\t\tt.Errorf(\"Expected output: %s, but got: %s\", tc.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n"},{"name":"doc.gno","body":"// Package bf implements a minimalist Brainfuck virtual machine in Gno.\n//\n// Brainfuck is an esoteric programming language known for its simplicity and minimalistic design.\n// It operates on an array of memory cells, with a memory pointer that can move left or right.\n// The language consists of eight commands: \u003e \u003c + - . , [ ].\n//\n// Usage:\n// To execute Brainfuck code, use the Execute function and provide the code as a string.\n//\n//\tcode := \"++++++++++[\u003e+++++++\u003e++++++++++\u003e+++\u003e+\u003c\u003c\u003c\u003c-]\u003e++.\u003e+.+++++++..+++.\u003e++.\u003c\u003c+++++++++++++++.\u003e.+++.------.--------.\"\n//\toutput := bf.Execute(code)\n//\n// Note:\n// This implementation is a minimalist version and may not handle all edge cases or advanced features of the Brainfuck language.\n//\n// Reference:\n// For more information on Brainfuck, refer to the Wikipedia page: https://en.wikipedia.org/wiki/Brainfuck\npackage bf // import \"gno.land/p/demo/bf\"\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/bf\"\ngno = \"0.9\"\n"},{"name":"run.gno","body":"package bf\n\n// for `gno run`\nfunc main() {\n\tcode := \"++++++++++[\u003e+++++++\u003e++++++++++\u003e+++\u003e+\u003c\u003c\u003c\u003c-]\u003e++.\u003e+.+++++++..+++.\u003e++.\u003c\u003c+++++++++++++++.\u003e.+++.------.--------.\"\n\t// TODO: code = os.Args...\n\tExecute(code)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"KI/CKe5iKaCSAQ/ZOvweSyW9wr521tiP8+Q442rrMaoZLi7fzp2jnlKuHuFQWMABzr7mhQNijpvSnLmkrhCwBg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"btree","path":"gno.land/p/demo/btree","files":[{"name":"btree.gno","body":"//////////\n//\n// Copyright 2014 Google Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n//\n// Copyright 2024 New Tendermint\n//\n// This Gno port of the original Go BTree is substantially rewritten/reimplemented\n// from the original, primarily for clarity of code, clarity of documentation,\n// and for compatibility with Gno.\n//\n// Authors:\n//   Original version authors -- https://github.com/google/btree/graphs/contributors\n//   Kirk Haines \u003cwyhaines@gmail.com\u003e\n//\n//////////\n\n// Package btree implements in-memory B-Trees of arbitrary degree.\n//\n// It has a flatter structure than an equivalent red-black or other binary tree,\n// which may yield better memory usage and/or performance.\npackage btree\n\nimport \"sort\"\n\n//////////\n//\n// Types\n//\n//////////\n\n// BTreeOption is a function interface for setting options on a btree with `New()`.\ntype BTreeOption func(*BTree)\n\n// BTree is an implementation of a B-Tree.\n//\n// BTree stores Record instances in an ordered structure, allowing easy insertion,\n// removal, and iteration.\ntype BTree struct {\n\tdegree int\n\tlength int\n\troot   *node\n\tcowCtx *copyOnWriteContext\n}\n\n//\tAny type that implements this interface can be stored in the BTree. This allows considerable\n//\n// flexiblity in storage within the BTree.\ntype Record interface {\n\t// Less compares self to `than`, returning true if self is less than `than`\n\tLess(than Record) bool\n}\n\n// records is the storage within a node. It is expressed as a slice of Record, where a Record\n// is any struct that implements the Record interface.\ntype records []Record\n\n// node is an internal node in a tree.\n//\n// It must at all times maintain on of the two conditions:\n//   - len(children) == 0, len(records) unconstrained\n//   - len(children) == len(records) + 1\ntype node struct {\n\trecords  records\n\tchildren children\n\tcowCtx   *copyOnWriteContext\n}\n\n// children is the list of child nodes below the current node. It is a slice of nodes.\ntype children []*node\n\n// FreeNodeList represents a slice of nodes which are available for reuse. The default\n// behavior of New() is for each BTree instance to have its own FreeNodeList. However,\n// it is possible for multiple instances of BTree to share the same tree. If one uses\n// New(WithFreeNodeList()) to create a tree, one may pass an existing FreeNodeList, allowing\n// multiple trees to use a single list. In an application with multiple trees, it might\n// be more efficient to allocate a single FreeNodeList with a significant initial capacity,\n// and then have all of the trees use that same large FreeNodeList.\ntype FreeNodeList struct {\n\tnodes []*node\n}\n\n// copyOnWriteContext manages node ownership and ensures that cloned trees\n// maintain isolation from each other when a node is changed.\n//\n// Ownership Rules:\n//   - Each node is associated with a specific copyOnWriteContext.\n//   - A tree can modify a node directly only if the tree's context matches the node's context.\n//   - If a tree attempts to modify a node with a different context, it must create a\n//     new, writable copy of that node (i.e., perform a clone) before making changes.\n//\n// Write Operation Invariant:\n//   - During any write operation, the current node being modified must have the same\n//     context as the tree requesting the write.\n//   - To maintain this invariant, before descending into a child node, the system checks\n//     if the child’s context matches the tree's context.\n//   - If the contexts match, the node can be modified in place.\n//   - If the contexts do not match, a mutable copy of the child node is created with the\n//     correct context before proceeding.\n//\n// Practical Implications:\n//   - The node currently being modified inherits the requesting tree's context, allowing\n//     in-place modifications.\n//   - Child nodes may initially have different contexts. Before any modification, these\n//     children are copied to ensure they share the correct context, enabling safe and\n//     isolated updates without affecting other trees that might be referencing the original nodes.\n//\n// Example Usage:\n// When a tree performs a write operation (e.g., inserting or deleting a node), it uses\n// its copyOnWriteContext to determine whether it can modify nodes directly or needs to\n// create copies. This mechanism ensures that trees can share nodes efficiently while\n// maintaining data integrity.\ntype copyOnWriteContext struct {\n\tnodes *FreeNodeList\n}\n\n// Record implements an interface with a single function, Less. Any type that implements\n// RecordIterator allows callers of all of the iteration functions for the BTree\n// to evaluate an element of the tree as it is traversed. The function will receive\n// a stored element from the tree. The function must return either a true or a false value.\n// True indicates that iteration should continue, while false indicates that it should halt.\ntype RecordIterator func(i Record) bool\n\n//////////\n//\n// Functions\n//\n//////////\n\n// NewFreeNodeList creates a new free list.\n// size is the maximum size of the returned free list.\nfunc NewFreeNodeList(size int) *FreeNodeList {\n\treturn \u0026FreeNodeList{nodes: make([]*node, 0, size)}\n}\n\nfunc (freeList *FreeNodeList) newNode() (nodeInstance *node) {\n\tindex := len(freeList.nodes) - 1\n\tif index \u003c 0 {\n\t\treturn new(node)\n\t}\n\tnodeInstance = freeList.nodes[index]\n\tfreeList.nodes[index] = nil\n\tfreeList.nodes = freeList.nodes[:index]\n\n\treturn nodeInstance\n}\n\n// freeNode adds the given node to the list, returning true if it was added\n// and false if it was discarded.\n\nfunc (freeList *FreeNodeList) freeNode(nodeInstance *node) (nodeWasAdded bool) {\n\tif len(freeList.nodes) \u003c cap(freeList.nodes) {\n\t\tfreeList.nodes = append(freeList.nodes, nodeInstance)\n\t\tnodeWasAdded = true\n\t}\n\treturn\n}\n\n// A default size for the free node list. We might want to run some benchmarks to see if\n// there are any pros or cons to this size versus other sizes. This seems to be a reasonable\n// compromise to reduce GC pressure by reusing nodes where possible, without stacking up too\n// much baggage in a given tree.\nconst DefaultFreeNodeListSize = 32\n\n// WithDegree sets the degree of the B-Tree.\nfunc WithDegree(degree int) BTreeOption {\n\treturn func(bt *BTree) {\n\t\tif degree \u003c= 1 {\n\t\t\tpanic(\"Degrees less than 1 do not make any sense for a BTree. Please provide a degree of 1 or greater.\")\n\t\t}\n\t\tbt.degree = degree\n\t}\n}\n\n// WithFreeNodeList sets a custom free node list for the B-Tree.\nfunc WithFreeNodeList(freeList *FreeNodeList) BTreeOption {\n\treturn func(bt *BTree) {\n\t\tbt.cowCtx = \u0026copyOnWriteContext{nodes: freeList}\n\t}\n}\n\n// New creates a new B-Tree with optional configurations. If configuration is not provided,\n// it will default to 16 element nodes. Degree may not be less than 1 (which effectively\n// makes the tree into a binary tree).\n//\n// `New(WithDegree(2))`, for example, will create a 2-3-4 tree (each node contains 1-3 records\n// and 2-4 children).\n//\n// `New(WithFreeNodeList(NewFreeNodeList(64)))` will create a tree with a degree of 16, and\n// with a free node list with a size of 64.\nfunc New(options ...BTreeOption) *BTree {\n\tbtree := \u0026BTree{\n\t\tdegree: 16, // default degree\n\t\tcowCtx: \u0026copyOnWriteContext{nodes: NewFreeNodeList(DefaultFreeNodeListSize)},\n\t}\n\tfor _, opt := range options {\n\t\topt(btree)\n\t}\n\treturn btree\n}\n\n// insertAt inserts a value into the given index, pushing all subsequent values\n// forward.\nfunc (recordsSlice *records) insertAt(index int, newRecord Record) {\n\toriginalLength := len(*recordsSlice)\n\n\t// Extend the slice by one element\n\t*recordsSlice = append(*recordsSlice, nil)\n\n\t// Move elements from the end to avoid overwriting during the copy\n\t// TODO: Make this work with slice appends, instead. It should be faster?\n\tif index \u003c originalLength {\n\t\tfor position := originalLength; position \u003e index; position-- {\n\t\t\t(*recordsSlice)[position] = (*recordsSlice)[position-1]\n\t\t}\n\t}\n\n\t// Insert the new record\n\t(*recordsSlice)[index] = newRecord\n}\n\n// removeAt removes a Record from the records slice at the specified index.\n// It shifts subsequent records to fill the gap and returns the removed Record.\nfunc (recordSlicePointer *records) removeAt(index int) Record {\n\trecordSlice := *recordSlicePointer\n\tremovedRecord := recordSlice[index]\n\tcopy(recordSlice[index:], recordSlice[index+1:])\n\trecordSlice[len(recordSlice)-1] = nil\n\t*recordSlicePointer = recordSlice[:len(recordSlice)-1]\n\n\treturn removedRecord\n}\n\n// Pop removes and returns the last Record from the records slice.\n// It also clears the reference to the removed Record to aid garbage collection.\nfunc (r *records) pop() Record {\n\trecordSlice := *r\n\tlastIndex := len(recordSlice) - 1\n\tremovedRecord := recordSlice[lastIndex]\n\trecordSlice[lastIndex] = nil\n\t*r = recordSlice[:lastIndex]\n\treturn removedRecord\n}\n\n// This slice is intended only as a supply of records for the truncate function\n// that follows, and it should not be changed or altered.\nvar emptyRecords = make(records, 32)\n\n// truncate reduces the length of the slice to the specified index,\n// and clears the elements beyond that index to prevent memory leaks.\n// The index must be less than or equal to the current length of the slice.\nfunc (originalSlice *records) truncate(index int) {\n\t// Split the slice into the part to keep and the part to clear.\n\trecordsToKeep := (*originalSlice)[:index]\n\trecordsToClear := (*originalSlice)[index:]\n\n\t// Update the original slice to only contain the records to keep.\n\t*originalSlice = recordsToKeep\n\n\t// Clear the memory of the part that was truncated.\n\tfor len(recordsToClear) \u003e 0 {\n\t\t// Copy empty values from `emptyRecords` to the recordsToClear slice.\n\t\t// This effectively \"clears\" the memory by overwriting elements.\n\t\tnumCleared := copy(recordsToClear, emptyRecords)\n\t\trecordsToClear = recordsToClear[numCleared:]\n\t}\n}\n\n// Find determines the appropriate index at which a given Record should be inserted\n// into the sorted records slice. If the Record already exists in the slice,\n// the method returns its index and sets found to true.\n//\n// Parameters:\n// - record: The Record to search for within the records slice.\n//\n// Returns:\n// - insertIndex: The index at which the Record should be inserted.\n// - found: A boolean indicating whether the Record already exists in the slice.\nfunc (recordsSlice records) find(record Record) (insertIndex int, found bool) {\n\ttotalRecords := len(recordsSlice)\n\n\t// Perform a binary search to find the insertion point for the record\n\tinsertionPoint := sort.Search(totalRecords, func(currentIndex int) bool {\n\t\treturn record.Less(recordsSlice[currentIndex])\n\t})\n\n\tif insertionPoint \u003e 0 {\n\t\tpreviousRecord := recordsSlice[insertionPoint-1]\n\n\t\tif !previousRecord.Less(record) {\n\t\t\treturn insertionPoint - 1, true\n\t\t}\n\t}\n\n\treturn insertionPoint, false\n}\n\n// insertAt inserts a value into the given index, pushing all subsequent values\n// forward.\nfunc (childSlice *children) insertAt(index int, n *node) {\n\toriginalLength := len(*childSlice)\n\n\t// Extend the slice by one element\n\t*childSlice = append(*childSlice, nil)\n\n\t// Move elements from the end to avoid overwriting during the copy\n\tif index \u003c originalLength {\n\t\tfor i := originalLength; i \u003e index; i-- {\n\t\t\t(*childSlice)[i] = (*childSlice)[i-1]\n\t\t}\n\t}\n\n\t// Insert the new record\n\t(*childSlice)[index] = n\n}\n\n// removeAt removes a Record from the records slice at the specified index.\n// It shifts subsequent records to fill the gap and returns the removed Record.\nfunc (childSlicePointer *children) removeAt(index int) *node {\n\tchildSlice := *childSlicePointer\n\tremovedChild := childSlice[index]\n\tcopy(childSlice[index:], childSlice[index+1:])\n\tchildSlice[len(childSlice)-1] = nil\n\t*childSlicePointer = childSlice[:len(childSlice)-1]\n\n\treturn removedChild\n}\n\n// Pop removes and returns the last Record from the records slice.\n// It also clears the reference to the removed Record to aid garbage collection.\nfunc (childSlicePointer *children) pop() *node {\n\tchildSlice := *childSlicePointer\n\tlastIndex := len(childSlice) - 1\n\tremovedChild := childSlice[lastIndex]\n\tchildSlice[lastIndex] = nil\n\t*childSlicePointer = childSlice[:lastIndex]\n\treturn removedChild\n}\n\n// This slice is intended only as a supply of records for the truncate function\n// that follows, and it should not be changed or altered.\nvar emptyChildren = make(children, 32)\n\n// truncate reduces the length of the slice to the specified index,\n// and clears the elements beyond that index to prevent memory leaks.\n// The index must be less than or equal to the current length of the slice.\nfunc (originalSlice *children) truncate(index int) {\n\t// Split the slice into the part to keep and the part to clear.\n\tchildrenToKeep := (*originalSlice)[:index]\n\tchildrenToClear := (*originalSlice)[index:]\n\n\t// Update the original slice to only contain the records to keep.\n\t*originalSlice = childrenToKeep\n\n\t// Clear the memory of the part that was truncated.\n\tfor len(childrenToClear) \u003e 0 {\n\t\t// Copy empty values from `emptyChildren` to the recordsToClear slice.\n\t\t// This effectively \"clears\" the memory by overwriting elements.\n\t\tnumCleared := copy(childrenToClear, emptyChildren)\n\n\t\t// Slice recordsToClear to exclude the elements that were just cleared.\n\t\tchildrenToClear = childrenToClear[numCleared:]\n\t}\n}\n\n// mutableFor creates a mutable copy of the node if the current node does not\n// already belong to the provided copy-on-write context (COW). If the node is\n// already associated with the given COW context, it returns the current node.\n//\n// Parameters:\n// - cowCtx: The copy-on-write context that should own the returned node.\n//\n// Returns:\n// - A pointer to the mutable node associated with the given COW context.\n//\n// If the current node belongs to a different COW context, this function:\n// - Allocates a new node using the provided context.\n// - Copies the node’s records and children slices into the newly allocated node.\n// - Returns the new node which is now owned by the given COW context.\nfunc (n *node) mutableFor(cowCtx *copyOnWriteContext) *node {\n\t// If the current node is already owned by the provided context, return it as-is.\n\tif n.cowCtx == cowCtx {\n\t\treturn n\n\t}\n\n\t// Create a new node in the provided context.\n\tnewNode := cowCtx.newNode()\n\n\t// Copy the records from the current node into the new node.\n\tnewNode.records = append(newNode.records[:0], n.records...)\n\n\t// Copy the children from the current node into the new node.\n\tnewNode.children = append(newNode.children[:0], n.children...)\n\n\treturn newNode\n}\n\n// mutableChild ensures that the child node at the given index is mutable and\n// associated with the same COW context as the parent node. If the child node\n// belongs to a different context, a copy of the child is created and stored in the\n// parent node.\n//\n// Parameters:\n// - i: The index of the child node to be made mutable.\n//\n// Returns:\n// - A pointer to the mutable child node.\nfunc (n *node) mutableChild(i int) *node {\n\t// Ensure that the child at index `i` is mutable and belongs to the same context as the parent.\n\tmutableChildNode := n.children[i].mutableFor(n.cowCtx)\n\t// Update the child node reference in the current node to the mutable version.\n\tn.children[i] = mutableChildNode\n\treturn mutableChildNode\n}\n\n// split splits the given node at the given index.  The current node shrinks,\n// and this function returns the record that existed at that index and a new node\n// containing all records/children after it.\nfunc (n *node) split(i int) (Record, *node) {\n\trecord := n.records[i]\n\tnext := n.cowCtx.newNode()\n\tnext.records = append(next.records, n.records[i+1:]...)\n\tn.records.truncate(i)\n\tif len(n.children) \u003e 0 {\n\t\tnext.children = append(next.children, n.children[i+1:]...)\n\t\tn.children.truncate(i + 1)\n\t}\n\treturn record, next\n}\n\n// maybeSplitChild checks if a child should be split, and if so splits it.\n// Returns whether or not a split occurred.\nfunc (n *node) maybeSplitChild(i, maxRecords int) bool {\n\tif len(n.children[i].records) \u003c maxRecords {\n\t\treturn false\n\t}\n\tfirst := n.mutableChild(i)\n\trecord, second := first.split(maxRecords / 2)\n\tn.records.insertAt(i, record)\n\tn.children.insertAt(i+1, second)\n\treturn true\n}\n\n// insert adds a record to the subtree rooted at the current node, ensuring that no node in the subtree\n// exceeds the maximum number of allowed records (`maxRecords`). If an equivalent record is already present,\n// it replaces the existing one and returns it; otherwise, it returns nil.\n//\n// Parameters:\n// - record: The record to be inserted.\n// - maxRecords: The maximum number of records allowed per node.\n//\n// Returns:\n// - The record that was replaced if an equivalent record already existed, otherwise nil.\nfunc (n *node) insert(record Record, maxRecords int) Record {\n\t// Find the position where the new record should be inserted and check if an equivalent record already exists.\n\tinsertionIndex, recordExists := n.records.find(record)\n\n\tif recordExists {\n\t\t// If an equivalent record is found, replace it and return the old record.\n\t\texistingRecord := n.records[insertionIndex]\n\t\tn.records[insertionIndex] = record\n\t\treturn existingRecord\n\t}\n\n\t// If the current node is a leaf (has no children), insert the new record at the calculated index.\n\tif len(n.children) == 0 {\n\t\tn.records.insertAt(insertionIndex, record)\n\t\treturn nil\n\t}\n\n\t// Check if the child node at the insertion index needs to be split due to exceeding maxRecords.\n\tif n.maybeSplitChild(insertionIndex, maxRecords) {\n\t\t// If a split occurred, compare the new record with the record moved up to the current node.\n\t\tsplitRecord := n.records[insertionIndex]\n\t\tswitch {\n\t\tcase record.Less(splitRecord):\n\t\t\t// The new record belongs to the first (left) split node; no change to insertion index.\n\t\tcase splitRecord.Less(record):\n\t\t\t// The new record belongs to the second (right) split node; move the insertion index to the next position.\n\t\t\tinsertionIndex++\n\t\tdefault:\n\t\t\t// If the record is equivalent to the split record, replace it and return the old record.\n\t\t\texistingRecord := n.records[insertionIndex]\n\t\t\tn.records[insertionIndex] = record\n\t\t\treturn existingRecord\n\t\t}\n\t}\n\n\t// Recursively insert the record into the appropriate child node, now guaranteed to have space.\n\treturn n.mutableChild(insertionIndex).insert(record, maxRecords)\n}\n\n// get finds the given key in the subtree and returns it.\nfunc (n *node) get(key Record) Record {\n\ti, found := n.records.find(key)\n\tif found {\n\t\treturn n.records[i]\n\t} else if len(n.children) \u003e 0 {\n\t\treturn n.children[i].get(key)\n\t}\n\treturn nil\n}\n\n// min returns the first record in the subtree.\nfunc min(n *node) Record {\n\tif n == nil {\n\t\treturn nil\n\t}\n\tfor len(n.children) \u003e 0 {\n\t\tn = n.children[0]\n\t}\n\tif len(n.records) == 0 {\n\t\treturn nil\n\t}\n\treturn n.records[0]\n}\n\n// max returns the last record in the subtree.\nfunc max(n *node) Record {\n\tif n == nil {\n\t\treturn nil\n\t}\n\tfor len(n.children) \u003e 0 {\n\t\tn = n.children[len(n.children)-1]\n\t}\n\tif len(n.records) == 0 {\n\t\treturn nil\n\t}\n\treturn n.records[len(n.records)-1]\n}\n\n// toRemove details what record to remove in a node.remove call.\ntype toRemove int\n\nconst (\n\tremoveRecord toRemove = iota // removes the given record\n\tremoveMin                    // removes smallest record in the subtree\n\tremoveMax                    // removes largest record in the subtree\n)\n\n// remove removes a record from the subtree rooted at the current node.\n//\n// Parameters:\n// - record: The record to be removed (can be nil when the removal type indicates min or max).\n// - minRecords: The minimum number of records a node should have after removal.\n// - typ: The type of removal operation to perform (removeMin, removeMax, or removeRecord).\n//\n// Returns:\n// - The record that was removed, or nil if no such record was found.\nfunc (n *node) remove(record Record, minRecords int, removalType toRemove) Record {\n\tvar targetIndex int\n\tvar recordFound bool\n\n\t// Determine the index of the record to remove based on the removal type.\n\tswitch removalType {\n\tcase removeMax:\n\t\t// If this node is a leaf, remove and return the last record.\n\t\tif len(n.children) == 0 {\n\t\t\treturn n.records.pop()\n\t\t}\n\t\ttargetIndex = len(n.records) // The last record index for removing max.\n\n\tcase removeMin:\n\t\t// If this node is a leaf, remove and return the first record.\n\t\tif len(n.children) == 0 {\n\t\t\treturn n.records.removeAt(0)\n\t\t}\n\t\ttargetIndex = 0 // The first record index for removing min.\n\n\tcase removeRecord:\n\t\t// Locate the index of the record to be removed.\n\t\ttargetIndex, recordFound = n.records.find(record)\n\t\tif len(n.children) == 0 {\n\t\t\tif recordFound {\n\t\t\t\treturn n.records.removeAt(targetIndex)\n\t\t\t}\n\t\t\treturn nil // The record was not found in the leaf node.\n\t\t}\n\n\tdefault:\n\t\tpanic(\"invalid removal type\")\n\t}\n\n\t// If the current node has children, handle the removal recursively.\n\tif len(n.children[targetIndex].records) \u003c= minRecords {\n\t\t// If the target child node has too few records, grow it before proceeding with removal.\n\t\treturn n.growChildAndRemove(targetIndex, record, minRecords, removalType)\n\t}\n\n\t// Get a mutable reference to the child node at the target index.\n\ttargetChild := n.mutableChild(targetIndex)\n\n\t// If the record to be removed was found in the current node:\n\tif recordFound {\n\t\t// Replace the current record with its predecessor from the child node, and return the removed record.\n\t\treplacedRecord := n.records[targetIndex]\n\t\tn.records[targetIndex] = targetChild.remove(nil, minRecords, removeMax)\n\t\treturn replacedRecord\n\t}\n\n\t// Recursively remove the record from the child node.\n\treturn targetChild.remove(record, minRecords, removalType)\n}\n\n// growChildAndRemove grows child 'i' to make sure it's possible to remove an\n// record from it while keeping it at minRecords, then calls remove to actually\n// remove it.\n//\n// Most documentation says we have to do two sets of special casing:\n//  1. record is in this node\n//  2. record is in child\n//\n// In both cases, we need to handle the two subcases:\n//\n//\tA) node has enough values that it can spare one\n//\tB) node doesn't have enough values\n//\n// For the latter, we have to check:\n//\n//\ta) left sibling has node to spare\n//\tb) right sibling has node to spare\n//\tc) we must merge\n//\n// To simplify our code here, we handle cases #1 and #2 the same:\n// If a node doesn't have enough records, we make sure it does (using a,b,c).\n// We then simply redo our remove call, and the second time (regardless of\n// whether we're in case 1 or 2), we'll have enough records and can guarantee\n// that we hit case A.\nfunc (n *node) growChildAndRemove(i int, record Record, minRecords int, typ toRemove) Record {\n\tif i \u003e 0 \u0026\u0026 len(n.children[i-1].records) \u003e minRecords {\n\t\t// Steal from left child\n\t\tchild := n.mutableChild(i)\n\t\tstealFrom := n.mutableChild(i - 1)\n\t\tstolenRecord := stealFrom.records.pop()\n\t\tchild.records.insertAt(0, n.records[i-1])\n\t\tn.records[i-1] = stolenRecord\n\t\tif len(stealFrom.children) \u003e 0 {\n\t\t\tchild.children.insertAt(0, stealFrom.children.pop())\n\t\t}\n\t} else if i \u003c len(n.records) \u0026\u0026 len(n.children[i+1].records) \u003e minRecords {\n\t\t// steal from right child\n\t\tchild := n.mutableChild(i)\n\t\tstealFrom := n.mutableChild(i + 1)\n\t\tstolenRecord := stealFrom.records.removeAt(0)\n\t\tchild.records = append(child.records, n.records[i])\n\t\tn.records[i] = stolenRecord\n\t\tif len(stealFrom.children) \u003e 0 {\n\t\t\tchild.children = append(child.children, stealFrom.children.removeAt(0))\n\t\t}\n\t} else {\n\t\tif i \u003e= len(n.records) {\n\t\t\ti--\n\t\t}\n\t\tchild := n.mutableChild(i)\n\t\t// merge with right child\n\t\tmergeRecord := n.records.removeAt(i)\n\t\tmergeChild := n.children.removeAt(i + 1).mutableFor(n.cowCtx)\n\t\tchild.records = append(child.records, mergeRecord)\n\t\tchild.records = append(child.records, mergeChild.records...)\n\t\tchild.children = append(child.children, mergeChild.children...)\n\t\tn.cowCtx.freeNode(mergeChild)\n\t}\n\treturn n.remove(record, minRecords, typ)\n}\n\ntype direction int\n\nconst (\n\tdescend = direction(-1)\n\tascend  = direction(+1)\n)\n\n// iterate provides a simple method for iterating over elements in the tree.\n//\n// When ascending, the 'start' should be less than 'stop' and when descending,\n// the 'start' should be greater than 'stop'. Setting 'includeStart' to true\n// will force the iterator to include the first record when it equals 'start',\n// thus creating a \"greaterOrEqual\" or \"lessThanEqual\" rather than just a\n// \"greaterThan\" or \"lessThan\" queries.\nfunc (n *node) iterate(dir direction, start, stop Record, includeStart bool, hit bool, iter RecordIterator) (bool, bool) {\n\tvar ok, found bool\n\tvar index int\n\tswitch dir {\n\tcase ascend:\n\t\tif start != nil {\n\t\t\tindex, _ = n.records.find(start)\n\t\t}\n\t\tfor i := index; i \u003c len(n.records); i++ {\n\t\t\tif len(n.children) \u003e 0 {\n\t\t\t\tif hit, ok = n.children[i].iterate(dir, start, stop, includeStart, hit, iter); !ok {\n\t\t\t\t\treturn hit, false\n\t\t\t\t}\n\t\t\t}\n\t\t\tif !includeStart \u0026\u0026 !hit \u0026\u0026 start != nil \u0026\u0026 !start.Less(n.records[i]) {\n\t\t\t\thit = true\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\thit = true\n\t\t\tif stop != nil \u0026\u0026 !n.records[i].Less(stop) {\n\t\t\t\treturn hit, false\n\t\t\t}\n\t\t\tif !iter(n.records[i]) {\n\t\t\t\treturn hit, false\n\t\t\t}\n\t\t}\n\t\tif len(n.children) \u003e 0 {\n\t\t\tif hit, ok = n.children[len(n.children)-1].iterate(dir, start, stop, includeStart, hit, iter); !ok {\n\t\t\t\treturn hit, false\n\t\t\t}\n\t\t}\n\tcase descend:\n\t\tif start != nil {\n\t\t\tindex, found = n.records.find(start)\n\t\t\tif !found {\n\t\t\t\tindex = index - 1\n\t\t\t}\n\t\t} else {\n\t\t\tindex = len(n.records) - 1\n\t\t}\n\t\tfor i := index; i \u003e= 0; i-- {\n\t\t\tif start != nil \u0026\u0026 !n.records[i].Less(start) {\n\t\t\t\tif !includeStart || hit || start.Less(n.records[i]) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(n.children) \u003e 0 {\n\t\t\t\tif hit, ok = n.children[i+1].iterate(dir, start, stop, includeStart, hit, iter); !ok {\n\t\t\t\t\treturn hit, false\n\t\t\t\t}\n\t\t\t}\n\t\t\tif stop != nil \u0026\u0026 !stop.Less(n.records[i]) {\n\t\t\t\treturn hit, false //\tcontinue\n\t\t\t}\n\t\t\thit = true\n\t\t\tif !iter(n.records[i]) {\n\t\t\t\treturn hit, false\n\t\t\t}\n\t\t}\n\t\tif len(n.children) \u003e 0 {\n\t\t\tif hit, ok = n.children[0].iterate(dir, start, stop, includeStart, hit, iter); !ok {\n\t\t\t\treturn hit, false\n\t\t\t}\n\t\t}\n\t}\n\treturn hit, true\n}\n\nfunc (tree *BTree) Iterate(dir direction, start, stop Record, includeStart bool, hit bool, iter RecordIterator) (bool, bool) {\n\treturn tree.root.iterate(dir, start, stop, includeStart, hit, iter)\n}\n\n// Clone creates a new BTree instance that shares the current tree's structure using a copy-on-write (COW) approach.\n//\n// How Cloning Works:\n//   - The cloned tree (`clonedTree`) shares the current tree’s nodes in a read-only state. This means that no additional memory\n//     is allocated for shared nodes, and read operations on the cloned tree are as fast as on the original tree.\n//   - When either the original tree (`t`) or the cloned tree (`clonedTree`) needs to perform a write operation (such as an insert, delete, etc.),\n//     a new copy of the affected nodes is created on-demand. This ensures that modifications to one tree do not affect the other.\n//\n// Performance Implications:\n//   - **Clone Creation:** The creation of a clone is inexpensive since it only involves copying references to the original tree's nodes\n//     and creating new copy-on-write contexts.\n//   - **Read Operations:** Reading from either the original tree or the cloned tree has no additional performance overhead compared to the original tree.\n//   - **Write Operations:** The first write operation on either tree may experience a slight slow-down due to the allocation of new nodes,\n//     but subsequent write operations will perform at the same speed as if the tree were not cloned.\n//\n// Returns:\n// - A new BTree instance (`clonedTree`) that shares the original tree's structure.\nfunc (t *BTree) Clone() *BTree {\n\t// Create two independent copy-on-write contexts, one for the original tree (`t`) and one for the cloned tree.\n\toriginalContext := *t.cowCtx\n\tclonedContext := *t.cowCtx\n\n\t// Create a shallow copy of the current tree, which will be the new cloned tree.\n\tclonedTree := *t\n\n\t// Assign the new contexts to their respective trees.\n\tt.cowCtx = \u0026originalContext\n\tclonedTree.cowCtx = \u0026clonedContext\n\n\treturn \u0026clonedTree\n}\n\n// maxRecords returns the max number of records to allow per node.\nfunc (t *BTree) maxRecords() int {\n\treturn t.degree*2 - 1\n}\n\n// minRecords returns the min number of records to allow per node (ignored for the\n// root node).\nfunc (t *BTree) minRecords() int {\n\treturn t.degree - 1\n}\n\nfunc (c *copyOnWriteContext) newNode() (n *node) {\n\tn = c.nodes.newNode()\n\tn.cowCtx = c\n\treturn\n}\n\ntype freeType int\n\nconst (\n\tftFreelistFull freeType = iota // node was freed (available for GC, not stored in nodes)\n\tftStored                       // node was stored in the nodes for later use\n\tftNotOwned                     // node was ignored by COW, since it's owned by another one\n)\n\n// freeNode frees a node within a given COW context, if it's owned by that\n// context.  It returns what happened to the node (see freeType const\n// documentation).\nfunc (c *copyOnWriteContext) freeNode(n *node) freeType {\n\tif n.cowCtx == c {\n\t\t// clear to allow GC\n\t\tn.records.truncate(0)\n\t\tn.children.truncate(0)\n\t\tn.cowCtx = nil\n\t\tif c.nodes.freeNode(n) {\n\t\t\treturn ftStored\n\t\t} else {\n\t\t\treturn ftFreelistFull\n\t\t}\n\t} else {\n\t\treturn ftNotOwned\n\t}\n}\n\n// Insert adds the given record to the B-tree. If a record already exists in the tree with the same value,\n// it is replaced, and the old record is returned. Otherwise, it returns nil.\n//\n// Notes:\n// - The function panics if a nil record is provided as input.\n// - If the root node is empty, a new root node is created and the record is inserted.\n//\n// Parameters:\n// - record: The record to be inserted into the B-tree.\n//\n// Returns:\n// - The replaced record if an equivalent record already exists, or nil if no replacement occurred.\nfunc (t *BTree) Insert(record Record) Record {\n\tif record == nil {\n\t\tpanic(\"nil record cannot be added to BTree\")\n\t}\n\n\t// If the tree is empty (no root), create a new root node and insert the record.\n\tif t.root == nil {\n\t\tt.root = t.cowCtx.newNode()\n\t\tt.root.records = append(t.root.records, record)\n\t\tt.length++\n\t\treturn nil\n\t}\n\n\t// Ensure that the root node is mutable (associated with the current tree's copy-on-write context).\n\tt.root = t.root.mutableFor(t.cowCtx)\n\n\t// If the root node is full (contains the maximum number of records), split the root.\n\tif len(t.root.records) \u003e= t.maxRecords() {\n\t\t// Split the root node, promoting the middle record and creating a new child node.\n\t\tmiddleRecord, newChildNode := t.root.split(t.maxRecords() / 2)\n\n\t\t// Create a new root node to hold the promoted middle record.\n\t\toldRoot := t.root\n\t\tt.root = t.cowCtx.newNode()\n\t\tt.root.records = append(t.root.records, middleRecord)\n\t\tt.root.children = append(t.root.children, oldRoot, newChildNode)\n\t}\n\n\t// Insert the new record into the subtree rooted at the current root node.\n\treplacedRecord := t.root.insert(record, t.maxRecords())\n\n\t// If no record was replaced, increase the tree's length.\n\tif replacedRecord == nil {\n\t\tt.length++\n\t}\n\n\treturn replacedRecord\n}\n\n// Delete removes an record equal to the passed in record from the tree, returning\n// it.  If no such record exists, returns nil.\nfunc (t *BTree) Delete(record Record) Record {\n\treturn t.deleteRecord(record, removeRecord)\n}\n\n// DeleteMin removes the smallest record in the tree and returns it.\n// If no such record exists, returns nil.\nfunc (t *BTree) DeleteMin() Record {\n\treturn t.deleteRecord(nil, removeMin)\n}\n\n// Shift is identical to DeleteMin. If the tree is thought of as an ordered list, then Shift()\n// removes the element at the start of the list, the smallest element, and returns it.\nfunc (t *BTree) Shift() Record {\n\treturn t.deleteRecord(nil, removeMin)\n}\n\n// DeleteMax removes the largest record in the tree and returns it.\n// If no such record exists, returns nil.\nfunc (t *BTree) DeleteMax() Record {\n\treturn t.deleteRecord(nil, removeMax)\n}\n\n// Pop is identical to DeleteMax. If the tree is thought of as an ordered list, then Shift()\n// removes the element at the end of the list, the largest element, and returns it.\nfunc (t *BTree) Pop() Record {\n\treturn t.deleteRecord(nil, removeMax)\n}\n\n// deleteRecord removes a record from the B-tree based on the specified removal type (removeMin, removeMax, or removeRecord).\n// It returns the removed record if it was found, or nil if no matching record was found.\n//\n// Parameters:\n// - record: The record to be removed (can be nil if the removal type indicates min or max).\n// - removalType: The type of removal operation to perform (removeMin, removeMax, or removeRecord).\n//\n// Returns:\n// - The removed record if it existed in the tree, or nil if it was not found.\nfunc (t *BTree) deleteRecord(record Record, removalType toRemove) Record {\n\t// If the tree is empty or the root has no records, return nil.\n\tif t.root == nil || len(t.root.records) == 0 {\n\t\treturn nil\n\t}\n\n\t// Ensure the root node is mutable (associated with the tree's copy-on-write context).\n\tt.root = t.root.mutableFor(t.cowCtx)\n\n\t// Attempt to remove the specified record from the root node.\n\tremovedRecord := t.root.remove(record, t.minRecords(), removalType)\n\n\t// Check if the root node has become empty but still has children.\n\t// In this case, the tree height should be reduced, making the first child the new root.\n\tif len(t.root.records) == 0 \u0026\u0026 len(t.root.children) \u003e 0 {\n\t\toldRoot := t.root\n\t\tt.root = t.root.children[0]\n\t\t// Free the old root node, as it is no longer needed.\n\t\tt.cowCtx.freeNode(oldRoot)\n\t}\n\n\t// If a record was successfully removed, decrease the tree's length.\n\tif removedRecord != nil {\n\t\tt.length--\n\t}\n\n\treturn removedRecord\n}\n\n// AscendRange calls the iterator for every value in the tree within the range\n// [greaterOrEqual, lessThan), until iterator returns false.\nfunc (t *BTree) AscendRange(greaterOrEqual, lessThan Record, iterator RecordIterator) {\n\tif t.root == nil {\n\t\treturn\n\t}\n\tt.root.iterate(ascend, greaterOrEqual, lessThan, true, false, iterator)\n}\n\n// AscendLessThan calls the iterator for every value in the tree within the range\n// [first, pivot), until iterator returns false.\nfunc (t *BTree) AscendLessThan(pivot Record, iterator RecordIterator) {\n\tif t.root == nil {\n\t\treturn\n\t}\n\tt.root.iterate(ascend, nil, pivot, false, false, iterator)\n}\n\n// AscendGreaterOrEqual calls the iterator for every value in the tree within\n// the range [pivot, last], until iterator returns false.\nfunc (t *BTree) AscendGreaterOrEqual(pivot Record, iterator RecordIterator) {\n\tif t.root == nil {\n\t\treturn\n\t}\n\tt.root.iterate(ascend, pivot, nil, true, false, iterator)\n}\n\n// Ascend calls the iterator for every value in the tree within the range\n// [first, last], until iterator returns false.\nfunc (t *BTree) Ascend(iterator RecordIterator) {\n\tif t.root == nil {\n\t\treturn\n\t}\n\tt.root.iterate(ascend, nil, nil, false, false, iterator)\n}\n\n// DescendRange calls the iterator for every value in the tree within the range\n// [lessOrEqual, greaterThan), until iterator returns false.\nfunc (t *BTree) DescendRange(lessOrEqual, greaterThan Record, iterator RecordIterator) {\n\tif t.root == nil {\n\t\treturn\n\t}\n\tt.root.iterate(descend, lessOrEqual, greaterThan, true, false, iterator)\n}\n\n// DescendLessOrEqual calls the iterator for every value in the tree within the range\n// [pivot, first], until iterator returns false.\nfunc (t *BTree) DescendLessOrEqual(pivot Record, iterator RecordIterator) {\n\tif t.root == nil {\n\t\treturn\n\t}\n\tt.root.iterate(descend, pivot, nil, true, false, iterator)\n}\n\n// DescendGreaterThan calls the iterator for every value in the tree within\n// the range [last, pivot), until iterator returns false.\nfunc (t *BTree) DescendGreaterThan(pivot Record, iterator RecordIterator) {\n\tif t.root == nil {\n\t\treturn\n\t}\n\tt.root.iterate(descend, nil, pivot, false, false, iterator)\n}\n\n// Descend calls the iterator for every value in the tree within the range\n// [last, first], until iterator returns false.\nfunc (t *BTree) Descend(iterator RecordIterator) {\n\tif t.root == nil {\n\t\treturn\n\t}\n\tt.root.iterate(descend, nil, nil, false, false, iterator)\n}\n\n// Get looks for the key record in the tree, returning it.  It returns nil if\n// unable to find that record.\nfunc (t *BTree) Get(key Record) Record {\n\tif t.root == nil {\n\t\treturn nil\n\t}\n\treturn t.root.get(key)\n}\n\n// Min returns the smallest record in the tree, or nil if the tree is empty.\nfunc (t *BTree) Min() Record {\n\treturn min(t.root)\n}\n\n// Max returns the largest record in the tree, or nil if the tree is empty.\nfunc (t *BTree) Max() Record {\n\treturn max(t.root)\n}\n\n// Has returns true if the given key is in the tree.\nfunc (t *BTree) Has(key Record) bool {\n\treturn t.Get(key) != nil\n}\n\n// Len returns the number of records currently in the tree.\nfunc (t *BTree) Len() int {\n\treturn t.length\n}\n\n// Clear removes all elements from the B-tree.\n//\n// Parameters:\n// - addNodesToFreelist:\n//     - If true, the tree's nodes are added to the freelist during the clearing process,\n//       up to the freelist's capacity.\n//     - If false, the root node is simply dereferenced, allowing Go's garbage collector\n//       to reclaim the memory.\n//\n// Benefits:\n// - **Performance:**\n//     - Significantly faster than deleting each element individually, as it avoids the overhead\n//       of searching and updating the tree structure for each deletion.\n//     - More efficient than creating a new tree, since it reuses existing nodes by adding them\n//       to the freelist instead of discarding them to the garbage collector.\n//\n// Time Complexity:\n// - **O(1):**\n//     - When `addNodesToFreelist` is false.\n//     - When `addNodesToFreelist` is true but the freelist is already full.\n// - **O(freelist size):**\n//     - When adding nodes to the freelist up to its capacity.\n// - **O(tree size):**\n//     - When iterating through all nodes to add to the freelist, but none can be added due to\n//       ownership by another tree.\n\nfunc (tree *BTree) Clear(addNodesToFreelist bool) {\n\tif tree.root != nil \u0026\u0026 addNodesToFreelist {\n\t\ttree.root.reset(tree.cowCtx)\n\t}\n\ttree.root = nil\n\ttree.length = 0\n}\n\n// reset adds all nodes in the current subtree to the freelist.\n//\n// The function operates recursively:\n// - It first attempts to reset all child nodes.\n// - If the freelist becomes full at any point, the process stops immediately.\n//\n// Parameters:\n// - copyOnWriteCtx: The copy-on-write context managing the freelist.\n//\n// Returns:\n// - true: Indicates that the parent node should continue attempting to reset its nodes.\n// - false: Indicates that the freelist is full and no further nodes should be added.\n//\n// Usage:\n// This method is called during the `Clear` operation of the B-tree to efficiently reuse\n// nodes by adding them to the freelist, thereby avoiding unnecessary allocations and reducing\n// garbage collection overhead.\nfunc (currentNode *node) reset(copyOnWriteCtx *copyOnWriteContext) bool {\n\t// Iterate through each child node and attempt to reset it.\n\tfor _, childNode := range currentNode.children {\n\t\t// If any child reset operation signals that the freelist is full, stop the process.\n\t\tif !childNode.reset(copyOnWriteCtx) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\t// Attempt to add the current node to the freelist.\n\t// If the freelist is full after this operation, indicate to the parent to stop.\n\tfreelistStatus := copyOnWriteCtx.freeNode(currentNode)\n\treturn freelistStatus != ftFreelistFull\n}\n"},{"name":"btree_test.gno","body":"package btree\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"testing\"\n)\n\n// Content represents a key-value pair where the Key can be either an int or string\n// and the Value can be any type.\ntype Content struct {\n\tKey   any\n\tValue any\n}\n\n// Less compares two Content records by their Keys.\n// The Key must be either an int or a string.\nfunc (c Content) Less(than Record) bool {\n\tother, ok := than.(Content)\n\tif !ok {\n\t\tpanic(\"cannot compare: incompatible types\")\n\t}\n\n\tswitch key := c.Key.(type) {\n\tcase int:\n\t\tswitch otherKey := other.Key.(type) {\n\t\tcase int:\n\t\t\treturn key \u003c otherKey\n\t\tcase string:\n\t\t\treturn true // ints are always less than strings\n\t\tdefault:\n\t\t\tpanic(\"unsupported key type: must be int or string\")\n\t\t}\n\tcase string:\n\t\tswitch otherKey := other.Key.(type) {\n\t\tcase int:\n\t\t\treturn false // strings are always greater than ints\n\t\tcase string:\n\t\t\treturn key \u003c otherKey\n\t\tdefault:\n\t\t\tpanic(\"unsupported key type: must be int or string\")\n\t\t}\n\tdefault:\n\t\tpanic(\"unsupported key type: must be int or string\")\n\t}\n}\n\ntype ContentSlice []Content\n\nfunc (s ContentSlice) Len() int {\n\treturn len(s)\n}\n\nfunc (s ContentSlice) Less(i, j int) bool {\n\treturn s[i].Less(s[j])\n}\n\nfunc (s ContentSlice) Swap(i, j int) {\n\ts[i], s[j] = s[j], s[i]\n}\n\nfunc (s ContentSlice) Copy() ContentSlice {\n\tnewSlice := make(ContentSlice, len(s))\n\tcopy(newSlice, s)\n\treturn newSlice\n}\n\n// Ensure Content implements the Record interface.\nvar _ Record = Content{}\n\n// ****************************************************************************\n// Test helpers\n// ****************************************************************************\n\nfunc genericSeeding(tree *BTree, size int) *BTree {\n\tfor i := 0; i \u003c size; i++ {\n\t\ttree.Insert(Content{Key: i, Value: fmt.Sprintf(\"Value_%d\", i)})\n\t}\n\treturn tree\n}\n\nfunc intSlicesCompare(left, right []int) int {\n\tif len(left) != len(right) {\n\t\tif len(left) \u003e len(right) {\n\t\t\treturn 1\n\t\t} else {\n\t\t\treturn -1\n\t\t}\n\t}\n\n\tfor position, leftInt := range left {\n\t\tif leftInt != right[position] {\n\t\t\tif leftInt \u003e right[position] {\n\t\t\t\treturn 1\n\t\t\t} else {\n\t\t\t\treturn -1\n\t\t\t}\n\t\t}\n\t}\n\n\treturn 0\n}\n\n// ****************************************************************************\n// Tests\n// ****************************************************************************\n\nfunc TestLen(t *testing.T) {\n\tlength := genericSeeding(New(WithDegree(10)), 7).Len()\n\tif length != 7 {\n\t\tt.Errorf(\"Length is incorrect. Expected 7, but got %d.\", length)\n\t}\n\n\tlength = genericSeeding(New(WithDegree(5)), 111).Len()\n\tif length != 111 {\n\t\tt.Errorf(\"Length is incorrect. Expected 111, but got %d.\", length)\n\t}\n\n\tlength = genericSeeding(New(WithDegree(30)), 123).Len()\n\tif length != 123 {\n\t\tt.Errorf(\"Length is incorrect. Expected 123, but got %d.\", length)\n\t}\n}\n\nfunc TestHas(t *testing.T) {\n\ttree := genericSeeding(New(WithDegree(10)), 40)\n\n\tif tree.Has(Content{Key: 7}) != true {\n\t\tt.Errorf(\"Has(7) reported false, but it should be true.\")\n\t}\n\tif tree.Has(Content{Key: 39}) != true {\n\t\tt.Errorf(\"Has(40) reported false, but it should be true.\")\n\t}\n\tif tree.Has(Content{Key: 1111}) == true {\n\t\tt.Errorf(\"Has(1111) reported true, but it should be false.\")\n\t}\n}\n\nfunc TestMin(t *testing.T) {\n\tmin := genericSeeding(New(WithDegree(10)), 53).Min().(Content)\n\n\tif min.Key != 0 {\n\t\tt.Errorf(\"Minimum should have been 0, but it was reported as %d.\", min)\n\t}\n}\n\nfunc TestMax(t *testing.T) {\n\tmax := genericSeeding(New(WithDegree(10)), 53).Max().(Content)\n\n\tif max.Key != 52 {\n\t\tt.Errorf(\"Maximum should have been 52, but it was reported as %d.\", max)\n\t}\n}\n\nfunc TestGet(t *testing.T) {\n\ttree := genericSeeding(New(WithDegree(10)), 40)\n\n\tif val := tree.Get(Content{Key: 7}); val != nil \u0026\u0026 val.(Content).Value != \"Value_7\" {\n\t\tt.Errorf(\"Get(7) should have returned 'Value_7', but it returned %v.\", val)\n\t}\n\tif val := tree.Get(Content{Key: 39}); val != nil \u0026\u0026 val.(Content).Value != \"Value_39\" {\n\t\tt.Errorf(\"Get(39) should have returned 'Value_39', but it returned %v.\", val)\n\t}\n\tif val := tree.Get(Content{Key: 1111}); val != nil {\n\t\tt.Errorf(\"Get(1111) returned %v, but it should be nil.\", val)\n\t}\n}\n\nfunc TestDescend(t *testing.T) {\n\ttree := genericSeeding(New(WithDegree(10)), 5)\n\n\texpected := []int{4, 3, 2, 1, 0}\n\tfound := []int{}\n\n\ttree.Descend(func(_record Record) bool {\n\t\trecord := _record.(Content)\n\t\tfound = append(found, record.Key.(int))\n\t\treturn true\n\t})\n\n\tif intSlicesCompare(expected, found) != 0 {\n\t\tt.Errorf(\"Descend returned the wrong sequence. Expected %v, but got %v.\", expected, found)\n\t}\n}\n\nfunc TestDescendGreaterThan(t *testing.T) {\n\ttree := genericSeeding(New(WithDegree(10)), 10)\n\n\texpected := []int{9, 8, 7, 6, 5}\n\tfound := []int{}\n\n\ttree.DescendGreaterThan(Content{Key: 4}, func(_record Record) bool {\n\t\trecord := _record.(Content)\n\t\tfound = append(found, record.Key.(int))\n\t\treturn true\n\t})\n\n\tif intSlicesCompare(expected, found) != 0 {\n\t\tt.Errorf(\"DescendGreaterThan returned the wrong sequence. Expected %v, but got %v.\", expected, found)\n\t}\n}\n\nfunc TestDescendLessOrEqual(t *testing.T) {\n\ttree := genericSeeding(New(WithDegree(10)), 10)\n\n\texpected := []int{4, 3, 2, 1, 0}\n\tfound := []int{}\n\n\ttree.DescendLessOrEqual(Content{Key: 4}, func(_record Record) bool {\n\t\trecord := _record.(Content)\n\t\tfound = append(found, record.Key.(int))\n\t\treturn true\n\t})\n\n\tif intSlicesCompare(expected, found) != 0 {\n\t\tt.Errorf(\"DescendLessOrEqual returned the wrong sequence. Expected %v, but got %v.\", expected, found)\n\t}\n}\n\nfunc TestDescendRange(t *testing.T) {\n\ttree := genericSeeding(New(WithDegree(10)), 10)\n\n\texpected := []int{6, 5, 4, 3, 2}\n\tfound := []int{}\n\n\ttree.DescendRange(Content{Key: 6}, Content{Key: 1}, func(_record Record) bool {\n\t\trecord := _record.(Content)\n\t\tfound = append(found, record.Key.(int))\n\t\treturn true\n\t})\n\n\tif intSlicesCompare(expected, found) != 0 {\n\t\tt.Errorf(\"DescendRange returned the wrong sequence. Expected %v, but got %v.\", expected, found)\n\t}\n}\n\nfunc TestAscend(t *testing.T) {\n\ttree := genericSeeding(New(WithDegree(10)), 5)\n\n\texpected := []int{0, 1, 2, 3, 4}\n\tfound := []int{}\n\n\ttree.Ascend(func(_record Record) bool {\n\t\trecord := _record.(Content)\n\t\tfound = append(found, record.Key.(int))\n\t\treturn true\n\t})\n\n\tif intSlicesCompare(expected, found) != 0 {\n\t\tt.Errorf(\"Ascend returned the wrong sequence. Expected %v, but got %v.\", expected, found)\n\t}\n}\n\nfunc TestAscendGreaterOrEqual(t *testing.T) {\n\ttree := genericSeeding(New(WithDegree(10)), 10)\n\n\texpected := []int{5, 6, 7, 8, 9}\n\tfound := []int{}\n\n\ttree.AscendGreaterOrEqual(Content{Key: 5}, func(_record Record) bool {\n\t\trecord := _record.(Content)\n\t\tfound = append(found, record.Key.(int))\n\t\treturn true\n\t})\n\n\tif intSlicesCompare(expected, found) != 0 {\n\t\tt.Errorf(\"AscendGreaterOrEqual returned the wrong sequence. Expected %v, but got %v.\", expected, found)\n\t}\n}\n\nfunc TestAscendLessThan(t *testing.T) {\n\ttree := genericSeeding(New(WithDegree(10)), 10)\n\n\texpected := []int{0, 1, 2, 3, 4}\n\tfound := []int{}\n\n\ttree.AscendLessThan(Content{Key: 5}, func(_record Record) bool {\n\t\trecord := _record.(Content)\n\t\tfound = append(found, record.Key.(int))\n\t\treturn true\n\t})\n\n\tif intSlicesCompare(expected, found) != 0 {\n\t\tt.Errorf(\"AscendLessThan returned the wrong sequence. Expected %v, but got %v.\", expected, found)\n\t}\n}\n\nfunc TestAscendRange(t *testing.T) {\n\ttree := genericSeeding(New(WithDegree(10)), 10)\n\n\texpected := []int{2, 3, 4, 5, 6}\n\tfound := []int{}\n\n\ttree.AscendRange(Content{Key: 2}, Content{Key: 7}, func(_record Record) bool {\n\t\trecord := _record.(Content)\n\t\tfound = append(found, record.Key.(int))\n\t\treturn true\n\t})\n\n\tif intSlicesCompare(expected, found) != 0 {\n\t\tt.Errorf(\"AscendRange returned the wrong sequence. Expected %v, but got %v.\", expected, found)\n\t}\n}\n\nfunc TestDeleteMin(t *testing.T) {\n\ttree := genericSeeding(New(WithDegree(3)), 100)\n\n\texpected := []int{0, 1, 2, 3, 4}\n\tfound := []int{}\n\n\tfound = append(found, tree.DeleteMin().(Content).Key.(int))\n\tfound = append(found, tree.DeleteMin().(Content).Key.(int))\n\tfound = append(found, tree.DeleteMin().(Content).Key.(int))\n\tfound = append(found, tree.DeleteMin().(Content).Key.(int))\n\tfound = append(found, tree.DeleteMin().(Content).Key.(int))\n\n\tif intSlicesCompare(expected, found) != 0 {\n\t\tt.Errorf(\"5 rounds of DeleteMin returned the wrong elements. Expected  %v, but got %v.\", expected, found)\n\t}\n}\n\nfunc TestShift(t *testing.T) {\n\ttree := genericSeeding(New(WithDegree(3)), 100)\n\n\texpected := []int{0, 1, 2, 3, 4}\n\tfound := []int{}\n\n\tfound = append(found, tree.Shift().(Content).Key.(int))\n\tfound = append(found, tree.Shift().(Content).Key.(int))\n\tfound = append(found, tree.Shift().(Content).Key.(int))\n\tfound = append(found, tree.Shift().(Content).Key.(int))\n\tfound = append(found, tree.Shift().(Content).Key.(int))\n\n\tif intSlicesCompare(expected, found) != 0 {\n\t\tt.Errorf(\"5 rounds of Shift returned the wrong elements. Expected  %v, but got %v.\", expected, found)\n\t}\n}\n\nfunc TestDeleteMax(t *testing.T) {\n\ttree := genericSeeding(New(WithDegree(3)), 100)\n\n\texpected := []int{99, 98, 97, 96, 95}\n\tfound := []int{}\n\n\tfound = append(found, tree.DeleteMax().(Content).Key.(int))\n\tfound = append(found, tree.DeleteMax().(Content).Key.(int))\n\tfound = append(found, tree.DeleteMax().(Content).Key.(int))\n\tfound = append(found, tree.DeleteMax().(Content).Key.(int))\n\tfound = append(found, tree.DeleteMax().(Content).Key.(int))\n\n\tif intSlicesCompare(expected, found) != 0 {\n\t\tt.Errorf(\"5 rounds of DeleteMax returned the wrong elements. Expected  %v, but got %v.\", expected, found)\n\t}\n}\n\nfunc TestPop(t *testing.T) {\n\ttree := genericSeeding(New(WithDegree(3)), 100)\n\n\texpected := []int{99, 98, 97, 96, 95}\n\tfound := []int{}\n\n\tfound = append(found, tree.Pop().(Content).Key.(int))\n\tfound = append(found, tree.Pop().(Content).Key.(int))\n\tfound = append(found, tree.Pop().(Content).Key.(int))\n\tfound = append(found, tree.Pop().(Content).Key.(int))\n\tfound = append(found, tree.Pop().(Content).Key.(int))\n\n\tif intSlicesCompare(expected, found) != 0 {\n\t\tt.Errorf(\"5 rounds of Pop returned the wrong elements. Expected  %v, but got %v.\", expected, found)\n\t}\n}\n\nfunc TestInsertGet(t *testing.T) {\n\ttree := New(WithDegree(4))\n\n\texpected := []Content{}\n\n\tfor count := 0; count \u003c 20; count++ {\n\t\tvalue := fmt.Sprintf(\"Value_%d\", count)\n\t\ttree.Insert(Content{Key: count, Value: value})\n\t\texpected = append(expected, Content{Key: count, Value: value})\n\t}\n\n\tfor count := 0; count \u003c 20; count++ {\n\t\tval := tree.Get(Content{Key: count})\n\t\tif val == nil || val.(Content) != expected[count] {\n\t\t\tt.Errorf(\"Insert/Get doesn't appear to be working. Expected to retrieve %v with key %d, but got %v.\", expected[count], count, val)\n\t\t}\n\t}\n}\n\nfunc TestClone(t *testing.T) {\n\t// Implement the clone test\n}\n\n// ***** The following tests are functional or stress testing type tests.\n\nfunc TestBTree(t *testing.T) {\n\t// Create a B-Tree of degree 3\n\ttree := New(WithDegree(3))\n\n\t// insertData := []Content{}\n\tvar insertData ContentSlice\n\n\t// Insert integer keys\n\tintKeys := []int{10, 20, 5, 6, 12, 30, 7, 17}\n\tfor _, key := range intKeys {\n\t\tcontent := Content{Key: key, Value: fmt.Sprintf(\"Value_%d\", key)}\n\t\tinsertData = append(insertData, content)\n\t\tresult := tree.Insert(content)\n\t\tif result != nil {\n\t\t\tt.Errorf(\"**** Already in the tree?  %v\", result)\n\t\t}\n\t}\n\n\t// Insert string keys\n\tstringKeys := []string{\"apple\", \"banana\", \"cherry\", \"date\", \"fig\", \"grape\"}\n\tfor _, key := range stringKeys {\n\t\tcontent := Content{Key: key, Value: fmt.Sprintf(\"Fruit_%s\", key)}\n\t\tinsertData = append(insertData, content)\n\t\ttree.Insert(content)\n\t}\n\n\tif tree.Len() != 14 {\n\t\tt.Errorf(\"Tree length wrong. Expected 14 but got %d\", tree.Len())\n\t}\n\n\t// Search for existing and non-existing keys\n\tsearchTests := []struct {\n\t\ttest     Content\n\t\texpected bool\n\t}{\n\t\t{Content{Key: 10, Value: \"Value_10\"}, true},\n\t\t{Content{Key: 15, Value: \"\"}, false},\n\t\t{Content{Key: \"banana\", Value: \"Fruit_banana\"}, true},\n\t\t{Content{Key: \"kiwi\", Value: \"\"}, false},\n\t}\n\n\tt.Logf(\"Search Tests:\\n\")\n\tfor _, test := range searchTests {\n\t\tval := tree.Get(test.test)\n\n\t\tif test.expected {\n\t\t\tif val != nil \u0026\u0026 val.(Content).Value == test.test.Value {\n\t\t\t\tt.Logf(\"Found expected key:value %v:%v\", test.test.Key, test.test.Value)\n\t\t\t} else {\n\t\t\t\tif val == nil {\n\t\t\t\t\tt.Logf(\"Didn't find %v, but expected\", test.test.Key)\n\t\t\t\t} else {\n\t\t\t\t\tt.Errorf(\"Expected key %v:%v, but found %v:%v.\", test.test.Key, test.test.Value, val.(Content).Key, val.(Content).Value)\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif val != nil {\n\t\t\t\tt.Errorf(\"Did not expect key %v, but found key:value %v:%v\", test.test.Key, val.(Content).Key, val.(Content).Value)\n\t\t\t} else {\n\t\t\t\tt.Logf(\"Didn't find %v, but wasn't expected\", test.test.Key)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Iterate in order\n\tt.Logf(\"\\nIn-order Iteration:\\n\")\n\tpos := 0\n\n\tif tree.Len() != 14 {\n\t\tt.Errorf(\"Tree length wrong. Expected 14 but got %d\", tree.Len())\n\t}\n\n\tsortedInsertData := insertData.Copy()\n\tsort.Sort(sortedInsertData)\n\n\tt.Logf(\"Insert Data Length: %d\", len(insertData))\n\tt.Logf(\"Sorted Data Length: %d\", len(sortedInsertData))\n\tt.Logf(\"Tree Length: %d\", tree.Len())\n\n\ttree.Ascend(func(_record Record) bool {\n\t\trecord := _record.(Content)\n\t\tt.Logf(\"Key:Value == %v:%v\", record.Key, record.Value)\n\t\tif record.Key != sortedInsertData[pos].Key {\n\t\t\tt.Errorf(\"Out of order! Expected %v, but got %v\", sortedInsertData[pos].Key, record.Key)\n\t\t}\n\t\tpos++\n\t\treturn true\n\t})\n\t// // Reverse Iterate\n\tt.Logf(\"\\nReverse-order Iteration:\\n\")\n\tpos = len(sortedInsertData) - 1\n\n\ttree.Descend(func(_record Record) bool {\n\t\trecord := _record.(Content)\n\t\tt.Logf(\"Key:Value == %v:%v\", record.Key, record.Value)\n\t\tif record.Key != sortedInsertData[pos].Key {\n\t\t\tt.Errorf(\"Out of order! Expected %v, but got %v\", sortedInsertData[pos].Key, record.Key)\n\t\t}\n\t\tpos--\n\t\treturn true\n\t})\n\n\tdeleteTests := []Content{\n\t\t{Key: 10, Value: \"Value_10\"},\n\t\t{Key: 15, Value: \"\"},\n\t\t{Key: \"banana\", Value: \"Fruit_banana\"},\n\t\t{Key: \"kiwi\", Value: \"\"},\n\t}\n\tfor _, test := range deleteTests {\n\t\tfmt.Printf(\"\\nDeleting %+v\\n\", test)\n\t\ttree.Delete(test)\n\t}\n\n\tif tree.Len() != 12 {\n\t\tt.Errorf(\"Tree length wrong. Expected 12 but got %d\", tree.Len())\n\t}\n\n\tfor _, test := range deleteTests {\n\t\tval := tree.Get(test)\n\t\tif val != nil {\n\t\t\tt.Errorf(\"Did not expect key %v, but found key:value %v:%v\", test.Key, val.(Content).Key, val.(Content).Value)\n\t\t} else {\n\t\t\tt.Logf(\"Didn't find %v, but wasn't expected\", test.Key)\n\t\t}\n\t}\n}\n\n// Write a test that populates a large B-Tree with 1000 records.\n// It should then `Clone` the tree, make some changes to both the original and the clone,\n// And then clone the clone, and make some changes to all three trees, and then check that the changes are isolated\n// to the tree they were made in.\nfunc TestBTreeCloneIsolation(t *testing.T) {\n\tt.Logf(\"Creating B-Tree of degree 10 with 1000 records\\n\")\n\tsize := 1000\n\ttree := genericSeeding(New(WithDegree(10)), size)\n\n\t// Clone the tree\n\tt.Logf(\"Cloning the tree\\n\")\n\tclone := tree.Clone()\n\n\t// Make some changes to the original and the clone\n\tt.Logf(\"Making changes to the original and the clone\\n\")\n\tfor i := 0; i \u003c size; i += 2 {\n\t\tcontent := Content{Key: i, Value: fmt.Sprintf(\"Value_%d\", i)}\n\t\ttree.Delete(content)\n\t\tcontent = Content{Key: i + 1, Value: fmt.Sprintf(\"Value_%d\", i+1)}\n\t\tclone.Delete(content)\n\t}\n\n\t// Clone the clone\n\tt.Logf(\"Cloning the clone\\n\")\n\tclone2 := clone.Clone()\n\n\t// Make some changes to all three trees\n\tt.Logf(\"Making changes to all three trees\\n\")\n\tfor i := 0; i \u003c size; i += 3 {\n\t\tcontent := Content{Key: i, Value: fmt.Sprintf(\"Value_%d\", i)}\n\t\ttree.Delete(content)\n\t\tcontent = Content{Key: i, Value: fmt.Sprintf(\"Value_%d\", i+1)}\n\t\tclone.Delete(content)\n\t\tcontent = Content{Key: i + 2, Value: fmt.Sprintf(\"Value_%d\", i+2)}\n\t\tclone2.Delete(content)\n\t}\n\n\t// Check that the changes are isolated to the tree they were made in\n\tt.Logf(\"Checking that the changes are isolated to the tree they were made in\\n\")\n\tfor i := 0; i \u003c size; i++ {\n\t\tcontent := Content{Key: i, Value: fmt.Sprintf(\"Value_%d\", i)}\n\t\tval := tree.Get(content)\n\n\t\tif i%3 == 0 || i%2 == 0 {\n\t\t\tif val != nil {\n\t\t\t\tt.Errorf(\"Didn't expect key %v, but found key:value %v:%v\", content.Key, val.(Content).Key, val.(Content).Value)\n\t\t\t}\n\t\t} else {\n\t\t\tif val == nil {\n\t\t\t\tt.Errorf(\"Expected key %v, but didn't find it\", content.Key)\n\t\t\t}\n\t\t}\n\n\t\tval = clone.Get(content)\n\t\tif i%2 != 0 || i%3 == 0 {\n\t\t\tif val != nil {\n\t\t\t\tt.Errorf(\"Didn't expect key %v, but found key:value %v:%v\", content.Key, val.(Content).Key, val.(Content).Value)\n\t\t\t}\n\t\t} else {\n\t\t\tif val == nil {\n\t\t\t\tt.Errorf(\"Expected key %v, but didn't find it\", content.Key)\n\t\t\t}\n\t\t}\n\n\t\tval = clone2.Get(content)\n\t\tif i%2 != 0 || (i-2)%3 == 0 {\n\t\t\tif val != nil {\n\t\t\t\tt.Errorf(\"Didn't expect key %v, but found key:value %v:%v\", content.Key, val.(Content).Key, val.(Content).Value)\n\t\t\t}\n\t\t} else {\n\t\t\tif val == nil {\n\t\t\t\tt.Errorf(\"Expected key %v, but didn't find it\", content.Key)\n\t\t\t}\n\t\t}\n\t}\n}\n\n// --------------------\n// Stress tests. Disabled for testing performance\n\n//func TestStress(t *testing.T) {\n//\t// Loop through creating B-Trees with a range of degrees from 3 to 12, stepping by 3.\n//\t// Insert 1000 records into each tree, then search for each record.\n//\t// Delete half of the records, skipping every other one, then search for each record.\n//\n//\tfor degree := 3; degree \u003c= 12; degree += 3 {\n//\t\tt.Logf(\"Testing B-Tree of degree %d\\n\", degree)\n//\t\ttree := New(WithDegree(degree))\n//\n//\t\t// Insert 1000 records\n//\t\tt.Logf(\"Inserting 1000 records\\n\")\n//\t\tfor i := 0; i \u003c 1000; i++ {\n//\t\t\tcontent := Content{Key: i, Value: fmt.Sprintf(\"Value_%d\", i)}\n//\t\t\ttree.Insert(content)\n//\t\t}\n//\n//\t\t// Search for all records\n//\t\tfor i := 0; i \u003c 1000; i++ {\n//\t\t\tcontent := Content{Key: i, Value: fmt.Sprintf(\"Value_%d\", i)}\n//\t\t\tval := tree.Get(content)\n//\t\t\tif val == nil {\n//\t\t\t\tt.Errorf(\"Expected key %v, but didn't find it\", content.Key)\n//\t\t\t}\n//\t\t}\n//\n//\t\t// Delete half of the records\n//\t\tfor i := 0; i \u003c 1000; i += 2 {\n//\t\t\tcontent := Content{Key: i, Value: fmt.Sprintf(\"Value_%d\", i)}\n//\t\t\ttree.Delete(content)\n//\t\t}\n//\n//\t\t// Search for all records\n//\t\tfor i := 0; i \u003c 1000; i++ {\n//\t\t\tcontent := Content{Key: i, Value: fmt.Sprintf(\"Value_%d\", i)}\n//\t\t\tval := tree.Get(content)\n//\t\t\tif i%2 == 0 {\n//\t\t\t\tif val != nil {\n//\t\t\t\t\tt.Errorf(\"Didn't expect key %v, but found key:value %v:%v\", content.Key, val.(Content).Key, val.(Content).Value)\n//\t\t\t\t}\n//\t\t\t} else {\n//\t\t\t\tif val == nil {\n//\t\t\t\t\tt.Errorf(\"Expected key %v, but didn't find it\", content.Key)\n//\t\t\t\t}\n//\t\t\t}\n//\t\t}\n//\t}\n//\n//\t// Now create a very large tree, with 100000 records\n//\t// Then delete roughly one third of them, using a very basic random number generation scheme\n//\t// (implement it right here) to determine which records to delete.\n//\t// Print a few lines using Logf to let the user know what's happening.\n//\n//\tt.Logf(\"Testing B-Tree of degree 10 with 100000 records\\n\")\n//\ttree := New(WithDegree(10))\n//\n//\t// Insert 100000 records\n//\tt.Logf(\"Inserting 100000 records\\n\")\n//\tfor i := 0; i \u003c 100000; i++ {\n//\t\tcontent := Content{Key: i, Value: fmt.Sprintf(\"Value_%d\", i)}\n//\t\ttree.Insert(content)\n//\t}\n//\n//\t// Implement a very basic random number generator\n//\tseed := 0\n//\trandom := func() int {\n//\t\tseed = (seed*1103515245 + 12345) \u0026 0x7fffffff\n//\t\treturn seed\n//\t}\n//\n//\t// Delete one third of the records\n//\tt.Logf(\"Deleting one third of the records\\n\")\n//\tfor i := 0; i \u003c 35000; i++ {\n//\t\tcontent := Content{Key: random() % 100000, Value: fmt.Sprintf(\"Value_%d\", i)}\n//\t\ttree.Delete(content)\n//\t}\n//}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/btree\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"alYb0MUYW3z312DsdZNQvohCnDRfeWmNifArofrGngU4+vQbPAyn4Sm8Ut418As2y/+cIEDMPBZkNOJyJNrfVQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"context","path":"gno.land/p/demo/context","files":[{"name":"context.gno","body":"// Package context provides a minimal implementation of Go context with support\n// for Value and WithValue.\n//\n// Adapted from https://github.com/golang/go/tree/master/src/context/.\n// Copyright 2016 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.\npackage context\n\ntype Context interface {\n\t// Value returns the value associated with this context for key, or nil\n\t// if no value is associated with key.\n\tValue(key any) any\n}\n\n// Empty returns a non-nil, empty context, similar with context.Background and\n// context.TODO in Go.\nfunc Empty() Context {\n\treturn \u0026emptyCtx{}\n}\n\ntype emptyCtx struct{}\n\nfunc (ctx emptyCtx) Value(key any) any {\n\treturn nil\n}\n\nfunc (ctx emptyCtx) String() string {\n\treturn \"context.Empty\"\n}\n\ntype valueCtx struct {\n\tparent   Context\n\tkey, val any\n}\n\nfunc (ctx *valueCtx) Value(key any) any {\n\tif ctx.key == key {\n\t\treturn ctx.val\n\t}\n\treturn ctx.parent.Value(key)\n}\n\nfunc stringify(v any) string {\n\tswitch s := v.(type) {\n\tcase stringer:\n\t\treturn s.String()\n\tcase string:\n\t\treturn s\n\t}\n\treturn \"non-stringer\"\n}\n\ntype stringer interface {\n\tString() string\n}\n\nfunc (c *valueCtx) String() string {\n\treturn stringify(c.parent) + \".WithValue(\" +\n\t\tstringify(c.key) + \", \" +\n\t\tstringify(c.val) + \")\"\n}\n\n// WithValue returns a copy of parent in which the value associated with key is\n// val.\nfunc WithValue(parent Context, key, val any) Context {\n\tif key == nil {\n\t\tpanic(\"nil key\")\n\t}\n\t// XXX: if !reflect.TypeOf(key).Comparable() { panic(\"key is not comparable\") }\n\treturn \u0026valueCtx{parent, key, val}\n}\n"},{"name":"context_test.gno","body":"package context\n\nimport \"testing\"\n\nfunc TestContextExample(t *testing.T) {\n\ttype favContextKey string\n\n\tk := favContextKey(\"language\")\n\tctx := WithValue(Empty(), k, \"Gno\")\n\n\tif v := ctx.Value(k); v != nil {\n\t\tif v.(string) != \"Gno\" {\n\t\t\tt.Errorf(\"language value should be Gno, but is %s\", v)\n\t\t}\n\t} else {\n\t\tt.Errorf(\"language key value was not found\")\n\t}\n\n\tif v := ctx.Value(favContextKey(\"color\")); v != nil {\n\t\tt.Errorf(\"color key was found\")\n\t}\n}\n\n// otherContext is a Context that's not one of the types defined in context.go.\n// This lets us test code paths that differ based on the underlying type of the\n// Context.\ntype otherContext struct {\n\tContext\n}\n\ntype (\n\tkey1 int\n\tkey2 int\n)\n\n// func (k key2) String() string { return fmt.Sprintf(\"%[1]T(%[1]d)\", k) }\n\nvar (\n\tk1 = key1(1)\n\tk2 = key2(1) // same int as k1, different type\n\tk3 = key2(3) // same type as k2, different int\n)\n\nfunc TestValues(t *testing.T) {\n\tcheck := func(c Context, nm, v1, v2, v3 string) {\n\t\tif v, ok := c.Value(k1).(string); ok == (len(v1) == 0) || v != v1 {\n\t\t\tt.Errorf(`%s.Value(k1).(string) = %q, %t want %q, %t`, nm, v, ok, v1, len(v1) != 0)\n\t\t}\n\t\tif v, ok := c.Value(k2).(string); ok == (len(v2) == 0) || v != v2 {\n\t\t\tt.Errorf(`%s.Value(k2).(string) = %q, %t want %q, %t`, nm, v, ok, v2, len(v2) != 0)\n\t\t}\n\t\tif v, ok := c.Value(k3).(string); ok == (len(v3) == 0) || v != v3 {\n\t\t\tt.Errorf(`%s.Value(k3).(string) = %q, %t want %q, %t`, nm, v, ok, v3, len(v3) != 0)\n\t\t}\n\t}\n\n\tc0 := Empty()\n\tcheck(c0, \"c0\", \"\", \"\", \"\")\n\n\tt.Skip() // XXX: depends on https://github.com/gnolang/gno/issues/2386\n\n\tc1 := WithValue(Empty(), k1, \"c1k1\")\n\tcheck(c1, \"c1\", \"c1k1\", \"\", \"\")\n\n\t/*if got, want := c1.String(), `context.Empty.WithValue(context_test.key1, c1k1)`; got != want {\n\t\tt.Errorf(\"c.String() = %q want %q\", got, want)\n\t}*/\n\n\tc2 := WithValue(c1, k2, \"c2k2\")\n\tcheck(c2, \"c2\", \"c1k1\", \"c2k2\", \"\")\n\n\t/*if got, want := fmt.Sprint(c2), `context.Empty.WithValue(context_test.key1, c1k1).WithValue(context_test.key2(1), c2k2)`; got != want {\n\t\tt.Errorf(\"c.String() = %q want %q\", got, want)\n\t}*/\n\n\tc3 := WithValue(c2, k3, \"c3k3\")\n\tcheck(c3, \"c2\", \"c1k1\", \"c2k2\", \"c3k3\")\n\n\tc4 := WithValue(c3, k1, nil)\n\tcheck(c4, \"c4\", \"\", \"c2k2\", \"c3k3\")\n\n\to0 := otherContext{Empty()}\n\tcheck(o0, \"o0\", \"\", \"\", \"\")\n\n\to1 := otherContext{WithValue(Empty(), k1, \"c1k1\")}\n\tcheck(o1, \"o1\", \"c1k1\", \"\", \"\")\n\n\to2 := WithValue(o1, k2, \"o2k2\")\n\tcheck(o2, \"o2\", \"c1k1\", \"o2k2\", \"\")\n\n\to3 := otherContext{c4}\n\tcheck(o3, \"o3\", \"\", \"c2k2\", \"c3k3\")\n\n\to4 := WithValue(o3, k3, nil)\n\tcheck(o4, \"o4\", \"\", \"c2k2\", \"\")\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/context\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"zJCjAl47XikLikHNVz0kyJ04YcBT1tMTEbF5Fyk/EqYYnF4VSd0GyaAneXckSBg2PD03XrdIICLXvoSTjSkz9Q=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"entropy","path":"gno.land/p/demo/entropy","files":[{"name":"entropy.gno","body":"// Entropy generates fully deterministic, cost-effective, and hard to guess\n// numbers.\n//\n// It is designed both for single-usage, like seeding math/rand or for being\n// reused which increases the entropy and its cost effectiveness.\n//\n// Disclaimer: this package is unsafe and won't prevent others to guess values\n// in advance.\n//\n// It uses the Bernstein's hash djb2 to be CPU-cycle efficient.\npackage entropy\n\nimport (\n\t\"chain/runtime\"\n\t\"chain/runtime/unsafe\"\n\t\"math\"\n\t\"time\"\n)\n\ntype Instance struct {\n\tvalue uint32\n}\n\nfunc New() *Instance {\n\tr := Instance{value: 5381}\n\tr.addEntropy()\n\treturn \u0026r\n}\n\nfunc FromSeed(seed uint32) *Instance {\n\tr := Instance{value: seed}\n\tr.addEntropy()\n\treturn \u0026r\n}\n\nfunc (i *Instance) Seed() uint32 {\n\treturn i.value\n}\n\nfunc (i *Instance) djb2String(input string) {\n\tfor _, c := range input {\n\t\ti.djb2Uint32(uint32(c))\n\t}\n}\n\n// super fast random algorithm.\n// http://www.cse.yorku.ca/~oz/hash.html\nfunc (i *Instance) djb2Uint32(input uint32) {\n\ti.value = (i.value \u003c\u003c 5) + i.value + input\n}\n\n// AddEntropy uses various runtime variables to add entropy to the existing seed.\nfunc (i *Instance) addEntropy() {\n\t// FIXME: reapply the 5381 initial value?\n\n\t// inherit previous entropy\n\t// nothing to do\n\n\t// handle callers\n\t{\n\t\tcurrentRealm := unsafe.CurrentRealm().Address().String()\n\t\ti.djb2String(currentRealm)\n\t\toriginCaller := unsafe.OriginCaller().String()\n\t\ti.djb2String(originCaller)\n\t}\n\n\t// height\n\t{\n\t\theight := runtime.ChainHeight()\n\t\tif height \u003e= math.MaxUint32 {\n\t\t\theight -= math.MaxUint32\n\t\t}\n\t\ti.djb2Uint32(uint32(height))\n\t}\n\n\t// time\n\t{\n\t\tsecs := time.Now().Second()\n\t\ti.djb2Uint32(uint32(secs))\n\t\tnsecs := time.Now().Nanosecond()\n\t\ti.djb2Uint32(uint32(nsecs))\n\t}\n\n\t// FIXME: compute other hard-to-guess but deterministic variables, like real gas?\n}\n\nfunc (i *Instance) Value() uint32 {\n\ti.addEntropy()\n\treturn i.value\n}\n\nfunc (i *Instance) Value64() uint64 {\n\ti.addEntropy()\n\thigh := i.value\n\ti.addEntropy()\n\n\treturn (uint64(high) \u003c\u003c 32) | uint64(i.value)\n}\n"},{"name":"entropy_test.gno","body":"package entropy\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n)\n\nfunc TestInstance(t *testing.T) {\n\tinstance := New()\n\tif instance == nil {\n\t\tt.Errorf(\"instance should not be nil\")\n\t}\n}\n\nfunc TestInstanceValue(t *testing.T) {\n\tbaseEntropy := New()\n\tbaseResult := computeValue(t, baseEntropy)\n\n\tsameHeightEntropy := New()\n\tsameHeightResult := computeValue(t, sameHeightEntropy)\n\n\tif baseResult != sameHeightResult {\n\t\tt.Errorf(\"should have the same result: new=%s, base=%s\", sameHeightResult, baseResult)\n\t}\n\n\ttesting.SkipHeights(1)\n\tdifferentHeightEntropy := New()\n\tdifferentHeightResult := computeValue(t, differentHeightEntropy)\n\n\tif baseResult == differentHeightResult {\n\t\tt.Errorf(\"should have different result: new=%s, base=%s\", differentHeightResult, baseResult)\n\t}\n}\n\nfunc TestInstanceValue64(t *testing.T) {\n\tbaseEntropy := New()\n\tbaseResult := computeValue64(t, baseEntropy)\n\n\tsameHeightEntropy := New()\n\tsameHeightResult := computeValue64(t, sameHeightEntropy)\n\n\tif baseResult != sameHeightResult {\n\t\tt.Errorf(\"should have the same result: new=%s, base=%s\", sameHeightResult, baseResult)\n\t}\n\n\ttesting.SkipHeights(1)\n\tdifferentHeightEntropy := New()\n\tdifferentHeightResult := computeValue64(t, differentHeightEntropy)\n\n\tif baseResult == differentHeightResult {\n\t\tt.Errorf(\"should have different result: new=%s, base=%s\", differentHeightResult, baseResult)\n\t}\n}\n\nfunc computeValue(t *testing.T, r *Instance) string {\n\tt.Helper()\n\n\tout := \"\"\n\tfor i := 0; i \u003c 10; i++ {\n\t\tval := int(r.Value())\n\t\tout += strconv.Itoa(val) + \" \"\n\t}\n\n\treturn out\n}\n\nfunc computeValue64(t *testing.T, r *Instance) string {\n\tt.Helper()\n\n\tout := \"\"\n\tfor i := 0; i \u003c 10; i++ {\n\t\tval := int(r.Value64())\n\t\tout += strconv.Itoa(val) + \" \"\n\t}\n\n\treturn out\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/entropy\"\ngno = \"0.9\"\n"},{"name":"z_filetest.gno","body":"package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/entropy\"\n)\n\nfunc main() {\n\t// initial\n\tprintln(\"---\")\n\tr := entropy.New()\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value64())\n\n\t// should be the same\n\tprintln(\"---\")\n\tr = entropy.New()\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value64())\n\n\ttesting.SkipHeights(1)\n\tprintln(\"---\")\n\tr = entropy.New()\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value64())\n}\n\n// Output:\n// ---\n// 2916840437\n// 3981870829\n// 1311573733\n// 2267403229\n// 484167125\n// 5277608318285564101\n// ---\n// 2916840437\n// 3981870829\n// 1311573733\n// 2267403229\n// 484167125\n// 5277608318285564101\n// ---\n// 3603434145\n// 911682639\n// 1320246589\n// 2861798763\n// 1938803929\n// 17792872788806539637\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"M8McjrdT77P3zktigejTPMnOJEr69suT1J6JSVdOF31PBO+KUVYJVEhL885Ou+gr8rbTtRQ2N4wdpIbzN0S/sQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"faker","path":"gno.land/p/demo/faker","files":[{"name":"data.gno","body":"package faker\n\nvar (\n\tFirstNames = []string{\n\t\t\"James\", \"Mary\", \"John\", \"Patricia\", \"Robert\", \"Jennifer\", \"Michael\", \"Linda\",\n\t\t\"William\", \"Elizabeth\", \"David\", \"Barbara\", \"Richard\", \"Susan\", \"Joseph\", \"Jessica\",\n\t\t\"Thomas\", \"Sarah\", \"Christopher\", \"Karen\", \"Charles\", \"Nancy\", \"Daniel\", \"Lisa\",\n\t\t\"Matthew\", \"Betty\", \"Anthony\", \"Helen\", \"Mark\", \"Sandra\", \"Donald\", \"Donna\",\n\t\t\"Steven\", \"Carol\", \"Andrew\", \"Ruth\", \"Kenneth\", \"Sharon\", \"Paul\", \"Michelle\",\n\t\t\"Joshua\", \"Laura\", \"Kevin\", \"Sarah\", \"Brian\", \"Kimberly\", \"George\", \"Deborah\",\n\t\t\"Timothy\", \"Dorothy\", \"Ronald\", \"Lisa\", \"Jason\", \"Nancy\", \"Edward\", \"Karen\",\n\t\t\"Jeffrey\", \"Betty\", \"Ryan\", \"Helen\", \"Jacob\", \"Sandra\", \"Gary\", \"Donna\",\n\t\t\"Nicholas\", \"Carol\", \"Eric\", \"Ruth\", \"Jonathan\", \"Sharon\", \"Stephen\", \"Michelle\",\n\t}\n\n\tLastNames = []string{\n\t\t\"Smith\", \"Johnson\", \"Williams\", \"Brown\", \"Jones\", \"Garcia\", \"Miller\", \"Davis\",\n\t\t\"Rodriguez\", \"Martinez\", \"Hernandez\", \"Lopez\", \"Gonzalez\", \"Wilson\", \"Anderson\", \"Thomas\",\n\t\t\"Taylor\", \"Moore\", \"Jackson\", \"Martin\", \"Lee\", \"Perez\", \"Thompson\", \"White\",\n\t\t\"Harris\", \"Sanchez\", \"Clark\", \"Ramirez\", \"Lewis\", \"Robinson\", \"Walker\", \"Young\",\n\t\t\"Allen\", \"King\", \"Wright\", \"Scott\", \"Torres\", \"Nguyen\", \"Hill\", \"Flores\",\n\t\t\"Green\", \"Adams\", \"Nelson\", \"Baker\", \"Hall\", \"Rivera\", \"Campbell\", \"Mitchell\",\n\t\t\"Carter\", \"Roberts\", \"Gomez\", \"Phillips\", \"Evans\", \"Turner\", \"Diaz\", \"Parker\",\n\t\t\"Cruz\", \"Edwards\", \"Collins\", \"Reyes\", \"Stewart\", \"Morris\", \"Morales\", \"Murphy\",\n\t}\n\n\tCities = []string{\n\t\t\"New York\", \"Los Angeles\", \"Chicago\", \"Houston\", \"Phoenix\", \"Philadelphia\",\n\t\t\"San Antonio\", \"San Diego\", \"Dallas\", \"San Jose\", \"Austin\", \"Jacksonville\",\n\t\t\"Fort Worth\", \"Columbus\", \"Charlotte\", \"San Francisco\", \"Indianapolis\", \"Seattle\",\n\t\t\"Denver\", \"Washington\", \"Boston\", \"El Paso\", \"Nashville\", \"Detroit\", \"Oklahoma City\",\n\t\t\"Portland\", \"Las Vegas\", \"Memphis\", \"Louisville\", \"Baltimore\", \"Milwaukee\",\n\t\t\"Albuquerque\", \"Tucson\", \"Fresno\", \"Sacramento\", \"Mesa\", \"Kansas City\",\n\t\t\"Atlanta\", \"Long Beach\", \"Colorado Springs\", \"Raleigh\", \"Miami\", \"Virginia Beach\",\n\t\t\"Omaha\", \"Oakland\", \"Minneapolis\", \"Tulsa\", \"Arlington\", \"Tampa\", \"New Orleans\",\n\t}\n\n\tCountries = []string{\n\t\t\"United States\", \"Canada\", \"Mexico\", \"United Kingdom\", \"France\", \"Germany\",\n\t\t\"Italy\", \"Spain\", \"Japan\", \"China\", \"India\", \"Brazil\", \"Argentina\", \"Australia\",\n\t\t\"Russia\", \"South Korea\", \"Netherlands\", \"Belgium\", \"Switzerland\", \"Sweden\",\n\t\t\"Norway\", \"Denmark\", \"Finland\", \"Poland\", \"Portugal\", \"Greece\", \"Turkey\",\n\t\t\"Egypt\", \"South Africa\", \"Nigeria\", \"Kenya\", \"Morocco\", \"Israel\", \"Saudi Arabia\",\n\t\t\"United Arab Emirates\", \"Thailand\", \"Singapore\", \"Malaysia\", \"Indonesia\", \"Philippines\",\n\t\t\"Vietnam\", \"Taiwan\", \"Hong Kong\", \"New Zealand\", \"Chile\", \"Peru\", \"Colombia\",\n\t}\n\n\tStreetNames = []string{\n\t\t\"Main St\", \"Oak Ave\", \"Elm St\", \"First St\", \"Second Ave\", \"Park Rd\",\n\t\t\"Maple Dr\", \"Cedar Ln\", \"Pine St\", \"Washington Ave\", \"Broadway\",\n\t\t\"Church St\", \"Market St\", \"Union Ave\", \"High St\", \"Mill Rd\",\n\t\t\"School Dr\", \"Hill St\", \"River Rd\", \"Spring Ave\",\n\t}\n\n\tEmailDomains = []string{\n\t\t\"example.com\", \"test.com\", \"demo.org\", \"sample.net\", \"fake.io\",\n\t}\n\n\tLoremWords = []string{\n\t\t\"lorem\", \"ipsum\", \"dolor\", \"sit\", \"amet\", \"consectetur\", \"adipiscing\", \"elit\",\n\t\t\"sed\", \"do\", \"eiusmod\", \"tempor\", \"incididunt\", \"ut\", \"labore\", \"et\", \"dolore\",\n\t\t\"magna\", \"aliqua\", \"enim\", \"ad\", \"minim\", \"veniam\", \"quis\", \"nostrud\",\n\t\t\"exercitation\", \"ullamco\", \"laboris\", \"nisi\", \"aliquip\", \"ex\", \"ea\", \"commodo\",\n\t\t\"consequat\", \"duis\", \"aute\", \"irure\", \"in\", \"reprehenderit\", \"voluptate\",\n\t\t\"velit\", \"esse\", \"cillum\", \"fugiat\", \"nulla\", \"pariatur\", \"excepteur\", \"sint\",\n\t\t\"occaecat\", \"cupidatat\", \"non\", \"proident\", \"sunt\", \"culpa\", \"qui\", \"officia\",\n\t\t\"deserunt\", \"mollit\", \"anim\", \"id\", \"est\", \"laborum\", \"at\", \"vero\", \"eos\",\n\t\t\"accusamus\", \"accusantium\", \"doloremque\", \"laudantium\", \"totam\", \"rem\",\n\t\t\"aperiam\", \"eaque\", \"ipsa\", \"quae\", \"ab\", \"illo\", \"inventore\", \"veritatis\",\n\t\t\"et\", \"quasi\", \"architecto\", \"beatae\", \"vitae\", \"dicta\", \"sunt\", \"explicabo\",\n\t\t\"nemo\", \"ipsam\", \"voluptatem\", \"quia\", \"voluptas\", \"aspernatur\", \"aut\", \"odit\",\n\t\t\"fugit\", \"sed\", \"quia\", \"consequuntur\", \"magni\", \"dolores\", \"ratione\",\n\t\t\"sequi\", \"nesciunt\", \"neque\", \"porro\", \"quisquam\", \"qui\", \"dolorem\",\n\t\t\"ipsum\", \"quia\", \"dolor\", \"sit\", \"amet\", \"consectetur\", \"adipisci\",\n\t\t\"numquam\", \"eius\", \"modi\", \"tempora\", \"incidunt\", \"ut\", \"labore\",\n\t\t\"et\", \"dolore\", \"magnam\", \"aliquam\", \"quaerat\", \"voluptatem\",\n\t\t\"ut\", \"enim\", \"ad\", \"minima\", \"veniam\", \"quis\", \"nostrum\",\n\t\t\"exercitationem\", \"ullam\", \"corporis\", \"suscipit\", \"laboriosam\",\n\t}\n)\n"},{"name":"faker.gno","body":"// Package faker provides fake data generation utilities for testing and\n// development purposes. It includes methods for generating names, addresses,\n// dates, emails, and other common data types using a pseudorandom generator.\npackage faker\n\nimport (\n\t\"crypto/bech32\"\n\t\"math/rand\"\n\t\"strconv\"\n\t\"strings\"\n\t\"unicode\"\n\n\t\"gno.land/p/demo/entropy\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Faker provides fake data generation using a pseudorandom number generator.\ntype Faker struct {\n\trng *rand.Rand\n}\n\n// NewGenerator creates a new Faker instance with a random seed.\nfunc NewGenerator() *Faker {\n\treturn NewGeneratorWithSeed(entropy.New().Value64())\n}\n\n// NewGeneratorWithSeed creates a new Faker instance with a specified seed.\nfunc NewGeneratorWithSeed(seed uint64) *Faker {\n\treturn \u0026Faker{\n\t\trng: rand.New(rand.NewPCG(seed, 0xdeadbeef)),\n\t}\n}\n\n// FirstName returns a random first name.\nfunc (f *Faker) FirstName() string {\n\treturn f.pickRandom(\u0026FirstNames)\n}\n\n// LastName returns a random last name.\nfunc (f *Faker) LastName() string {\n\treturn f.pickRandom(\u0026LastNames)\n}\n\n// FullName returns a random full name (first name + last name).\nfunc (f *Faker) FullName() string {\n\treturn f.FirstName() + \" \" + f.LastName()\n}\n\n// Age returns a random age between min and min+80.\nfunc (f *Faker) Age(min int) int {\n\treturn f.rng.IntN(80) + min\n}\n\n// City returns a random city name.\nfunc (f *Faker) City() string {\n\treturn f.pickRandom(\u0026Cities)\n}\n\n// Country returns a random country name.\nfunc (f *Faker) Country() string {\n\treturn f.pickRandom(\u0026Countries)\n}\n\n// Address returns a random address in the format \"123 Street Name, City, Country\".\nfunc (f *Faker) Address() string {\n\tvar (\n\t\tstreetNum  = strconv.Itoa(f.rng.IntN(9999) + 1)\n\t\tstreetName = f.pickRandom(\u0026StreetNames)\n\t\tcity       = f.City()\n\t\tcountry    = f.Country()\n\t)\n\n\treturn ufmt.Sprintf(\"%s %s, %s, %s\", streetNum, streetName, city, country)\n}\n\n// Lorem returns random lorem ipsum text with the specified number of words.\nfunc (f *Faker) Lorem(wordCount int) string {\n\tif wordCount \u003c= 0 {\n\t\treturn \"\"\n\t}\n\n\tresult := \"\"\n\n\tfor i := 0; i \u003c wordCount; i++ {\n\t\tif i \u003e 0 {\n\t\t\tresult += \" \"\n\t\t}\n\t\tresult += f.pickRandom(\u0026LoremWords)\n\t}\n\n\treturn result\n}\n\n// LoremSentence returns a random lorem ipsum sentence with 5-15 words.\nfunc (f *Faker) LoremSentence() string {\n\tvar (\n\t\twordCount = f.rng.IntN(10) + 5\n\t\tsentence  = f.Lorem(wordCount)\n\t)\n\n\tif len(sentence) \u003e 0 {\n\t\tsentence = string(sentence[0]-32) + sentence[1:] + \".\"\n\t}\n\n\treturn sentence\n}\n\n// LoremParagraph returns a random lorem ipsum paragraph with 3-8 sentences.\nfunc (f *Faker) LoremParagraph() string {\n\tvar (\n\t\tsentenceCount = f.rng.IntN(5) + 3\n\t\tparagraph     = \"\"\n\t)\n\n\tfor i := 0; i \u003c sentenceCount; i++ {\n\t\tif i \u003e 0 {\n\t\t\tparagraph += \" \"\n\t\t}\n\t\tparagraph += f.LoremSentence()\n\t}\n\n\treturn paragraph\n}\n\n// Date returns a random date in YYYY-MM-DD format between 1974 and 2025.\nfunc (f *Faker) Date() string {\n\tvar (\n\t\tyear  = strconv.Itoa(f.rng.IntN(52) + 1974)\n\t\tmonth = itoa2Digits(f.rng.IntN(12) + 1)\n\t\tday   = itoa2Digits(f.rng.IntN(28) + 1) // Don't bother handling month lengths\n\t)\n\n\treturn ufmt.Sprintf(\"%s-%s-%s\", year, month, day)\n}\n\n// Time returns a random time in HH:MM:SS format (24-hour).\nfunc (f *Faker) Time() string {\n\tvar (\n\t\thour   = itoa2Digits(f.rng.IntN(24))\n\t\tminute = itoa2Digits(f.rng.IntN(60))\n\t\tsecond = itoa2Digits(f.rng.IntN(60))\n\t)\n\n\treturn ufmt.Sprintf(\"%s:%s:%s\", hour, minute, second)\n}\n\n// DateTime returns a random date and time in \"YYYY-MM-DD HH:MM:SS\" format.\nfunc (f *Faker) DateTime() string {\n\treturn f.Date() + \" \" + f.Time()\n}\n\n// Email returns a random email address in firstname.lastname@domain format.\nfunc (f *Faker) Email() string {\n\tvar (\n\t\tfirstName = strings.ToLower(f.FirstName())\n\t\tlastName  = strings.ToLower(f.LastName())\n\t\tdomain    = f.pickRandom(\u0026EmailDomains)\n\t)\n\n\treturn ufmt.Sprintf(\"%s.%s@%s\", firstName, lastName, domain)\n}\n\n// Phone returns a random phone number in (XXX) XXX-XXXX format.\nfunc (f *Faker) Phone() string {\n\tvar (\n\t\tarea     = f.rng.IntN(900) + 100\n\t\texchange = f.rng.IntN(900) + 100\n\t\tnumber   = f.rng.IntN(10000)\n\t)\n\n\treturn ufmt.Sprintf(\"(%d) %d-%d\", area, exchange, number)\n}\n\n// StdAddress returns a random chain.Address.\nfunc (f *Faker) StdAddress() address {\n\trawAddr := make([]byte, 32)\n\tfor i := 0; i \u003c 32; i++ {\n\t\trawAddr[i] = byte(f.rng.IntN(32))\n\t}\n\n\taddr, err := bech32.Encode(\"g\", rawAddr)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn address(addr)\n}\n\n// Username returns a random gno username following the rules:\n// - Start with minimum 3 letters (lowercase)\n// - End with minimum 3 numbers\n// - Less than 20 chars total\n// - Only letters, numbers, and underscore allowed\nfunc (f *Faker) Username() string {\n\tvar (\n\t\tusername = \"\"\n\t\tsources  = []*[]string{\n\t\t\t\u0026FirstNames,\n\t\t\t\u0026LastNames,\n\t\t\t\u0026LoremWords,\n\t\t}\n\t)\n\n\tfor {\n\t\tvar (\n\t\t\tsource = sources[f.rng.IntN(len(sources))]\n\t\t\tword   = strings.ToLower(f.pickRandomLettersOnly(source))\n\t\t\tnext   = username + word\n\t\t)\n\n\t\tif len(next) \u003e 16 \u0026\u0026 len(username) \u003e 3 {\n\t\t\tbreak\n\t\t} else if len(next) \u003c= 16 {\n\t\t\tusername = next\n\t\t}\n\t}\n\n\tfor i := 0; i \u003c 3; i++ {\n\t\tusername += strconv.Itoa(f.rng.IntN(10))\n\t}\n\n\treturn username\n}\n\nfunc (f *Faker) pickRandom(source *[]string) string {\n\treturn (*source)[f.rng.IntN(len(*source))]\n}\n\nfunc (f *Faker) pickRandomLettersOnly(source *[]string) string {\n\tisLettersOnly := func(s string) bool {\n\t\tfor _, r := range s {\n\t\t\tif !unicode.IsLetter(r) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\treturn true\n\t}\n\n\t// Limit attempts to 100 to avoid infinite loops.\n\tfor i := 0; i \u003c 100; i++ {\n\t\tword := f.pickRandom(source)\n\t\tif isLettersOnly(word) {\n\t\t\treturn word\n\t\t}\n\t}\n\n\tpanic(\"could not find a letters-only word after 100 attempts\")\n}\n\nfunc itoa2Digits(n int) string {\n\tstr := strconv.Itoa(n)\n\tif n \u003c 10 {\n\t\treturn \"0\" + str\n\t}\n\treturn str\n}\n\n// defaultFaker is the default Faker instance used by package-level functions.\nvar defaultFaker = NewGenerator()\n\n// FirstName returns a random first name using the default faker.\nfunc FirstName() string {\n\treturn defaultFaker.FirstName()\n}\n\n// LastName returns a random last name using the default faker.\nfunc LastName() string {\n\treturn defaultFaker.LastName()\n}\n\n// FullName returns a random full name using the default faker.\nfunc FullName() string {\n\treturn defaultFaker.FullName()\n}\n\n// Age returns a random age using the default faker.\nfunc Age(min int) int {\n\treturn defaultFaker.Age(min)\n}\n\n// City returns a random city name using the default faker.\nfunc City() string {\n\treturn defaultFaker.City()\n}\n\n// Country returns a random country name using the default faker.\nfunc Country() string {\n\treturn defaultFaker.Country()\n}\n\n// Address returns a random address using the default faker.\nfunc Address() string {\n\treturn defaultFaker.Address()\n}\n\n// Lorem returns random lorem ipsum text using the default faker.\nfunc Lorem(wordCount int) string {\n\treturn defaultFaker.Lorem(wordCount)\n}\n\n// LoremSentence returns a random lorem ipsum sentence using the default faker.\nfunc LoremSentence() string {\n\treturn defaultFaker.LoremSentence()\n}\n\n// LoremParagraph returns a random lorem ipsum paragraph using the default faker.\nfunc LoremParagraph() string {\n\treturn defaultFaker.LoremParagraph()\n}\n\n// Date returns a random date using the default faker.\nfunc Date() string {\n\treturn defaultFaker.Date()\n}\n\n// Time returns a random time using the default faker.\nfunc Time() string {\n\treturn defaultFaker.Time()\n}\n\n// DateTime returns a random date and time using the default faker.\nfunc DateTime() string {\n\treturn defaultFaker.DateTime()\n}\n\n// Email returns a random email address using the default faker.\nfunc Email() string {\n\treturn defaultFaker.Email()\n}\n\n// Phone returns a random phone number using the default faker.\nfunc Phone() string {\n\treturn defaultFaker.Phone()\n}\n\n// StdAddress returns a random chain.Address using the default faker.\nfunc StdAddress() address {\n\treturn defaultFaker.StdAddress()\n}\n\n// Username returns a random gno username using the default faker.\nfunc Username() string {\n\treturn defaultFaker.Username()\n}\n"},{"name":"faker_test.gno","body":"package faker\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// cur is a zero-value realm used as a placeholder when forwarding to\n// uassert/urequire dispatch helpers that gained an `rlm realm` param.\n// These tests pass `func()` callbacks (no crossing inside the callback),\n// so rlm is ignored — a nil realm here is safe.\nvar cur realm\n\nfunc TestNewGenerator(t *testing.T) {\n\tf := NewGenerator()\n\n\tuassert.NotNil(t, f)\n\tuassert.NotNil(t, f.rng)\n}\n\nfunc TestNewGeneratorWithSeed(t *testing.T) {\n\tvar (\n\t\tseed = uint64(12345)\n\t\tf    = NewGeneratorWithSeed(seed)\n\t)\n\n\tuassert.NotNil(t, f)\n\tuassert.NotNil(t, f.rng)\n}\n\nfunc TestDeterministicOutput(t *testing.T) {\n\tvar (\n\t\tseed = uint64(42)\n\t\tf1   = NewGeneratorWithSeed(seed)\n\t\tf2   = NewGeneratorWithSeed(seed)\n\t)\n\n\tfor i := 0; i \u003c 42; i++ {\n\t\tuassert.Equal(t, f1.FirstName(), f2.FirstName())\n\t\tuassert.Equal(t, f1.LastName(), f2.LastName())\n\t\tuassert.Equal(t, f1.FullName(), f2.FullName())\n\t\tuassert.Equal(t, f1.Age(0), f2.Age(0))\n\t\tuassert.Equal(t, f1.City(), f2.City())\n\t\tuassert.Equal(t, f1.Country(), f2.Country())\n\t\tuassert.Equal(t, f1.Address(), f2.Address())\n\t\tuassert.Equal(t, f1.Lorem(42), f2.Lorem(42))\n\t\tuassert.Equal(t, f1.LoremSentence(), f2.LoremSentence())\n\t\tuassert.Equal(t, f1.LoremParagraph(), f2.LoremParagraph())\n\t\tuassert.Equal(t, f1.Date(), f2.Date())\n\t\tuassert.Equal(t, f1.Time(), f2.Time())\n\t\tuassert.Equal(t, f1.DateTime(), f2.DateTime())\n\t\tuassert.Equal(t, f1.Email(), f2.Email())\n\t\tuassert.Equal(t, f1.Phone(), f2.Phone())\n\t\tuassert.Equal(t, f1.StdAddress(), f2.StdAddress())\n\t\tuassert.Equal(t, f1.Username(), f2.Username())\n\t}\n}\n\nfunc TestPickRandomLettersOnly(cur realm, t *testing.T) {\n\tf := NewGenerator()\n\n\t// Source without any letters only word\n\ttestData := []string{\"hello8\", \"123\", \"6world\", \"hey_hey\", \"ok.\"}\n\n\tuassert.PanicsWithMessage(\n\t\tt, cur,\n\t\t\"could not find a letters-only word after 100 attempts\",\n\t\tfunc() { f.pickRandomLettersOnly(\u0026testData) },\n\t)\n}\n\ntype goldenTestCase struct {\n\tseed       uint64\n\titerations []goldenValues\n}\n\ntype goldenValues struct {\n\tfirstName      string\n\tlastName       string\n\tfullName       string\n\tage            int\n\tcity           string\n\tcountry        string\n\taddress        string\n\tlorem          string\n\tloremSentence  string\n\tloremParagraph string\n\tdate           string\n\ttime           string\n\tdateTime       string\n\temail          string\n\tphone          string\n\tstdAddress     address\n\tusername       string\n}\n\nfunc TestGoldenValues(t *testing.T) {\n\tfor _, testCase := range goldenTestCases { // See golden_test_cases.gno\n\t\tf := NewGeneratorWithSeed(testCase.seed)\n\n\t\tfor i, c := range testCase.iterations {\n\t\t\tuassert.Equal(\n\t\t\t\tt,\n\t\t\t\tc.firstName,\n\t\t\t\tf.FirstName(),\n\t\t\t\tufmt.Sprintf(\"First name doesn't match for seed %d (%d)\", testCase.seed, i),\n\t\t\t)\n\n\t\t\tuassert.Equal(\n\t\t\t\tt,\n\t\t\t\tc.lastName,\n\t\t\t\tf.LastName(),\n\t\t\t\tufmt.Sprintf(\"Last name doesn't match for seed %d (%d)\", testCase.seed, i),\n\t\t\t)\n\n\t\t\tuassert.Equal(\n\t\t\t\tt,\n\t\t\t\tc.fullName,\n\t\t\t\tf.FullName(),\n\t\t\t\tufmt.Sprintf(\"Full name doesn't match for seed %d (%d)\", testCase.seed, i),\n\t\t\t)\n\n\t\t\tuassert.Equal(\n\t\t\t\tt,\n\t\t\t\tc.age,\n\t\t\t\tf.Age(0),\n\t\t\t\tufmt.Sprintf(\"Age doesn't match for seed %d (%d)\", testCase.seed, i),\n\t\t\t)\n\n\t\t\tuassert.Equal(\n\t\t\t\tt,\n\t\t\t\tc.city,\n\t\t\t\tf.City(),\n\t\t\t\tufmt.Sprintf(\"City doesn't match for seed %d (%d)\", testCase.seed, i),\n\t\t\t)\n\n\t\t\tuassert.Equal(\n\t\t\t\tt,\n\t\t\t\tc.country,\n\t\t\t\tf.Country(),\n\t\t\t\tufmt.Sprintf(\"Country doesn't match for seed %d (%d)\", testCase.seed, i),\n\t\t\t)\n\n\t\t\tuassert.Equal(\n\t\t\t\tt,\n\t\t\t\tc.address,\n\t\t\t\tf.Address(),\n\t\t\t\tufmt.Sprintf(\"Address doesn't match for seed %d (%d)\", testCase.seed, i),\n\t\t\t)\n\n\t\t\tuassert.Equal(\n\t\t\t\tt,\n\t\t\t\tc.lorem,\n\t\t\t\tf.Lorem(42),\n\t\t\t\tufmt.Sprintf(\"Lorem doesn't match for seed %d (%d)\", testCase.seed, i),\n\t\t\t)\n\n\t\t\tuassert.Equal(\n\t\t\t\tt,\n\t\t\t\tc.loremSentence,\n\t\t\t\tf.LoremSentence(),\n\t\t\t\tufmt.Sprintf(\"Lorem sentence doesn't match for seed %d (%d)\", testCase.seed, i),\n\t\t\t)\n\n\t\t\tuassert.Equal(\n\t\t\t\tt,\n\t\t\t\tc.loremParagraph,\n\t\t\t\tf.LoremParagraph(),\n\t\t\t\tufmt.Sprintf(\"Lorem paragraph doesn't match for seed %d (%d)\", testCase.seed, i),\n\t\t\t)\n\n\t\t\tuassert.Equal(\n\t\t\t\tt,\n\t\t\t\tc.date,\n\t\t\t\tf.Date(),\n\t\t\t\tufmt.Sprintf(\"Date doesn't match for seed %d (%d)\", testCase.seed, i),\n\t\t\t)\n\n\t\t\tuassert.Equal(\n\t\t\t\tt,\n\t\t\t\tc.time,\n\t\t\t\tf.Time(),\n\t\t\t\tufmt.Sprintf(\"Time doesn't match for seed %d (%d)\", testCase.seed, i),\n\t\t\t)\n\n\t\t\tuassert.Equal(\n\t\t\t\tt,\n\t\t\t\tc.dateTime,\n\t\t\t\tf.DateTime(),\n\t\t\t\tufmt.Sprintf(\"DateTime doesn't match for seed %d (%d)\", testCase.seed, i),\n\t\t\t)\n\n\t\t\tuassert.Equal(\n\t\t\t\tt,\n\t\t\t\tc.email,\n\t\t\t\tf.Email(),\n\t\t\t\tufmt.Sprintf(\"Email doesn't match for seed %d (%d)\", testCase.seed, i),\n\t\t\t)\n\n\t\t\tuassert.Equal(\n\t\t\t\tt,\n\t\t\t\tc.phone,\n\t\t\t\tf.Phone(),\n\t\t\t\tufmt.Sprintf(\"Phone doesn't match for seed %d (%d)\", testCase.seed, i),\n\t\t\t)\n\n\t\t\tuassert.Equal(\n\t\t\t\tt,\n\t\t\t\tc.stdAddress,\n\t\t\t\tf.StdAddress(),\n\t\t\t\tufmt.Sprintf(\"StdAddress doesn't match for seed %d (%d)\", testCase.seed, i),\n\t\t\t)\n\n\t\t\tuassert.Equal(\n\t\t\t\tt,\n\t\t\t\tc.username,\n\t\t\t\tf.Username(),\n\t\t\t\tufmt.Sprintf(\"Username doesn't match for seed %d (%d)\", testCase.seed, i),\n\t\t\t)\n\t\t}\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/faker\"\ngno = \"0.9\"\n"},{"name":"golden_cases_test.gno","body":"package faker\n\n// To generate these test cases, run the script hosted here: https://gist.github.com/aeddi/9a089bf02e8e4a015f38e3948337189d\nvar goldenTestCases = []goldenTestCase{\n\t{\n\t\tseed: 42,\n\t\titerations: []goldenValues{\n\t\t\t{\n\t\t\t\tfirstName:      \"Nicholas\",\n\t\t\t\tlastName:       \"Jones\",\n\t\t\t\tfullName:       \"Matthew Ramirez\",\n\t\t\t\tage:            10,\n\t\t\t\tcity:           \"Oakland\",\n\t\t\t\tcountry:        \"Greece\",\n\t\t\t\taddress:        \"3413 Church St, Baltimore, Taiwan\",\n\t\t\t\tlorem:          \"aperiam quisquam culpa esse do quis incididunt sed laboriosam et veritatis ex occaecat dolor dolor explicabo ullam et magnam nesciunt nostrud quasi aliquam at dolor sed aliquam minima dolor quia quia corporis proident ullamco lorem enim aut sed rem eos totam exercitation\",\n\t\t\t\tloremSentence:  \"Non aspernatur magna eiusmod pariatur exercitation sit neque consectetur et ea minim dolores.\",\n\t\t\t\tloremParagraph: \"Ab do quis beatae sit consequuntur tempora. Enim nemo incidunt consectetur ad sint suscipit irure doloremque. Pariatur tempor ad minima ullam cillum cillum ipsum.\",\n\t\t\t\tdate:           \"2018-06-03\",\n\t\t\t\ttime:           \"08:03:00\",\n\t\t\t\tdateTime:       \"2013-06-01 03:18:06\",\n\t\t\t\temail:          \"sharon.collins@test.com\",\n\t\t\t\tphone:          \"(198) 641-1385\",\n\t\t\t\tstdAddress:     \"g1km2qq5885nxld9m9plt0x22u3eecy70gzhw3f9\",\n\t\t\t\tusername:       \"charlesrivera056\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tfirstName:      \"Sarah\",\n\t\t\t\tlastName:       \"Lewis\",\n\t\t\t\tfullName:       \"George Turner\",\n\t\t\t\tage:            22,\n\t\t\t\tcity:           \"Boston\",\n\t\t\t\tcountry:        \"Thailand\",\n\t\t\t\taddress:        \"4204 Pine St, Fort Worth, Malaysia\",\n\t\t\t\tlorem:          \"sunt quisquam eos enim veniam ullam et ipsum consectetur quia accusamus totam sint explicabo et ipsum fugiat magnam sit ab quae in quaerat nisi accusantium sit laboriosam quia ipsum quaerat sit non consequat deserunt voluptas beatae ipsa aperiam ex inventore quia eos\",\n\t\t\t\tloremSentence:  \"Excepteur modi quia laudantium et fugit quis rem quia aute vero.\",\n\t\t\t\tloremParagraph: \"Irure quasi dolore minima quaerat enim ullamco aliqua aliquam explicabo nulla. Illo qui dolor totam mollit dolore veritatis tempor vero mollit magna. Laborum non deserunt sunt cupidatat dolores odit sunt numquam eaque deserunt.\",\n\t\t\t\tdate:           \"1976-04-04\",\n\t\t\t\ttime:           \"08:52:55\",\n\t\t\t\tdateTime:       \"2008-12-15 04:05:55\",\n\t\t\t\temail:          \"lisa.rivera@test.com\",\n\t\t\t\tphone:          \"(645) 598-9929\",\n\t\t\t\tstdAddress:     \"g1nphhzdwk07qewm6zlhwpgucn89uxtmqqz7e7ke\",\n\t\t\t\tusername:       \"robertleemartin940\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tfirstName:      \"Stephen\",\n\t\t\t\tlastName:       \"Lee\",\n\t\t\t\tfullName:       \"Susan Hall\",\n\t\t\t\tage:            26,\n\t\t\t\tcity:           \"San Francisco\",\n\t\t\t\tcountry:        \"Thailand\",\n\t\t\t\taddress:        \"7912 Washington Ave, Columbus, Saudi Arabia\",\n\t\t\t\tlorem:          \"ipsum accusantium dicta eius quia dolore eaque reprehenderit porro suscipit inventore cillum cupidatat dolore eius anim ratione accusantium et velit laboriosam dolore excepteur sed ab eiusmod inventore magnam ullam dolore eius non excepteur quis explicabo accusamus dolor suscipit sed nostrum quisquam illo\",\n\t\t\t\tloremSentence:  \"Corporis minim id laudantium sint non occaecat sit porro ut nostrud fugiat.\",\n\t\t\t\tloremParagraph: \"Voluptate totam mollit voluptate fugiat et excepteur numquam dolor proident mollit sed. Proident proident qui qui et dolores ipsam. Enim amet quae incidunt ipsam incididunt quasi quasi corporis accusamus consequat vitae et.\",\n\t\t\t\tdate:           \"1990-10-09\",\n\t\t\t\ttime:           \"08:20:56\",\n\t\t\t\tdateTime:       \"2010-10-06 16:03:34\",\n\t\t\t\temail:          \"sandra.campbell@sample.net\",\n\t\t\t\tphone:          \"(394) 964-9105\",\n\t\t\t\tstdAddress:     \"g1lmwtmeqdethnt3kenlf6327wkp3f9fz7tgsag8\",\n\t\t\t\tusername:       \"inciduntjonathan959\",\n\t\t\t},\n\t\t},\n\t}, {\n\t\tseed: 404,\n\t\titerations: []goldenValues{\n\t\t\t{\n\t\t\t\tfirstName:      \"Lisa\",\n\t\t\t\tlastName:       \"Johnson\",\n\t\t\t\tfullName:       \"Steven Young\",\n\t\t\t\tage:            57,\n\t\t\t\tcity:           \"Denver\",\n\t\t\t\tcountry:        \"Egypt\",\n\t\t\t\taddress:        \"2842 Market St, New Orleans, Sweden\",\n\t\t\t\tlorem:          \"suscipit veritatis in et est sunt nulla sit laudantium minim labore eaque aperiam ipsum quaerat magna sint mollit amet proident commodo magnam cillum modi proident nostrum enim cupidatat excepteur fugiat tempor fugiat lorem aliquam ullamco incididunt dolorem occaecat aliquam adipisci amet dolor\",\n\t\t\t\tloremSentence:  \"Quae sed ullamco reprehenderit aute mollit et deserunt beatae do.\",\n\t\t\t\tloremParagraph: \"Veritatis minim nisi mollit quia veniam explicabo. Aute dicta aliquip corporis adipiscing accusantium ea consectetur labore ipsa minima porro ut quaerat. Nesciunt est consequat explicabo totam consectetur anim. Occaecat numquam dolorem accusantium qui culpa velit nulla incidunt accusantium. Magnam excepteur accusamus voluptatem dolore. Elit ad qui modi cupidatat.\",\n\t\t\t\tdate:           \"1992-03-04\",\n\t\t\t\ttime:           \"06:18:38\",\n\t\t\t\tdateTime:       \"2015-07-14 06:46:34\",\n\t\t\t\temail:          \"nicholas.thompson@fake.io\",\n\t\t\t\tphone:          \"(529) 158-9162\",\n\t\t\t\tstdAddress:     \"g1gnkpd5hlnscj2fcl83rl87rk70denmpj4jp5kk\",\n\t\t\t\tusername:       \"jonesporroquia121\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tfirstName:      \"Michael\",\n\t\t\t\tlastName:       \"Ramirez\",\n\t\t\t\tfullName:       \"Mary Moore\",\n\t\t\t\tage:            5,\n\t\t\t\tcity:           \"Austin\",\n\t\t\t\tcountry:        \"Australia\",\n\t\t\t\taddress:        \"8445 Market St, Detroit, Switzerland\",\n\t\t\t\tlorem:          \"quis aspernatur fugit incidunt et deserunt consectetur culpa eaque laudantium ex minim inventore accusantium enim porro porro at ad laboriosam deserunt consectetur quis et in ratione dolores officia vitae pariatur eos ex cupidatat sit aliquip est ad amet minim illo do ratione\",\n\t\t\t\tloremSentence:  \"Voluptatem aperiam voluptatem accusantium ut.\",\n\t\t\t\tloremParagraph: \"Veniam tempor veniam nemo sequi cillum doloremque consequuntur. Est nostrud aspernatur aute ipsum nostrud. Sit nulla duis ad aute ullam numquam dolorem eiusmod ipsam veniam. Magni sunt amet ea consequat ullamco magna eius irure aspernatur.\",\n\t\t\t\tdate:           \"2015-10-15\",\n\t\t\t\ttime:           \"22:51:05\",\n\t\t\t\tdateTime:       \"2000-11-15 08:54:31\",\n\t\t\t\temail:          \"deborah.gonzalez@example.com\",\n\t\t\t\tphone:          \"(729) 653-7778\",\n\t\t\t\tstdAddress:     \"g1fedr3nyrs59235q9h5c4gxp5cvzwska736rt9s\",\n\t\t\t\tusername:       \"laboriosamruth713\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tfirstName:      \"Lisa\",\n\t\t\t\tlastName:       \"Williams\",\n\t\t\t\tfullName:       \"Sarah Taylor\",\n\t\t\t\tage:            41,\n\t\t\t\tcity:           \"Oakland\",\n\t\t\t\tcountry:        \"Singapore\",\n\t\t\t\taddress:        \"4180 First St, Omaha, Switzerland\",\n\t\t\t\tlorem:          \"voluptate aut ea ut quaerat nemo eos modi deserunt quis cupidatat sequi qui nemo illo dolore ipsam quaerat veniam suscipit minim dolor eius quasi numquam explicabo amet nemo doloremque illo reprehenderit aliquam ea modi consequat minima voluptate ab sed deserunt neque corporis\",\n\t\t\t\tloremSentence:  \"Labore consequat odit accusamus quis labore exercitationem odit enim quisquam voluptatem architecto.\",\n\t\t\t\tloremParagraph: \"Amet consequat neque ratione nostrud. Quasi veniam inventore exercitationem dolore aliquam veniam doloremque enim. Eos enim nostrum sed sit quae ipsa magni labore adipisci. Veniam sed eaque exercitation et sed aliqua ut irure exercitation qui anim adipisci aliquam.\",\n\t\t\t\tdate:           \"2017-05-27\",\n\t\t\t\ttime:           \"20:36:57\",\n\t\t\t\tdateTime:       \"2014-07-01 01:53:12\",\n\t\t\t\temail:          \"donna.gomez@sample.net\",\n\t\t\t\tphone:          \"(250) 473-8838\",\n\t\t\t\tstdAddress:     \"g1c3rzldvfn53ddkeakvqn2k3fq99jtyv3uhkf0a\",\n\t\t\t\tusername:       \"kevinnancycruz246\",\n\t\t\t},\n\t\t},\n\t}, {\n\t\tseed: 1337,\n\t\titerations: []goldenValues{\n\t\t\t{\n\t\t\t\tfirstName:      \"Sarah\",\n\t\t\t\tlastName:       \"Davis\",\n\t\t\t\tfullName:       \"Dorothy Reyes\",\n\t\t\t\tage:            68,\n\t\t\t\tcity:           \"Columbus\",\n\t\t\t\tcountry:        \"Thailand\",\n\t\t\t\taddress:        \"3639 Second Ave, Boston, Indonesia\",\n\t\t\t\tlorem:          \"voluptas fugit anim consequat do sunt esse mollit doloremque sit magna ab sequi nostrud ut porro fugiat laudantium laboriosam voluptatem ipsum tempora ex neque ex aut nostrud et sint ut qui odit ullam quis magnam labore accusantium quia nostrud occaecat quia beatae\",\n\t\t\t\tloremSentence:  \"Veniam aut consequat dicta totam ratione dolore neque nisi nisi dicta qui amet mollit.\",\n\t\t\t\tloremParagraph: \"Dolore magna minima eos aliquip quisquam voluptas. Exercitation minima enim nemo eos mollit commodo. Accusamus ea modi aliqua ut fugiat qui anim illo cillum corporis do eaque. Cupidatat ipsa laboris officia et. Neque veritatis dolor officia quis ut magni tempor quia ipsum adipisci consectetur quis. Dolorem culpa excepteur accusamus explicabo numquam.\",\n\t\t\t\tdate:           \"1974-06-08\",\n\t\t\t\ttime:           \"08:14:32\",\n\t\t\t\tdateTime:       \"2010-04-07 22:25:29\",\n\t\t\t\temail:          \"susan.reyes@demo.org\",\n\t\t\t\tphone:          \"(417) 813-7768\",\n\t\t\t\tstdAddress:     \"g1pcfhsz7menqxg9rzka4j276nhdffjvzjllhfka\",\n\t\t\t\tusername:       \"eiusmoddavis015\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tfirstName:      \"Ruth\",\n\t\t\t\tlastName:       \"Young\",\n\t\t\t\tfullName:       \"Sharon Nguyen\",\n\t\t\t\tage:            61,\n\t\t\t\tcity:           \"Seattle\",\n\t\t\t\tcountry:        \"Greece\",\n\t\t\t\taddress:        \"3013 Oak Ave, Fresno, Mexico\",\n\t\t\t\tlorem:          \"aliqua eius in do esse doloremque accusamus est in quis dolor quisquam laboris dolores quasi illo consequuntur commodo commodo tempora vitae quaerat dolore corporis dolore labore cillum duis lorem dolore nesciunt duis vero minima totam voluptatem aperiam architecto ipsam excepteur vero esse\",\n\t\t\t\tloremSentence:  \"Labore duis labore pariatur aut vero.\",\n\t\t\t\tloremParagraph: \"Adipiscing tempor occaecat reprehenderit magni eius veritatis ut et minim. Vitae beatae nemo quaerat ut. Incididunt ipsum incididunt laboris ad minima deserunt minim quis enim at minim veniam.\",\n\t\t\t\tdate:           \"1979-05-09\",\n\t\t\t\ttime:           \"22:45:41\",\n\t\t\t\tdateTime:       \"2013-07-21 23:52:22\",\n\t\t\t\temail:          \"michelle.parker@demo.org\",\n\t\t\t\tphone:          \"(675) 814-3202\",\n\t\t\t\tstdAddress:     \"g1vvm4wumwhtwjc6jn66jrkkx0wk88w20gw0hnsh\",\n\t\t\t\tusername:       \"barbaradonnaodit581\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tfirstName:      \"Stephen\",\n\t\t\t\tlastName:       \"Garcia\",\n\t\t\t\tfullName:       \"Sarah Walker\",\n\t\t\t\tage:            32,\n\t\t\t\tcity:           \"Boston\",\n\t\t\t\tcountry:        \"Netherlands\",\n\t\t\t\taddress:        \"7697 Pine St, Tucson, Philippines\",\n\t\t\t\tlorem:          \"aperiam corporis velit consequat eius nostrud velit adipiscing fugiat at sunt consectetur inventore quaerat rem nostrum voluptatem odit eiusmod laborum occaecat tempor sed nostrum vero veritatis amet id sit culpa eiusmod lorem do et ipsa sit magna ad aperiam ipsam velit sunt\",\n\t\t\t\tloremSentence:  \"Ipsum ipsum sed quia incidunt.\",\n\t\t\t\tloremParagraph: \"Do exercitation excepteur et laboriosam aspernatur exercitationem dolores. Ipsum lorem et in culpa amet laborum ad consectetur accusamus id. Consequat ullamco qui deserunt elit nisi culpa quia proident exercitationem laborum duis. Dolor aliquam dolorem esse exercitation sunt ipsum sequi. Ipsum ex modi laudantium labore.\",\n\t\t\t\tdate:           \"2000-10-27\",\n\t\t\t\ttime:           \"06:44:19\",\n\t\t\t\tdateTime:       \"2020-01-26 20:36:17\",\n\t\t\t\temail:          \"jonathan.perez@example.com\",\n\t\t\t\tphone:          \"(816) 174-1755\",\n\t\t\t\tstdAddress:     \"g1gnj5tq9tm5vfqqjnue9re3uw5vlc962gp7huu6\",\n\t\t\t\tusername:       \"georgeallenillo577\",\n\t\t\t},\n\t\t},\n\t},\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"Nq+tZwVPtqOtBgY7n6AEYV1+9PBlWRhpRiFd6aDi1mFEtSNSLTjwFFpYgG0As1anMuRy448DYmKvfVAVCVsd2Q=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"flow","path":"gno.land/p/demo/flow","files":[{"name":"LICENSE","body":"https://github.com/mxk/go-flowrate/blob/master/LICENSE\nBSD 3-Clause \"New\" or \"Revised\" License\n\nCopyright (c) 2014 The Go-FlowRate 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\n   notice, this list of conditions and the following disclaimer.\n\n * Redistributions in binary form must reproduce the above copyright\n   notice, this list of conditions and the following disclaimer in the\n   documentation and/or other materials provided with the\n   distribution.\n\n * Neither the name of the go-flowrate project nor the names of its\n   contributors may be used to endorse or promote products derived\n   from this 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":"Data Flow Rate Control\n======================\n\nTo download and install this package run:\n\ngo get github.com/mxk/go-flowrate/flowrate\n\nThe documentation is available at:\n\nhttp://godoc.org/github.com/mxk/go-flowrate/flowrate\n"},{"name":"flow.gno","body":"//\n// Written by Maxim Khitrov (November 2012)\n//\n// XXX modified to disable blocking, time.Sleep().\n\n// Package flow provides the tools for monitoring and limiting the flow rate\n// of an arbitrary data stream.\npackage flow\n\nimport (\n\t\"math\"\n\t// \"sync\"\n\t\"time\"\n)\n\n// Monitor monitors and limits the transfer rate of a data stream.\ntype Monitor struct {\n\t// mu      sync.Mutex    // Mutex guarding access to all internal fields\n\tactive  bool          // Flag indicating an active transfer\n\tstart   time.Duration // Transfer start time (clock() value)\n\tbytes   int64         // Total number of bytes transferred\n\tsamples int64         // Total number of samples taken\n\n\trSample float64 // Most recent transfer rate sample (bytes per second)\n\trEMA    float64 // Exponential moving average of rSample\n\trPeak   float64 // Peak transfer rate (max of all rSamples)\n\trWindow float64 // rEMA window (seconds)\n\n\tsBytes int64         // Number of bytes transferred since sLast\n\tsLast  time.Duration // Most recent sample time (stop time when inactive)\n\tsRate  time.Duration // Sampling rate\n\n\ttBytes int64         // Number of bytes expected in the current transfer\n\ttLast  time.Duration // Time of the most recent transfer of at least 1 byte\n}\n\n// New creates a new flow control monitor. Instantaneous transfer rate is\n// measured and updated for each sampleRate interval. windowSize determines the\n// weight of each sample in the exponential moving average (EMA) calculation.\n// The exact formulas are:\n//\n//\tsampleTime = currentTime - prevSampleTime\n//\tsampleRate = byteCount / sampleTime\n//\tweight     = 1 - exp(-sampleTime/windowSize)\n//\tnewRate    = weight*sampleRate + (1-weight)*oldRate\n//\n// The default values for sampleRate and windowSize (if \u003c= 0) are 100ms and 1s,\n// respectively.\nfunc New(sampleRate, windowSize time.Duration) *Monitor {\n\tif sampleRate = clockRound(sampleRate); sampleRate \u003c= 0 {\n\t\tsampleRate = 5 * clockRate\n\t}\n\tif windowSize \u003c= 0 {\n\t\twindowSize = 1 * time.Second\n\t}\n\tnow := clock()\n\treturn \u0026Monitor{\n\t\tactive:  true,\n\t\tstart:   now,\n\t\trWindow: windowSize.Seconds(),\n\t\tsLast:   now,\n\t\tsRate:   sampleRate,\n\t\ttLast:   now,\n\t}\n}\n\n// Update records the transfer of n bytes and returns n. It should be called\n// after each Read/Write operation, even if n is 0.\nfunc (m *Monitor) Update(n int) int {\n\t// m.mu.Lock()\n\tm.update(n)\n\t// m.mu.Unlock()\n\treturn n\n}\n\n// Hack to set the current rEMA.\nfunc (m *Monitor) SetREMA(rEMA float64) {\n\t// m.mu.Lock()\n\tm.rEMA = rEMA\n\tm.samples++\n\t// m.mu.Unlock()\n}\n\n// IO is a convenience method intended to wrap io.Reader and io.Writer method\n// execution. It calls m.Update(n) and then returns (n, err) unmodified.\nfunc (m *Monitor) IO(n int, err error) (int, error) {\n\treturn m.Update(n), err\n}\n\n// Done marks the transfer as finished and prevents any further updates or\n// limiting. Instantaneous and current transfer rates drop to 0. Update, IO, and\n// Limit methods become NOOPs. It returns the total number of bytes transferred.\nfunc (m *Monitor) Done() int64 {\n\t// m.mu.Lock()\n\tif now := m.update(0); m.sBytes \u003e 0 {\n\t\tm.reset(now)\n\t}\n\tm.active = false\n\tm.tLast = 0\n\tn := m.bytes\n\t// m.mu.Unlock()\n\treturn n\n}\n\n// timeRemLimit is the maximum Status.TimeRem value.\nconst timeRemLimit = 999*time.Hour + 59*time.Minute + 59*time.Second\n\n// Status represents the current Monitor status. All transfer rates are in bytes\n// per second rounded to the nearest byte.\ntype Status struct {\n\tActive   bool          // Flag indicating an active transfer\n\tStart    time.Time     // Transfer start time\n\tDuration time.Duration // Time period covered by the statistics\n\tIdle     time.Duration // Time since the last transfer of at least 1 byte\n\tBytes    int64         // Total number of bytes transferred\n\tSamples  int64         // Total number of samples taken\n\tInstRate int64         // Instantaneous transfer rate\n\tCurRate  int64         // Current transfer rate (EMA of InstRate)\n\tAvgRate  int64         // Average transfer rate (Bytes / Duration)\n\tPeakRate int64         // Maximum instantaneous transfer rate\n\tBytesRem int64         // Number of bytes remaining in the transfer\n\tTimeRem  time.Duration // Estimated time to completion\n\tProgress Percent       // Overall transfer progress\n}\n\nfunc (s Status) String() string {\n\treturn \"STATUS{}\"\n}\n\n// Status returns current transfer status information. The returned value\n// becomes static after a call to Done.\nfunc (m *Monitor) Status() Status {\n\t// m.mu.Lock()\n\tnow := m.update(0)\n\ts := Status{\n\t\tActive:   m.active,\n\t\tStart:    clockToTime(m.start),\n\t\tDuration: m.sLast - m.start,\n\t\tIdle:     now - m.tLast,\n\t\tBytes:    m.bytes,\n\t\tSamples:  m.samples,\n\t\tPeakRate: round(m.rPeak),\n\t\tBytesRem: m.tBytes - m.bytes,\n\t\tProgress: percentOf(float64(m.bytes), float64(m.tBytes)),\n\t}\n\tif s.BytesRem \u003c 0 {\n\t\ts.BytesRem = 0\n\t}\n\tif s.Duration \u003e 0 {\n\t\trAvg := float64(s.Bytes) / s.Duration.Seconds()\n\t\ts.AvgRate = round(rAvg)\n\t\tif s.Active {\n\t\t\ts.InstRate = round(m.rSample)\n\t\t\ts.CurRate = round(m.rEMA)\n\t\t\tif s.BytesRem \u003e 0 {\n\t\t\t\tif tRate := 0.8*m.rEMA + 0.2*rAvg; tRate \u003e 0 {\n\t\t\t\t\tns := float64(s.BytesRem) / tRate * 1e9\n\t\t\t\t\tif ns \u003e float64(timeRemLimit) {\n\t\t\t\t\t\tns = float64(timeRemLimit)\n\t\t\t\t\t}\n\t\t\t\t\ts.TimeRem = clockRound(time.Duration(ns))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t// m.mu.Unlock()\n\treturn s\n}\n\n// Limit restricts the instantaneous (per-sample) data flow to rate bytes per\n// second. It returns the maximum number of bytes (0 \u003c= n \u003c= want) that may be\n// transferred immediately without exceeding the limit. If block == true, the\n// call blocks until n \u003e 0. want is returned unmodified if want \u003c 1, rate \u003c 1,\n// or the transfer is inactive (after a call to Done).\n//\n// At least one byte is always allowed to be transferred in any given sampling\n// period. Thus, if the sampling rate is 100ms, the lowest achievable flow rate\n// is 10 bytes per second.\n//\n// For usage examples, see the implementation of Reader and Writer in io.go.\nfunc (m *Monitor) Limit(want int, rate int64, block bool) (n int) {\n\tif block {\n\t\tpanic(\"blocking not yet supported\")\n\t}\n\tif want \u003c 1 || rate \u003c 1 {\n\t\treturn want\n\t}\n\t// m.mu.Lock()\n\n\t// Determine the maximum number of bytes that can be sent in one sample\n\tlimit := round(float64(rate) * m.sRate.Seconds())\n\tif limit \u003c= 0 {\n\t\tlimit = 1\n\t}\n\n\t_ = m.update(0)\n\t/* XXX\n\t// If block == true, wait until m.sBytes \u003c limit\n\tif now := m.update(0); block {\n\t\tfor m.sBytes \u003e= limit \u0026\u0026 m.active {\n\t\t\tnow = m.waitNextSample(now)\n\t\t}\n\t}\n\t*/\n\n\t// Make limit \u003c= want (unlimited if the transfer is no longer active)\n\tif limit -= m.sBytes; limit \u003e int64(want) || !m.active {\n\t\tlimit = int64(want)\n\t}\n\t// m.mu.Unlock()\n\n\tif limit \u003c 0 {\n\t\tlimit = 0\n\t}\n\treturn int(limit)\n}\n\n// SetTransferSize specifies the total size of the data transfer, which allows\n// the Monitor to calculate the overall progress and time to completion.\nfunc (m *Monitor) SetTransferSize(bytes int64) {\n\tif bytes \u003c 0 {\n\t\tbytes = 0\n\t}\n\t// m.mu.Lock()\n\tm.tBytes = bytes\n\t// m.mu.Unlock()\n}\n\n// update accumulates the transferred byte count for the current sample until\n// clock() - m.sLast \u003e= m.sRate. The monitor status is updated once the current\n// sample is done.\nfunc (m *Monitor) update(n int) (now time.Duration) {\n\tif !m.active {\n\t\treturn\n\t}\n\tif now = clock(); n \u003e 0 {\n\t\tm.tLast = now\n\t}\n\tm.sBytes += int64(n)\n\tif sTime := now - m.sLast; sTime \u003e= m.sRate {\n\t\tt := sTime.Seconds()\n\t\tif m.rSample = float64(m.sBytes) / t; m.rSample \u003e m.rPeak {\n\t\t\tm.rPeak = m.rSample\n\t\t}\n\n\t\t// Exponential moving average using a method similar to *nix load\n\t\t// average calculation. Longer sampling periods carry greater weight.\n\t\tif m.samples \u003e 0 {\n\t\t\tw := math.Exp(-t / m.rWindow)\n\t\t\tm.rEMA = m.rSample + w*(m.rEMA-m.rSample)\n\t\t} else {\n\t\t\tm.rEMA = m.rSample\n\t\t}\n\t\tm.reset(now)\n\t}\n\treturn\n}\n\n// reset clears the current sample state in preparation for the next sample.\nfunc (m *Monitor) reset(sampleTime time.Duration) {\n\tm.bytes += m.sBytes\n\tm.samples++\n\tm.sBytes = 0\n\tm.sLast = sampleTime\n}\n\n/*\n// waitNextSample sleeps for the remainder of the current sample. The lock is\n// released and reacquired during the actual sleep period, so it's possible for\n// the transfer to be inactive when this method returns.\nfunc (m *Monitor) waitNextSample(now time.Duration) time.Duration {\n\tconst minWait = 5 * time.Millisecond\n\tcurrent := m.sLast\n\n\t// sleep until the last sample time changes (ideally, just one iteration)\n\tfor m.sLast == current \u0026\u0026 m.active {\n\t\td := current + m.sRate - now\n\t\t// m.mu.Unlock()\n\t\tif d \u003c minWait {\n\t\t\td = minWait\n\t\t}\n\t\ttime.Sleep(d)\n\t\t// m.mu.Lock()\n\t\tnow = m.update(0)\n\t}\n\treturn now\n}\n*/\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/flow\"\ngno = \"0.9\"\n"},{"name":"io.gno","body":"//\n// Written by Maxim Khitrov (November 2012)\n//\n\npackage flow\n\nimport (\n\t\"errors\"\n\t\"io\"\n)\n\n// ErrLimit is returned by the Writer when a non-blocking write is short due to\n// the transfer rate limit.\nvar ErrLimit = errors.New(\"flowrate: flow rate limit exceeded\")\n\n// Limiter is implemented by the Reader and Writer to provide a consistent\n// interface for monitoring and controlling data transfer.\ntype Limiter interface {\n\tDone() int64\n\tStatus() Status\n\tSetTransferSize(bytes int64)\n\tSetLimit(new int64) (old int64)\n\tSetBlocking(new bool) (old bool)\n}\n\n// Reader implements io.ReadCloser with a restriction on the rate of data\n// transfer.\ntype Reader struct {\n\tio.Reader // Data source\n\t*Monitor  // Flow control monitor\n\n\tlimit int64 // Rate limit in bytes per second (unlimited when \u003c= 0)\n\tblock bool  // What to do when no new bytes can be read due to the limit\n}\n\n// NewReader restricts all Read operations on r to limit bytes per second.\nfunc NewReader(r io.Reader, limit int64) *Reader {\n\treturn \u0026Reader{r, New(0, 0), limit, false} // XXX default false\n}\n\n// Read reads up to len(p) bytes into p without exceeding the current transfer\n// rate limit. It returns (0, nil) immediately if r is non-blocking and no new\n// bytes can be read at this time.\nfunc (r *Reader) Read(p []byte) (n int, err error) {\n\tp = p[:r.Limit(len(p), r.limit, r.block)]\n\tif len(p) \u003e 0 {\n\t\tn, err = r.IO(r.Reader.Read(p))\n\t}\n\treturn\n}\n\n// SetLimit changes the transfer rate limit to new bytes per second and returns\n// the previous setting.\nfunc (r *Reader) SetLimit(new int64) (old int64) {\n\told, r.limit = r.limit, new\n\treturn\n}\n\n// SetBlocking changes the blocking behavior and returns the previous setting. A\n// Read call on a non-blocking reader returns immediately if no additional bytes\n// may be read at this time due to the rate limit.\nfunc (r *Reader) SetBlocking(new bool) (old bool) {\n\tif new == true {\n\t\tpanic(\"blocking not yet supported\")\n\t}\n\told, r.block = r.block, new\n\treturn\n}\n\n// Close closes the underlying reader if it implements the io.Closer interface.\nfunc (r *Reader) Close() error {\n\tdefer r.Done()\n\tif c, ok := r.Reader.(io.Closer); ok {\n\t\treturn c.Close()\n\t}\n\treturn nil\n}\n\n// Writer implements io.WriteCloser with a restriction on the rate of data\n// transfer.\ntype Writer struct {\n\tio.Writer // Data destination\n\t*Monitor  // Flow control monitor\n\n\tlimit int64 // Rate limit in bytes per second (unlimited when \u003c= 0)\n\tblock bool  // What to do when no new bytes can be written due to the limit\n}\n\n// NewWriter restricts all Write operations on w to limit bytes per second. The\n// transfer rate and the default blocking behavior (true) can be changed\n// directly on the returned *Writer.\nfunc NewWriter(w io.Writer, limit int64) *Writer {\n\treturn \u0026Writer{w, New(0, 0), limit, false} // XXX default false\n}\n\n// Write writes len(p) bytes from p to the underlying data stream without\n// exceeding the current transfer rate limit. It returns (n, ErrLimit) if w is\n// non-blocking and no additional bytes can be written at this time.\nfunc (w *Writer) Write(p []byte) (n int, err error) {\n\tvar c int\n\tfor len(p) \u003e 0 \u0026\u0026 err == nil {\n\t\ts := p[:w.Limit(len(p), w.limit, w.block)]\n\t\tif len(s) \u003e 0 {\n\t\t\tc, err = w.IO(w.Writer.Write(s))\n\t\t} else {\n\t\t\treturn n, ErrLimit\n\t\t}\n\t\tp = p[c:]\n\t\tn += c\n\t}\n\treturn\n}\n\n// SetLimit changes the transfer rate limit to new bytes per second and returns\n// the previous setting.\nfunc (w *Writer) SetLimit(new int64) (old int64) {\n\told, w.limit = w.limit, new\n\treturn\n}\n\n// SetBlocking changes the blocking behavior and returns the previous setting. A\n// Write call on a non-blocking writer returns as soon as no additional bytes\n// may be written at this time due to the rate limit.\nfunc (w *Writer) SetBlocking(new bool) (old bool) {\n\told, w.block = w.block, new\n\treturn\n}\n\n// Close closes the underlying writer if it implements the io.Closer interface.\nfunc (w *Writer) Close() error {\n\tdefer w.Done()\n\tif c, ok := w.Writer.(io.Closer); ok {\n\t\treturn c.Close()\n\t}\n\treturn nil\n}\n"},{"name":"io_test.gno","body":"//\n// Written by Maxim Khitrov (November 2012)\n//\n\npackage flow\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\t\"time\"\n\n\tios_test \"os\"\n)\n\n// XXX ugh, I can't even sleep milliseconds.\n// XXX\n\nconst (\n\t_50ms  = 50 * time.Millisecond\n\t_100ms = 100 * time.Millisecond\n\t_200ms = 200 * time.Millisecond\n\t_300ms = 300 * time.Millisecond\n\t_400ms = 400 * time.Millisecond\n\t_500ms = 500 * time.Millisecond\n)\n\nfunc nextStatus(m *Monitor) Status {\n\tsamples := m.samples\n\tfor i := 0; i \u003c 30; i++ {\n\t\tif s := m.Status(); s.Samples != samples {\n\t\t\treturn s\n\t\t}\n\t\tios_test.Sleep(5 * time.Millisecond)\n\t}\n\treturn m.Status()\n}\n\nfunc TestReader(t *testing.T) {\n\tin := make([]byte, 100)\n\tfor i := range in {\n\t\tin[i] = byte(i)\n\t}\n\tb := make([]byte, 100)\n\tr := NewReader(bytes.NewReader(in), 100)\n\tstart := time.Now()\n\n\t// Make sure r implements Limiter\n\t_ = Limiter(r)\n\n\t// 1st read of 10 bytes is performed immediately\n\tif n, err := r.Read(b); n != 10 {\n\t\tt.Fatalf(\"r.Read(b) expected 10 (\u003cnil\u003e); got %v\", n)\n\t} else if err != nil {\n\t\tt.Fatalf(\"r.Read(b) expected 10 (\u003cnil\u003e); got %v (%v)\", n, err.Error())\n\t} else if rt := time.Since(start); rt \u003e _50ms {\n\t\tt.Fatalf(\"r.Read(b) took too long (%v)\", rt.String())\n\t}\n\n\t// No new Reads allowed in the current sample\n\tr.SetBlocking(false)\n\tif n, err := r.Read(b); n != 0 {\n\t\tt.Fatalf(\"r.Read(b) expected 0 (\u003cnil\u003e); got %v\", n)\n\t} else if err != nil {\n\t\tt.Fatalf(\"r.Read(b) expected 0 (\u003cnil\u003e); got %v (%v)\", n, err.Error())\n\t} else if rt := time.Since(start); rt \u003e _50ms {\n\t\tt.Fatalf(\"r.Read(b) took too long (%v)\", rt.String())\n\t}\n\n\tstatus := [6]Status{0: r.Status()} // No samples in the first status\n\n\t// 2nd read of 10 bytes blocks until the next sample\n\t// r.SetBlocking(true)\n\tios_test.Sleep(100 * time.Millisecond)\n\tif n, err := r.Read(b[10:]); n != 10 {\n\t\tt.Fatalf(\"r.Read(b[10:]) expected 10 (\u003cnil\u003e); got %v\", n)\n\t} else if err != nil {\n\t\tt.Fatalf(\"r.Read(b[10:]) expected 10 (\u003cnil\u003e); got %v (%v)\", n, err.Error())\n\t} else if rt := time.Since(start); rt \u003c _100ms {\n\t\tt.Fatalf(\"r.Read(b[10:]) returned ahead of time (%v)\", rt.String())\n\t}\n\n\tstatus[1] = r.Status()            // 1st sample\n\tstatus[2] = nextStatus(r.Monitor) // 2nd sample\n\tstatus[3] = nextStatus(r.Monitor) // No activity for the 3rd sample\n\n\tif n := r.Done(); n != 20 {\n\t\tt.Fatalf(\"r.Done() expected 20; got %v\", n)\n\t}\n\n\tstatus[4] = r.Status()\n\tstatus[5] = nextStatus(r.Monitor) // Timeout\n\tstart = status[0].Start\n\n\t// Active, Start, Duration, Idle, Bytes, Samples, InstRate, CurRate, AvgRate, PeakRate, BytesRem, TimeRem, Progress\n\twant := []Status{\n\t\t{true, start, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},\n\t\t{true, start, _100ms, 0, 10, 1, 100, 100, 100, 100, 0, 0, 0},\n\t\t{true, start, _200ms, _100ms, 20, 2, 100, 100, 100, 100, 0, 0, 0},\n\t\t{true, start, _300ms, _200ms, 20, 3, 0, 90, 67, 100, 0, 0, 0},\n\t\t{false, start, _300ms, 0, 20, 3, 0, 0, 67, 100, 0, 0, 0},\n\t\t{false, start, _300ms, 0, 20, 3, 0, 0, 67, 100, 0, 0, 0},\n\t}\n\tfor i, s := range status {\n\t\t// XXX s := s\n\t\tif !statusesAreEqual(\u0026s, \u0026want[i]) {\n\t\t\tt.Errorf(\"r.Status(%v)\\nexpected: %v\\ngot     : %v\", i, want[i].String(), s.String())\n\t\t}\n\t}\n\tif !bytes.Equal(b[:20], in[:20]) {\n\t\tt.Errorf(\"r.Read() input doesn't match output\")\n\t}\n}\n\n// XXX blocking writer test doesn't work.\nfunc _TestWriter(t *testing.T) {\n\tb := make([]byte, 100)\n\tfor i := range b {\n\t\tb[i] = byte(i)\n\t}\n\tw := NewWriter(\u0026bytes.Buffer{}, 200)\n\tstart := time.Now()\n\n\t// Make sure w implements Limiter\n\t_ = Limiter(w)\n\n\t// Non-blocking 20-byte write for the first sample returns ErrLimit\n\tw.SetBlocking(false)\n\tif n, err := w.Write(b); n != 20 || err != ErrLimit {\n\t\tt.Fatalf(\"w.Write(b) expected 20 (ErrLimit); got %v (%v)\", n, err.Error())\n\t} else if rt := time.Since(start); rt \u003e _50ms {\n\t\tt.Fatalf(\"w.Write(b) took too long (%v)\", rt)\n\t}\n\n\t// Blocking 80-byte write\n\t// w.SetBlocking(true)\n\t// XXX This test doesn't work, because w.Write calls w.Limit(block=false),\n\t// XXX and it returns ErrLimit after 20. What we want is to keep waiting until 80 is returned,\n\t// XXX but blocking isn't supported. Sleeping 800 shouldn't be sufficient either (its a burst).\n\t// XXX This limits the usage of Limiter and m.Limit().\n\tios_test.Sleep(800 * time.Millisecond)\n\tif n, err := w.Write(b[20:]); n \u003c 80 {\n\t} else if n != 80 || err != nil {\n\t\tt.Fatalf(\"w.Write(b[20:]) expected 80 (\u003cnil\u003e); got %v (%v)\", n, err.Error())\n\t} else if rt := time.Since(start); rt \u003c _300ms {\n\t\t// Explanation for `rt \u003c _300ms` (as opposed to `\u003c _400ms`)\n\t\t//\n\t\t//                 |\u003c-- start        |        |\n\t\t// epochs: -----0ms|---100ms|---200ms|---300ms|---400ms\n\t\t// sends:        20|20      |20      |20      |20#\n\t\t//\n\t\t// NOTE: The '#' symbol can thus happen before 400ms is up.\n\t\t// Thus, we can only panic if rt \u003c _300ms.\n\t\tt.Fatalf(\"w.Write(b[20:]) returned ahead of time (%v)\", rt.String())\n\t}\n\n\tw.SetTransferSize(100)\n\tstatus := []Status{w.Status(), nextStatus(w.Monitor)}\n\tstart = status[0].Start\n\n\t// Active, Start, Duration, Idle, Bytes, Samples, InstRate, CurRate, AvgRate, PeakRate, BytesRem, TimeRem, Progress\n\twant := []Status{\n\t\t{true, start, _400ms, 0, 80, 4, 200, 200, 200, 200, 20, _100ms, 80000},\n\t\t{true, start, _500ms, _100ms, 100, 5, 200, 200, 200, 200, 0, 0, 100000},\n\t}\n\tfor i, s := range status {\n\t\t// XXX s := s\n\t\tif !statusesAreEqual(\u0026s, \u0026want[i]) {\n\t\t\tt.Errorf(\"w.Status(%v)\\nexpected: %v\\ngot     : %v\\n\", i, want[i].String(), s.String())\n\t\t}\n\t}\n\tif !bytes.Equal(b, w.Writer.(*bytes.Buffer).Bytes()) {\n\t\tt.Errorf(\"w.Write() input doesn't match output\")\n\t}\n}\n\nconst (\n\tmaxDeviationForDuration       = 50 * time.Millisecond\n\tmaxDeviationForRate     int64 = 50\n)\n\n// statusesAreEqual returns true if s1 is equal to s2. Equality here means\n// general equality of fields except for the duration and rates, which can\n// drift due to unpredictable delays (e.g. thread wakes up 25ms after\n// `time.Sleep` has ended).\nfunc statusesAreEqual(s1 *Status, s2 *Status) bool {\n\tif s1.Active == s2.Active \u0026\u0026\n\t\ts1.Start == s2.Start \u0026\u0026\n\t\tdurationsAreEqual(s1.Duration, s2.Duration, maxDeviationForDuration) \u0026\u0026\n\t\ts1.Idle == s2.Idle \u0026\u0026\n\t\ts1.Bytes == s2.Bytes \u0026\u0026\n\t\ts1.Samples == s2.Samples \u0026\u0026\n\t\tratesAreEqual(s1.InstRate, s2.InstRate, maxDeviationForRate) \u0026\u0026\n\t\tratesAreEqual(s1.CurRate, s2.CurRate, maxDeviationForRate) \u0026\u0026\n\t\tratesAreEqual(s1.AvgRate, s2.AvgRate, maxDeviationForRate) \u0026\u0026\n\t\tratesAreEqual(s1.PeakRate, s2.PeakRate, maxDeviationForRate) \u0026\u0026\n\t\ts1.BytesRem == s2.BytesRem \u0026\u0026\n\t\tdurationsAreEqual(s1.TimeRem, s2.TimeRem, maxDeviationForDuration) \u0026\u0026\n\t\ts1.Progress == s2.Progress {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc durationsAreEqual(d1 time.Duration, d2 time.Duration, maxDeviation time.Duration) bool {\n\treturn d2-d1 \u003c= maxDeviation\n}\n\nfunc ratesAreEqual(r1 int64, r2 int64, maxDeviation int64) bool {\n\tsub := r1 - r2\n\tif sub \u003c 0 {\n\t\tsub = -sub\n\t}\n\tif sub \u003c= maxDeviation {\n\t\treturn true\n\t}\n\treturn false\n}\n"},{"name":"util.gno","body":"//\n// Written by Maxim Khitrov (November 2012)\n//\n\npackage flow\n\nimport (\n\t\"math\"\n\t\"strconv\"\n\t\"time\"\n)\n\n// clockRate is the resolution and precision of clock().\nconst clockRate = 20 * time.Millisecond\n\n// czero is the process start time rounded down to the nearest clockRate\n// increment.\nvar czero = time.Now().Round(clockRate)\n\n// clock returns a low resolution timestamp relative to the process start time.\nfunc clock() time.Duration {\n\treturn time.Now().Round(clockRate).Sub(czero)\n}\n\n// clockToTime converts a clock() timestamp to an absolute time.Time value.\nfunc clockToTime(c time.Duration) time.Time {\n\treturn czero.Add(c)\n}\n\n// clockRound returns d rounded to the nearest clockRate increment.\nfunc clockRound(d time.Duration) time.Duration {\n\treturn (d + clockRate\u003e\u003e1) / clockRate * clockRate\n}\n\n// round returns x rounded to the nearest int64 (non-negative values only).\nfunc round(x float64) int64 {\n\tif _, frac := math.Modf(x); frac \u003e= 0.5 {\n\t\treturn int64(math.Ceil(x))\n\t}\n\treturn int64(math.Floor(x))\n}\n\n// Percent represents a percentage in increments of 1/1000th of a percent.\ntype Percent uint32\n\n// percentOf calculates what percent of the total is x.\nfunc percentOf(x, total float64) Percent {\n\tif x \u003c 0 || total \u003c= 0 {\n\t\treturn 0\n\t} else if p := round(x / total * 1e5); p \u003c= math.MaxUint32 {\n\t\treturn Percent(p)\n\t}\n\treturn Percent(math.MaxUint32)\n}\n\nfunc (p Percent) Float() float64 {\n\treturn float64(p) * 1e-3\n}\n\nfunc (p Percent) String() string {\n\tvar buf [12]byte\n\tb := strconv.AppendUint(buf[:0], uint64(p)/1000, 10)\n\tn := len(b)\n\tb = strconv.AppendUint(b, 1000+uint64(p)%1000, 10)\n\tb[n] = '.'\n\treturn string(append(b, '%'))\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"kXQrJyT6ycaGyNcGbKAbNww3VtQXpri1ytcbxOZpyjVnjs9OwFs3+2kjli1ONDeuNHak7yb9zHhTQMs1qer7vw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"int32","path":"gno.land/p/demo/math_eval/int32","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/math_eval/int32\"\ngno = \"0.9\"\n"},{"name":"int32.gno","body":"// eval/int32 is a evaluator for int32 expressions.\n// This code is heavily forked from https://github.com/dengsgo/math-engine\n// which is licensed under Apache 2.0:\n// https://raw.githubusercontent.com/dengsgo/math-engine/298e2b57b7e7350d0f67bd036916efd5709abe25/LICENSE\npackage int32\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nconst (\n\tIdentifier = iota\n\tNumber     // numbers\n\tOperator   // +, -, *, /, etc.\n\tVariable   // x, y, z, etc. (one-letter only)\n)\n\ntype expression interface {\n\tString() string\n}\n\ntype expressionRaw struct {\n\texpression string\n\tType       int\n\tFlag       int\n\tOffset     int\n}\n\ntype parser struct {\n\tInput  string\n\tch     byte\n\toffset int\n\terr    error\n}\n\ntype expressionNumber struct {\n\tVal int\n\tStr string\n}\n\ntype expressionVariable struct {\n\tVal int\n\tStr string\n}\n\ntype expressionOperation struct {\n\tOp string\n\tLhs,\n\tRhs expression\n}\n\ntype ast struct {\n\trawexpressions    []*expressionRaw\n\tsource            string\n\tcurrentexpression *expressionRaw\n\tcurrentIndex      int\n\tdepth             int\n\terr               error\n}\n\n// Parse takes an expression string, e.g. \"1+2\" and returns\n// a parsed expression. If there is an error it will return.\nfunc Parse(s string) (ar expression, err error) {\n\ttoks, err := lexer(s)\n\tif err != nil {\n\t\treturn\n\t}\n\tast, err := newAST(toks, s)\n\tif err != nil {\n\t\treturn\n\t}\n\tar, err = ast.parseExpression()\n\treturn\n}\n\n// Eval takes a parsed expression and a map of variables (or nil). The parsed\n// expression is evaluated using any variables and returns the\n// resulting int and/or error.\nfunc Eval(expr expression, variables map[string]int) (res int, err error) {\n\tif err != nil {\n\t\treturn\n\t}\n\tvar l, r int\n\tswitch expr.(type) {\n\tcase expressionVariable:\n\t\tast := expr.(expressionVariable)\n\t\tok := false\n\t\tif variables != nil {\n\t\t\tres, ok = variables[ast.Str]\n\t\t}\n\t\tif !ok {\n\t\t\terr = ufmt.Errorf(\"variable '%s' not found\", ast.Str)\n\t\t}\n\t\treturn\n\tcase expressionOperation:\n\t\tast := expr.(expressionOperation)\n\t\tl, err = Eval(ast.Lhs, variables)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\tr, err = Eval(ast.Rhs, variables)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\tswitch ast.Op {\n\t\tcase \"+\":\n\t\t\tres = l + r\n\t\tcase \"-\":\n\t\t\tres = l - r\n\t\tcase \"*\":\n\t\t\tres = l * r\n\t\tcase \"/\":\n\t\t\tif r == 0 {\n\t\t\t\terr = ufmt.Errorf(\"violation of arithmetic specification: a division by zero in Eval: [%d/%d]\", l, r)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tres = l / r\n\t\tcase \"%\":\n\t\t\tif r == 0 {\n\t\t\t\tres = 0\n\t\t\t} else {\n\t\t\t\tres = l % r\n\t\t\t}\n\t\tcase \"^\":\n\t\t\tres = l ^ r\n\t\tcase \"\u003e\u003e\":\n\t\t\tres = l \u003e\u003e r\n\t\tcase \"\u003c\u003c\":\n\t\t\tres = l \u003c\u003c r\n\t\tcase \"\u003e\":\n\t\t\tif l \u003e r {\n\t\t\t\tres = 1\n\t\t\t} else {\n\t\t\t\tres = 0\n\t\t\t}\n\t\tcase \"\u003c\":\n\t\t\tif l \u003c r {\n\t\t\t\tres = 1\n\t\t\t} else {\n\t\t\t\tres = 0\n\t\t\t}\n\t\tcase \"\u0026\":\n\t\t\tres = l \u0026 r\n\t\tcase \"|\":\n\t\t\tres = l | r\n\t\tdefault:\n\n\t\t}\n\tcase expressionNumber:\n\t\tres = expr.(expressionNumber).Val\n\t}\n\n\treturn\n}\n\nfunc expressionError(s string, pos int) string {\n\tr := strings.Repeat(\"-\", len(s)) + \"\\n\"\n\ts += \"\\n\"\n\tfor i := 0; i \u003c pos; i++ {\n\t\ts += \" \"\n\t}\n\ts += \"^\\n\"\n\treturn r + s + r\n}\n\nfunc (n expressionVariable) String() string {\n\treturn ufmt.Sprintf(\n\t\t\"expressionVariable: %s\",\n\t\tn.Str,\n\t)\n}\n\nfunc (n expressionNumber) String() string {\n\treturn ufmt.Sprintf(\n\t\t\"expressionNumber: %s\",\n\t\tn.Str,\n\t)\n}\n\nfunc (b expressionOperation) String() string {\n\treturn ufmt.Sprintf(\n\t\t\"expressionOperation: (%s %s %s)\",\n\t\tb.Op,\n\t\tb.Lhs.String(),\n\t\tb.Rhs.String(),\n\t)\n}\n\nfunc newAST(toks []*expressionRaw, s string) (*ast, error) {\n\ta := \u0026ast{\n\t\trawexpressions: toks,\n\t\tsource:         s,\n\t}\n\tif a.rawexpressions == nil || len(a.rawexpressions) == 0 {\n\t\treturn a, errors.New(\"empty token\")\n\t} else {\n\t\ta.currentIndex = 0\n\t\ta.currentexpression = a.rawexpressions[0]\n\t}\n\treturn a, nil\n}\n\nfunc (a *ast) parseExpression() (expression, error) {\n\ta.depth++ // called depth\n\tlhs := a.parsePrimary()\n\tr := a.parseBinOpRHS(0, lhs)\n\ta.depth--\n\tif a.depth == 0 \u0026\u0026 a.currentIndex != len(a.rawexpressions) \u0026\u0026 a.err == nil {\n\t\treturn r, ufmt.Errorf(\"bad expression, reaching the end or missing the operator\\n%s\",\n\t\t\texpressionError(a.source, a.currentexpression.Offset))\n\t}\n\treturn r, nil\n}\n\nfunc (a *ast) getNextexpressionRaw() *expressionRaw {\n\ta.currentIndex++\n\tif a.currentIndex \u003c len(a.rawexpressions) {\n\t\ta.currentexpression = a.rawexpressions[a.currentIndex]\n\t\treturn a.currentexpression\n\t}\n\treturn nil\n}\n\nfunc (a *ast) getTokPrecedence() int {\n\tswitch a.currentexpression.expression {\n\tcase \"/\", \"%\", \"*\":\n\t\treturn 100\n\tcase \"\u003c\u003c\", \"\u003e\u003e\":\n\t\treturn 80\n\tcase \"+\", \"-\":\n\t\treturn 75\n\tcase \"\u003c\", \"\u003e\":\n\t\treturn 70\n\tcase \"\u0026\":\n\t\treturn 60\n\tcase \"^\":\n\t\treturn 50\n\tcase \"|\":\n\t\treturn 40\n\t}\n\treturn -1\n}\n\nfunc (a *ast) parseNumber() expressionNumber {\n\tf64, err := strconv.Atoi(a.currentexpression.expression)\n\tif err != nil {\n\t\ta.err = ufmt.Errorf(\"%v\\nwant '(' or '0-9' but get '%s'\\n%s\",\n\t\t\terr.Error(),\n\t\t\ta.currentexpression.expression,\n\t\t\texpressionError(a.source, a.currentexpression.Offset))\n\t\treturn expressionNumber{}\n\t}\n\tn := expressionNumber{\n\t\tVal: f64,\n\t\tStr: a.currentexpression.expression,\n\t}\n\ta.getNextexpressionRaw()\n\treturn n\n}\n\nfunc (a *ast) parseVariable() expressionVariable {\n\tn := expressionVariable{\n\t\tVal: 0,\n\t\tStr: a.currentexpression.expression,\n\t}\n\ta.getNextexpressionRaw()\n\treturn n\n}\n\nfunc (a *ast) parsePrimary() expression {\n\tswitch a.currentexpression.Type {\n\tcase Variable:\n\t\treturn a.parseVariable()\n\tcase Number:\n\t\treturn a.parseNumber()\n\tcase Operator:\n\t\tif a.currentexpression.expression == \"(\" {\n\t\t\tt := a.getNextexpressionRaw()\n\t\t\tif t == nil {\n\t\t\t\ta.err = ufmt.Errorf(\"want '(' or '0-9' but get EOF\\n%s\",\n\t\t\t\t\texpressionError(a.source, a.currentexpression.Offset))\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\te, _ := a.parseExpression()\n\t\t\tif e == nil {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tif a.currentexpression.expression != \")\" {\n\t\t\t\ta.err = ufmt.Errorf(\"want ')' but get %s\\n%s\",\n\t\t\t\t\ta.currentexpression.expression,\n\t\t\t\t\texpressionError(a.source, a.currentexpression.Offset))\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\ta.getNextexpressionRaw()\n\t\t\treturn e\n\t\t} else if a.currentexpression.expression == \"-\" {\n\t\t\tif a.getNextexpressionRaw() == nil {\n\t\t\t\ta.err = ufmt.Errorf(\"want '0-9' but get '-'\\n%s\",\n\t\t\t\t\texpressionError(a.source, a.currentexpression.Offset))\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tbin := expressionOperation{\n\t\t\t\tOp:  \"-\",\n\t\t\t\tLhs: expressionNumber{},\n\t\t\t\tRhs: a.parsePrimary(),\n\t\t\t}\n\t\t\treturn bin\n\t\t} else {\n\t\t\treturn a.parseNumber()\n\t\t}\n\tdefault:\n\t\treturn nil\n\t}\n}\n\nfunc (a *ast) parseBinOpRHS(execPrec int, lhs expression) expression {\n\tfor {\n\t\ttokPrec := a.getTokPrecedence()\n\t\tif tokPrec \u003c execPrec {\n\t\t\treturn lhs\n\t\t}\n\t\tbinOp := a.currentexpression.expression\n\t\tif a.getNextexpressionRaw() == nil {\n\t\t\ta.err = ufmt.Errorf(\"want '(' or '0-9' but get EOF\\n%s\",\n\t\t\t\texpressionError(a.source, a.currentexpression.Offset))\n\t\t\treturn nil\n\t\t}\n\t\trhs := a.parsePrimary()\n\t\tif rhs == nil {\n\t\t\treturn nil\n\t\t}\n\t\tnextPrec := a.getTokPrecedence()\n\t\tif tokPrec \u003c nextPrec {\n\t\t\trhs = a.parseBinOpRHS(tokPrec+1, rhs)\n\t\t\tif rhs == nil {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tlhs = expressionOperation{\n\t\t\tOp:  binOp,\n\t\t\tLhs: lhs,\n\t\t\tRhs: rhs,\n\t\t}\n\t}\n}\n\nfunc lexer(s string) ([]*expressionRaw, error) {\n\tp := \u0026parser{\n\t\tInput: s,\n\t\terr:   nil,\n\t\tch:    s[0],\n\t}\n\ttoks := p.parse()\n\tif p.err != nil {\n\t\treturn nil, p.err\n\t}\n\treturn toks, nil\n}\n\nfunc (p *parser) parse() []*expressionRaw {\n\ttoks := make([]*expressionRaw, 0)\n\tfor {\n\t\ttok := p.nextTok()\n\t\tif tok == nil {\n\t\t\tbreak\n\t\t}\n\t\ttoks = append(toks, tok)\n\t}\n\treturn toks\n}\n\nfunc (p *parser) nextTok() *expressionRaw {\n\tif p.offset \u003e= len(p.Input) || p.err != nil {\n\t\treturn nil\n\t}\n\tvar err error\n\tfor p.isWhitespace(p.ch) \u0026\u0026 err == nil {\n\t\terr = p.nextCh()\n\t}\n\tstart := p.offset\n\tvar tok *expressionRaw\n\tswitch p.ch {\n\tcase\n\t\t'(',\n\t\t')',\n\t\t'+',\n\t\t'-',\n\t\t'*',\n\t\t'/',\n\t\t'^',\n\t\t'\u0026',\n\t\t'|',\n\t\t'%':\n\t\ttok = \u0026expressionRaw{\n\t\t\texpression: string(p.ch),\n\t\t\tType:       Operator,\n\t\t}\n\t\ttok.Offset = start\n\t\terr = p.nextCh()\n\tcase '\u003e', '\u003c':\n\t\ttokS := string(p.ch)\n\t\tbb, be := p.nextChPeek()\n\t\tif be == nil \u0026\u0026 string(bb) == tokS {\n\t\t\ttokS += string(p.ch)\n\t\t}\n\t\ttok = \u0026expressionRaw{\n\t\t\texpression: tokS,\n\t\t\tType:       Operator,\n\t\t}\n\t\ttok.Offset = start\n\t\tif len(tokS) \u003e 1 {\n\t\t\tp.nextCh()\n\t\t}\n\t\terr = p.nextCh()\n\tcase\n\t\t'0',\n\t\t'1',\n\t\t'2',\n\t\t'3',\n\t\t'4',\n\t\t'5',\n\t\t'6',\n\t\t'7',\n\t\t'8',\n\t\t'9':\n\t\tfor p.isDigitNum(p.ch) \u0026\u0026 p.nextCh() == nil {\n\t\t\tif (p.ch == '-' || p.ch == '+') \u0026\u0026 p.Input[p.offset-1] != 'e' {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\ttok = \u0026expressionRaw{\n\t\t\texpression: strings.ReplaceAll(p.Input[start:p.offset], \"_\", \"\"),\n\t\t\tType:       Number,\n\t\t}\n\t\ttok.Offset = start\n\tdefault:\n\t\tif p.isChar(p.ch) {\n\t\t\ttok = \u0026expressionRaw{\n\t\t\t\texpression: string(p.ch),\n\t\t\t\tType:       Variable,\n\t\t\t}\n\t\t\ttok.Offset = start\n\t\t\terr = p.nextCh()\n\t\t} else if p.ch != ' ' {\n\t\t\tp.err = ufmt.Errorf(\"symbol error: unknown '%v', pos [%v:]\\n%s\",\n\t\t\t\tstring(p.ch),\n\t\t\t\tstart,\n\t\t\t\texpressionError(p.Input, start))\n\t\t}\n\t}\n\treturn tok\n}\n\nfunc (p *parser) nextChPeek() (byte, error) {\n\toffset := p.offset + 1\n\tif offset \u003c len(p.Input) {\n\t\treturn p.Input[offset], nil\n\t}\n\treturn byte(0), errors.New(\"no byte\")\n}\n\nfunc (p *parser) nextCh() error {\n\tp.offset++\n\tif p.offset \u003c len(p.Input) {\n\t\tp.ch = p.Input[p.offset]\n\t\treturn nil\n\t}\n\treturn errors.New(\"EOF\")\n}\n\nfunc (p *parser) isWhitespace(c byte) bool {\n\treturn c == ' ' ||\n\t\tc == '\\t' ||\n\t\tc == '\\n' ||\n\t\tc == '\\v' ||\n\t\tc == '\\f' ||\n\t\tc == '\\r'\n}\n\nfunc (p *parser) isDigitNum(c byte) bool {\n\treturn '0' \u003c= c \u0026\u0026 c \u003c= '9' || c == '.' || c == '_' || c == 'e' || c == '-' || c == '+'\n}\n\nfunc (p *parser) isChar(c byte) bool {\n\treturn 'a' \u003c= c \u0026\u0026 c \u003c= 'z' || 'A' \u003c= c \u0026\u0026 c \u003c= 'Z'\n}\n\nfunc (p *parser) isWordChar(c byte) bool {\n\treturn p.isChar(c) || '0' \u003c= c \u0026\u0026 c \u003c= '9'\n}\n"},{"name":"int32_test.gno","body":"package int32\n\nimport \"testing\"\n\nfunc TestOne(t *testing.T) {\n\tttt := []struct {\n\t\texp string\n\t\tres int\n\t}{\n\t\t{\"1\", 1},\n\t\t{\"--1\", 1},\n\t\t{\"1+2\", 3},\n\t\t{\"-1+2\", 1},\n\t\t{\"-(1+2)\", -3},\n\t\t{\"-(1+2)*5\", -15},\n\t\t{\"-(1+2)*5/3\", -5},\n\t\t{\"1+(-(1+2)*5/3)\", -4},\n\t\t{\"3^4\", 3 ^ 4},\n\t\t{\"8%2\", 8 % 2},\n\t\t{\"8%3\", 8 % 3},\n\t\t{\"8|3\", 8 | 3},\n\t\t{\"10%2\", 0},\n\t\t{\"(4    + 3)/2-1+11*15\", (4+3)/2 - 1 + 11*15},\n\t\t{\n\t\t\t\"(30099\u003e\u003e10^30099\u003e\u003e11)%5*((30099\u003e\u003e14\u00263^30099\u003e\u003e15\u00261)+1)*30099%99 + ((3 + (30099 \u003e\u003e 14 \u0026 3) - (30099 \u003e\u003e 16 \u0026 1)) / 3 * 30099 % 99 \u0026 64)\",\n\t\t\t(30099\u003e\u003e10^30099\u003e\u003e11)%5*((30099\u003e\u003e14\u00263^30099\u003e\u003e15\u00261)+1)*30099%99 + ((3 + (30099 \u003e\u003e 14 \u0026 3) - (30099 \u003e\u003e 16 \u0026 1)) / 3 * 30099 % 99 \u0026 64),\n\t\t},\n\t\t{\n\t\t\t\"(1023850\u003e\u003e10^1023850\u003e\u003e11)%5*((1023850\u003e\u003e14\u00263^1023850\u003e\u003e15\u00261)+1)*1023850%99 + ((3 + (1023850 \u003e\u003e 14 \u0026 3) - (1023850 \u003e\u003e 16 \u0026 1)) / 3 * 1023850 % 99 \u0026 64)\",\n\t\t\t(1023850\u003e\u003e10^1023850\u003e\u003e11)%5*((1023850\u003e\u003e14\u00263^1023850\u003e\u003e15\u00261)+1)*1023850%99 + ((3 + (1023850 \u003e\u003e 14 \u0026 3) - (1023850 \u003e\u003e 16 \u0026 1)) / 3 * 1023850 % 99 \u0026 64),\n\t\t},\n\t\t{\"((0000+1)*0000)\", 0},\n\t}\n\tfor _, tc := range ttt {\n\t\tt.Run(tc.exp, func(t *testing.T) {\n\t\t\texp, err := Parse(tc.exp)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"%s:\\n%s\", tc.exp, err.Error())\n\t\t\t} else {\n\t\t\t\tres, errEval := Eval(exp, nil)\n\t\t\t\tif errEval != nil {\n\t\t\t\t\tt.Errorf(\"eval error: %s\", errEval.Error())\n\t\t\t\t} else if res != tc.res {\n\t\t\t\t\tt.Errorf(\"%s:\\nexpected %d, got %d\", tc.exp, tc.res, res)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestVariables(t *testing.T) {\n\tfn := func(x, y int) int {\n\t\treturn 1 + ((x*3+1)*(x*2))\u003e\u003ey + 1\n\t}\n\texpr := \"1 + ((x*3+1)*(x*2))\u003e\u003ey + 1\"\n\texp, err := Parse(expr)\n\tif err != nil {\n\t\tt.Errorf(\"could not parse: %s\", err.Error())\n\t}\n\tvariables := make(map[string]int)\n\tfor i := 0; i \u003c 10; i++ {\n\t\tvariables[\"x\"] = i\n\t\tvariables[\"y\"] = 2\n\t\tres, errEval := Eval(exp, variables)\n\t\tif errEval != nil {\n\t\t\tt.Errorf(\"could not evaluate: %s\", err.Error())\n\t\t}\n\t\texpected := fn(variables[\"x\"], variables[\"y\"])\n\t\tif res != expected {\n\t\t\tt.Errorf(\"expected: %d, actual: %d\", expected, res)\n\t\t}\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"OdLBG0KjPxZN1f+FlNLVL2B6CvFBArhB4HhHW1CBTwxJ6fGOdayoVOcG3LDG2R4yFPd4PDEqMxi31JUMTtZ85g=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"memeland","path":"gno.land/p/demo/memeland","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/memeland\"\ngno = \"0.9\"\n"},{"name":"memeland.gno","body":"package memeland\n\nimport (\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/ownable/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n)\n\nconst (\n\tDATE_CREATED = \"DATE_CREATED\"\n\tUPVOTES      = \"UPVOTES\"\n)\n\ntype Post struct {\n\tID            string\n\tData          string\n\tAuthor        address\n\tTimestamp     time.Time\n\tUpvoteTracker *avl.Tree // address \u003e struct{}{}\n}\n\ntype Memeland struct {\n\t*ownable.Ownable\n\tPosts       []*Post\n\tMemeCounter seqid.ID\n}\n\nfunc NewMemeland(owner address) *Memeland {\n\treturn \u0026Memeland{\n\t\tOwnable: ownable.NewWithAddress(owner),\n\t\tPosts:   make([]*Post, 0),\n\t}\n}\n\n// PostMeme adds a new post. rlm must be the caller's own captured cur\n// (asserted via rlm.IsCurrent()); the post's Author is set to\n// rlm.Previous().Address(), so an external realm cannot forge a post\n// attributed to a different address.\nfunc (m *Memeland) PostMeme(_ int, rlm realm, data string, timestamp int64) string {\n\tif data == \"\" || timestamp \u003c= 0 {\n\t\tpanic(\"timestamp or data cannot be empty\")\n\t}\n\tif !rlm.IsCurrent() {\n\t\tpanic(ownable.ErrUnauthorized)\n\t}\n\n\tid := m.MemeCounter.Next().String()\n\n\tnewPost := \u0026Post{\n\t\tID:            id,\n\t\tData:          data,\n\t\tAuthor:        rlm.Previous().Address(),\n\t\tTimestamp:     time.Unix(timestamp, 0),\n\t\tUpvoteTracker: avl.NewTree(),\n\t}\n\n\tm.Posts = append(m.Posts, newPost)\n\treturn id\n}\n\n// Upvote records an upvote on a post. rlm must be the caller's own\n// captured cur; the voter is rlm.Previous().Address(). Closes the\n// vote-stuffing forgery: an external realm cannot upvote-as-victim.\nfunc (m *Memeland) Upvote(_ int, rlm realm, id string) string {\n\tif !rlm.IsCurrent() {\n\t\tpanic(ownable.ErrUnauthorized)\n\t}\n\tpost := m.getPost(id)\n\tif post == nil {\n\t\tpanic(\"post with specified ID does not exist\")\n\t}\n\n\tvoterKey := rlm.Previous().Address().String()\n\tif post.UpvoteTracker.Has(voterKey) {\n\t\tpanic(\"user has already upvoted this post\")\n\t}\n\tpost.UpvoteTracker.Set(voterKey, struct{}{})\n\n\treturn \"upvote successful\"\n}\n\n// GetPostsInRange returns a JSON string of posts within the given timestamp range, supporting pagination\nfunc (m *Memeland) GetPostsInRange(startTimestamp, endTimestamp int64, page, pageSize int, sortBy string) string {\n\tif len(m.Posts) == 0 {\n\t\treturn \"[]\"\n\t}\n\n\tif page \u003c 1 {\n\t\tpanic(\"page number cannot be less than 1\")\n\t}\n\n\t// No empty pages\n\tif pageSize \u003c 1 {\n\t\tpanic(\"page size cannot be less than 1\")\n\t}\n\n\t// No pages larger than 10\n\tif pageSize \u003e 10 {\n\t\tpanic(\"page size cannot be larger than 10\")\n\t}\n\n\t// Need to pass in a sort parameter\n\tif sortBy == \"\" {\n\t\tpanic(\"sort order cannot be empty\")\n\t}\n\n\tvar filteredPosts []*Post\n\n\tstart := time.Unix(startTimestamp, 0)\n\tend := time.Unix(endTimestamp, 0)\n\n\t// Filtering posts\n\tfor _, p := range m.Posts {\n\t\tif !p.Timestamp.Before(start) \u0026\u0026 !p.Timestamp.After(end) {\n\t\t\tfilteredPosts = append(filteredPosts, p)\n\t\t}\n\t}\n\n\tswitch sortBy {\n\t// Sort by upvote descending\n\tcase UPVOTES:\n\t\tdateSorter := PostSorter{\n\t\t\tPosts: filteredPosts,\n\t\t\tLessF: func(i, j int) bool {\n\t\t\t\treturn filteredPosts[i].UpvoteTracker.Size() \u003e filteredPosts[j].UpvoteTracker.Size()\n\t\t\t},\n\t\t}\n\t\tsort.Sort(dateSorter)\n\tcase DATE_CREATED:\n\t\t// Sort by timestamp, beginning with newest\n\t\tdateSorter := PostSorter{\n\t\t\tPosts: filteredPosts,\n\t\t\tLessF: func(i, j int) bool {\n\t\t\t\treturn filteredPosts[i].Timestamp.After(filteredPosts[j].Timestamp)\n\t\t\t},\n\t\t}\n\t\tsort.Sort(dateSorter)\n\tdefault:\n\t\tpanic(\"sort order can only be \\\"UPVOTES\\\" or \\\"DATE_CREATED\\\"\")\n\t}\n\n\t// Pagination\n\tstartIndex := (page - 1) * pageSize\n\tendIndex := startIndex + pageSize\n\n\t// If page does not contain any posts\n\tif startIndex \u003e= len(filteredPosts) {\n\t\treturn \"[]\"\n\t}\n\n\t// If page contains fewer posts than the page size\n\tif endIndex \u003e len(filteredPosts) {\n\t\tendIndex = len(filteredPosts)\n\t}\n\n\t// Return JSON representation of paginated and sorted posts\n\treturn PostsToJSONString(filteredPosts[startIndex:endIndex])\n}\n\n// RemovePost allows the owner to remove a post with a specific ID. rlm\n// must be the caller's own captured cur; rlm.Previous().Address() must\n// equal the owner.\nfunc (m *Memeland) RemovePost(_ int, rlm realm, id string) string {\n\tif id == \"\" {\n\t\tpanic(\"id cannot be empty\")\n\t}\n\n\tif !rlm.IsCurrent() {\n\t\tpanic(ownable.ErrUnauthorized)\n\t}\n\tif !m.OwnedBy(rlm.Previous().Address()) {\n\t\tpanic(ownable.ErrUnauthorized)\n\t}\n\n\tfor i, post := range m.Posts {\n\t\tif post.ID == id {\n\t\t\tm.Posts = append(m.Posts[:i], m.Posts[i+1:]...)\n\t\t\treturn id\n\t\t}\n\t}\n\n\tpanic(\"post with specified id does not exist\")\n}\n\n// PostsToJSONString converts a slice of Post structs into a JSON string\nfunc PostsToJSONString(posts []*Post) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"[\")\n\n\tfor i, post := range posts {\n\t\tif i \u003e 0 {\n\t\t\tsb.WriteString(\",\")\n\t\t}\n\n\t\tsb.WriteString(PostToJSONString(post))\n\t}\n\tsb.WriteString(\"]\")\n\n\treturn sb.String()\n}\n\n// PostToJSONString returns a Post formatted as a JSON string\nfunc PostToJSONString(post *Post) string {\n\tvar sb strings.Builder\n\n\tsb.WriteString(\"{\")\n\tsb.WriteString(`\"id\":\"` + post.ID + `\",`)\n\tsb.WriteString(`\"data\":\"` + escapeString(post.Data) + `\",`)\n\tsb.WriteString(`\"author\":\"` + escapeString(post.Author.String()) + `\",`)\n\tsb.WriteString(`\"timestamp\":\"` + strconv.Itoa(int(post.Timestamp.Unix())) + `\",`)\n\tsb.WriteString(`\"upvotes\":` + strconv.Itoa(post.UpvoteTracker.Size()))\n\tsb.WriteString(\"}\")\n\n\treturn sb.String()\n}\n\n// escapeString escapes quotes in a string for JSON compatibility.\nfunc escapeString(s string) string {\n\treturn strings.ReplaceAll(s, `\"`, `\\\"`)\n}\n\nfunc (m *Memeland) getPost(id string) *Post {\n\tfor _, p := range m.Posts {\n\t\tif p.ID == id {\n\t\t\treturn p\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// PostSorter is a flexible sorter for the *Post slice\ntype PostSorter struct {\n\tPosts []*Post\n\tLessF func(i, j int) bool\n}\n\nfunc (p PostSorter) Len() int {\n\treturn len(p.Posts)\n}\n\nfunc (p PostSorter) Swap(i, j int) {\n\tp.Posts[i], p.Posts[j] = p.Posts[j], p.Posts[i]\n}\n\nfunc (p PostSorter) Less(i, j int) bool {\n\treturn p.LessF(i, j)\n}\n"},{"name":"memeland_test.gno","body":"package memeland\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// postMemeAs / upvoteAs / removePostAs are tiny helpers that thread the\n// migrated (_ int, rlm realm) API: each sets the test realm to the\n// actor's user realm and invokes the call inside a crossing closure so\n// cur.Previous().Address() resolves to actor.\nfunc postMemeAs(cur realm, actor address, m *Memeland, data string, timestamp int64) string {\n\ttesting.SetRealm(testing.NewUserRealm(actor))\n\tvar id string\n\tfunc(cur realm) { id = m.PostMeme(0, cur, data, timestamp) }(cross(cur))\n\treturn id\n}\n\nfunc upvoteAs(cur realm, actor address, m *Memeland, id string) string {\n\ttesting.SetRealm(testing.NewUserRealm(actor))\n\tvar result string\n\tfunc(cur realm) { result = m.Upvote(0, cur, id) }(cross(cur))\n\treturn result\n}\n\nfunc removePostAs(cur realm, actor address, m *Memeland, id string) string {\n\ttesting.SetRealm(testing.NewUserRealm(actor))\n\tvar result string\n\tfunc(cur realm) { result = m.RemovePost(0, cur, id) }(cross(cur))\n\treturn result\n}\n\nfunc TestPostMeme(cur realm, t *testing.T) {\n\tadmin := testutils.TestAddress(\"admin\")\n\tm := NewMemeland(admin)\n\tid := postMemeAs(cur, admin, m, \"Test meme data\", time.Now().Unix())\n\tuassert.NotEqual(t, \"\", string(id), \"Expected valid ID, got empty string\")\n}\n\nfunc TestGetPostsInRangePagination(cur realm, t *testing.T) {\n\tadmin := testutils.TestAddress(\"admin\")\n\tm := NewMemeland(admin)\n\tnow := time.Now()\n\n\tnumOfPosts := 5\n\tvar memeData []string\n\tfor i := 1; i \u003c= numOfPosts; i++ {\n\t\t// Prepare meme data\n\t\tnextTime := now.Add(time.Duration(i) * time.Minute)\n\t\tdata := ufmt.Sprintf(\"Meme #%d\", i)\n\t\tmemeData = append(memeData, data)\n\n\t\tpostMemeAs(cur, admin, m, data, nextTime.Unix())\n\t}\n\n\t// Get timestamps\n\tbeforeEarliest := now.Add(-1 * time.Minute)\n\tafterLatest := now.Add(time.Duration(numOfPosts)*time.Minute + time.Minute)\n\n\ttestCases := []struct {\n\t\tpage               int\n\t\tpageSize           int\n\t\texpectedNumOfPosts int\n\t}{\n\t\t{page: 1, pageSize: 1, expectedNumOfPosts: 1},                       // one per page\n\t\t{page: 2, pageSize: 1, expectedNumOfPosts: 1},                       // one on second page\n\t\t{page: 1, pageSize: numOfPosts, expectedNumOfPosts: numOfPosts},     // all posts on single page\n\t\t{page: 12, pageSize: 1, expectedNumOfPosts: 0},                      // empty page\n\t\t{page: 1, pageSize: numOfPosts + 1, expectedNumOfPosts: numOfPosts}, // page with fewer posts than its size\n\t\t{page: 5, pageSize: numOfPosts / 5, expectedNumOfPosts: 1},          // evenly distribute posts per page\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(ufmt.Sprintf(\"Page%d_Size%d\", tc.page, tc.pageSize), func(t *testing.T) {\n\t\t\tresult := m.GetPostsInRange(beforeEarliest.Unix(), afterLatest.Unix(), tc.page, tc.pageSize, \"DATE_CREATED\")\n\n\t\t\t// Count posts by how many times id: shows up in JSON string\n\t\t\tpostCount := strings.Count(result, `\"id\":\"`)\n\t\t\tuassert.Equal(t, tc.expectedNumOfPosts, postCount)\n\t\t})\n\t}\n}\n\nfunc TestGetPostsInRangeByTimestamp(cur realm, t *testing.T) {\n\tadmin := testutils.TestAddress(\"admin\")\n\tm := NewMemeland(admin)\n\tnow := time.Now()\n\n\tnumOfPosts := 5\n\tvar memeData []string\n\tfor i := 1; i \u003c= numOfPosts; i++ {\n\t\t// Prepare meme data\n\t\tnextTime := now.Add(time.Duration(i) * time.Minute)\n\t\tdata := ufmt.Sprintf(\"Meme #%d\", i)\n\t\tmemeData = append(memeData, data)\n\n\t\tpostMemeAs(cur, admin, m, data, nextTime.Unix())\n\t}\n\n\t// Get timestamps\n\tbeforeEarliest := now.Add(-1 * time.Minute)\n\tafterLatest := now.Add(time.Duration(numOfPosts)*time.Minute + time.Minute)\n\n\t// Default sort is by addition order/timestamp\n\tjsonStr := m.GetPostsInRange(\n\t\tbeforeEarliest.Unix(), // start at earliest post\n\t\tafterLatest.Unix(),    // end at latest post\n\t\t1,                     // first page\n\t\tnumOfPosts,            // all memes on the page\n\t\t\"DATE_CREATED\",        // sort by newest first\n\t)\n\n\tuassert.NotEmpty(t, jsonStr, \"Expected non-empty JSON string, got empty string\")\n\n\t// Count the number of posts returned in the JSON string as a rudimentary check for correct pagination/filtering\n\tpostCount := strings.Count(jsonStr, `\"id\":\"`)\n\tuassert.Equal(t, uint64(m.MemeCounter), uint64(postCount))\n\n\t// Check if data is there\n\tfor _, expData := range memeData {\n\t\tcheck := strings.Contains(jsonStr, expData)\n\t\tuassert.True(t, check, ufmt.Sprintf(\"Expected %s in the JSON string, but counld't find it\", expData))\n\t}\n\n\t// Check if ordering is correct, sort by created date\n\tfor i := 0; i \u003c len(memeData)-2; i++ {\n\t\tcheck := strings.Index(jsonStr, memeData[i]) \u003e= strings.Index(jsonStr, memeData[i+1])\n\t\tuassert.True(t, check, ufmt.Sprintf(\"Expected %s to be before %s, but was at %d, and %d\", memeData[i], memeData[i+1], i, i+1))\n\t}\n}\n\nfunc TestGetPostsInRangeByUpvote(cur realm, t *testing.T) {\n\tadmin := testutils.TestAddress(\"admin\")\n\talice := testutils.TestAddress(\"alice\")\n\tm := NewMemeland(admin)\n\tnow := time.Now()\n\n\tmemeData1 := \"Meme #1\"\n\tmemeData2 := \"Meme #2\"\n\n\t// Create posts at specific times for testing\n\tid1 := postMemeAs(cur, admin, m, memeData1, now.Unix())\n\tid2 := postMemeAs(cur, admin, m, memeData2, now.Add(time.Minute).Unix())\n\n\tupvoteAs(cur, admin, m, id1)\n\tupvoteAs(cur, admin, m, id2)\n\tupvoteAs(cur, alice, m, id1)\n\n\t// Final upvote count:\n\t// Meme #1 - 2 upvote\n\t// Meme #2 - 1 upvotes\n\n\t// Get timestamps\n\tbeforeEarliest := now.Add(-time.Minute)\n\tafterLatest := now.Add(time.Hour)\n\n\t// Default sort is by addition order/timestamp\n\tjsonStr := m.GetPostsInRange(\n\t\tbeforeEarliest.Unix(), // start at earliest post\n\t\tafterLatest.Unix(),    // end at latest post\n\t\t1,                     // first page\n\t\t2,                     // all memes on the page\n\t\t\"UPVOTES\",             // sort by upvote\n\t)\n\n\tuassert.NotEmpty(t, jsonStr, \"Expected non-empty JSON string, got empty string\")\n\n\t// Count the number of posts returned in the JSON string as a rudimentary check for correct pagination/filtering\n\tpostCount := strings.Count(jsonStr, `\"id\":\"`)\n\tuassert.Equal(t, uint64(m.MemeCounter), uint64(postCount))\n\n\t// Check if ordering is correct\n\tcheck := strings.Index(jsonStr, \"Meme #1\") \u003c= strings.Index(jsonStr, \"Meme #2\")\n\tuassert.True(t, check, ufmt.Sprintf(\"Expected %s to be before %s\", memeData1, memeData2))\n}\n\nfunc TestBadSortBy(cur realm, t *testing.T) {\n\tadmin := testutils.TestAddress(\"admin\")\n\tm := NewMemeland(admin)\n\tnow := time.Now()\n\n\tnumOfPosts := 5\n\tvar memeData []string\n\tfor i := 1; i \u003c= numOfPosts; i++ {\n\t\t// Prepare meme data\n\t\tnextTime := now.Add(time.Duration(i) * time.Minute)\n\t\tdata := ufmt.Sprintf(\"Meme #%d\", i)\n\t\tmemeData = append(memeData, data)\n\n\t\tpostMemeAs(cur, admin, m, data, nextTime.Unix())\n\t}\n\n\t// Get timestamps\n\tbeforeEarliest := now.Add(-1 * time.Minute)\n\tafterLatest := now.Add(time.Duration(numOfPosts)*time.Minute + time.Minute)\n\n\ttests := []struct {\n\t\tname      string\n\t\tsortBy    string\n\t\twantPanic string\n\t}{\n\t\t{\n\t\t\tname:      \"Empty sortBy\",\n\t\t\tsortBy:    \"\",\n\t\t\twantPanic: \"runtime error: index out of range\",\n\t\t},\n\t\t{\n\t\t\tname:      \"Wrong sortBy\",\n\t\t\tsortBy:    \"random string\",\n\t\t\twantPanic: \"\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r == nil {\n\t\t\t\t\tt.Errorf(\"code did not panic when it should have\")\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\t// Panics should be caught\n\t\t\t_ = m.GetPostsInRange(beforeEarliest.Unix(), afterLatest.Unix(), 1, 1, tc.sortBy)\n\t\t})\n\t}\n}\n\nfunc TestNoPosts(cur realm, t *testing.T) {\n\tm := NewMemeland(testutils.TestAddress(\"admin\"))\n\n\t// Add a post to Memeland\n\tnow := time.Now().Unix()\n\n\tjsonStr := m.GetPostsInRange(0, now, 1, 1, \"DATE_CREATED\")\n\n\tuassert.Equal(t, jsonStr, \"[]\")\n}\n\nfunc TestUpvote(cur realm, t *testing.T) {\n\tadmin := testutils.TestAddress(\"admin\")\n\tm := NewMemeland(admin)\n\n\t// Add a post to Memeland\n\tnow := time.Now().Unix()\n\tpostID := postMemeAs(cur, admin, m, \"Test meme data\", now)\n\n\t// Initial upvote count should be 0\n\tpost := m.getPost(postID)\n\tuassert.Equal(t, 0, post.UpvoteTracker.Size())\n\n\t// Upvote the post\n\tupvoteResult := upvoteAs(cur, admin, m, postID)\n\tuassert.Equal(t, \"upvote successful\", upvoteResult)\n\n\t// Retrieve the post again and check the upvote count\n\tpost = m.getPost(postID)\n\tuassert.Equal(t, 1, post.UpvoteTracker.Size())\n}\n\nfunc TestDelete(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\n\t// Alice is admin\n\tm := NewMemeland(alice)\n\n\t// Bob adds post to Memeland\n\tnow := time.Now()\n\tpostID := postMemeAs(cur, bob, m, \"Meme #1\", now.Unix())\n\n\t// Alice removes Bob's post\n\tid := removePostAs(cur, alice, m, postID)\n\tuassert.Equal(t, postID, id, \"post IDs not matching\")\n\tuassert.Equal(t, 0, len(m.Posts), \"there should be 0 posts after removing\")\n}\n\nfunc TestDeleteByNonAdmin(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tm := NewMemeland(alice)\n\n\t// Add a post to Memeland\n\tnow := time.Now()\n\tpostID := postMemeAs(cur, alice, m, \"Meme #1\", now.Unix())\n\n\t// Bob will try to delete meme posted by Alice, which should fail.\n\t// removePostAs wraps the call in cross(cur), so the panic crosses\n\t// a realm boundary and must be caught with revive(); a plain\n\t// defer-recover wouldn't see it.\n\tbob := testutils.TestAddress(\"bob\")\n\tr := revive(func() {\n\t\tremovePostAs(cur, bob, m, postID)\n\t})\n\tif r == nil {\n\t\tt.Errorf(\"code did not panic when it should have\")\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"iBuuzR8nTlyVrRkXTBqXAXWhBxOeJIqfFk8lEYheDOxolVYe0e+1UEVlTA/Q2tTH1h6H7o0/SdNAH42D3hNccQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"merkle","path":"gno.land/p/demo/merkle","files":[{"name":"README.md","body":"# p/demo/merkle\n\nThis package implement a merkle tree that is complient with [merkletreejs](https://github.com/merkletreejs/merkletreejs)\n\n## [merkletreejs](https://github.com/merkletreejs/merkletreejs)\n\n```javascript\nconst { MerkleTree } = require(\"merkletreejs\");\nconst SHA256 = require(\"crypto-js/sha256\");\n\nlet leaves = [];\nfor (let i = 0; i \u003c 10; i++) {\n  leaves.push(SHA256(`node_${i}`));\n}\n\nconst tree = new MerkleTree(leaves, SHA256);\nconst root = tree.getRoot().toString(\"hex\");\n\nconsole.log(root); // cd8a40502b0b92bf58e7432a5abb2d8b60121cf2b7966d6ebaf103f907a1bc21\n```\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/merkle\"\ngno = \"0.9\"\n"},{"name":"merkle.gno","body":"package merkle\n\nimport (\n\t\"bytes\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"errors\"\n)\n\ntype Hashable interface {\n\tBytes() []byte\n}\n\ntype nodes []Node\n\ntype Node struct {\n\thash []byte\n\n\tposition uint8\n}\n\nfunc NewNode(hash []byte, position uint8) Node {\n\treturn Node{\n\t\thash:     hash,\n\t\tposition: position,\n\t}\n}\n\nfunc (n Node) Position() uint8 {\n\treturn n.position\n}\n\nfunc (n Node) Hash() string {\n\treturn hex.EncodeToString(n.hash[:])\n}\n\ntype Tree struct {\n\tlayers []nodes\n}\n\n// Root return the merkle root of the tree\nfunc (t *Tree) Root() string {\n\tfor _, l := range t.layers {\n\t\tif len(l) == 1 {\n\t\t\treturn l[0].Hash()\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// NewTree create a new Merkle Tree\nfunc NewTree(data []Hashable) *Tree {\n\ttree := \u0026Tree{}\n\n\tleaves := make([]Node, len(data))\n\n\tfor i, d := range data {\n\t\thash := sha256.Sum256(d.Bytes())\n\t\tleaves[i] = Node{hash: hash[:]}\n\t}\n\n\ttree.layers = []nodes{nodes(leaves)}\n\n\tvar buff bytes.Buffer\n\tfor len(leaves) \u003e 1 {\n\t\tlevel := make([]Node, 0, len(leaves)/2+1)\n\t\tfor i := 0; i \u003c len(leaves); i += 2 {\n\t\t\tbuff.Reset()\n\n\t\t\tif i \u003c len(leaves)-1 {\n\t\t\t\tbuff.Write(leaves[i].hash)\n\t\t\t\tbuff.Write(leaves[i+1].hash)\n\t\t\t\thash := sha256.Sum256(buff.Bytes())\n\t\t\t\tlevel = append(level, Node{\n\t\t\t\t\thash: hash[:],\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\tlevel = append(level, leaves[i])\n\t\t\t}\n\t\t}\n\t\tleaves = level\n\t\ttree.layers = append(tree.layers, level)\n\t}\n\treturn tree\n}\n\n// Proof return a MerkleProof\nfunc (t *Tree) Proof(data Hashable) ([]Node, error) {\n\ttargetHash := sha256.Sum256(data.Bytes())\n\ttargetIndex := -1\n\n\tfor i, layer := range t.layers[0] {\n\t\tif bytes.Equal(targetHash[:], layer.hash) {\n\t\t\ttargetIndex = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif targetIndex == -1 {\n\t\treturn nil, errors.New(\"target not found\")\n\t}\n\n\tproofs := make([]Node, 0, len(t.layers))\n\n\tfor _, layer := range t.layers {\n\t\tvar pairIndex int\n\n\t\tif targetIndex%2 == 0 {\n\t\t\tpairIndex = targetIndex + 1\n\t\t} else {\n\t\t\tpairIndex = targetIndex - 1\n\t\t}\n\t\tif pairIndex \u003c len(layer) {\n\t\t\tproofs = append(proofs, Node{\n\t\t\t\thash:     layer[pairIndex].hash,\n\t\t\t\tposition: uint8(targetIndex) % 2,\n\t\t\t})\n\t\t}\n\t\ttargetIndex /= 2\n\t}\n\treturn proofs, nil\n}\n\n// Verify if a merkle proof is valid\nfunc (t *Tree) Verify(leaf Hashable, proofs []Node) bool {\n\treturn Verify(t.Root(), leaf, proofs)\n}\n\n// Verify if a merkle proof is valid\nfunc Verify(root string, leaf Hashable, proofs []Node) bool {\n\thash := sha256.Sum256(leaf.Bytes())\n\n\tfor i := 0; i \u003c len(proofs); i += 1 {\n\t\tvar h []byte\n\t\tif proofs[i].position == 0 {\n\t\t\th = append(hash[:], proofs[i].hash...)\n\t\t} else {\n\t\t\th = append(proofs[i].hash, hash[:]...)\n\t\t}\n\t\thash = sha256.Sum256(h)\n\t}\n\treturn hex.EncodeToString(hash[:]) == root\n}\n"},{"name":"merkle_test.gno","body":"package merkle\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\ntype testData struct {\n\tcontent string\n}\n\nfunc (d testData) Bytes() []byte {\n\treturn []byte(d.content)\n}\n\nfunc TestMerkleTree(t *testing.T) {\n\ttests := []struct {\n\t\tsize     int\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tsize:     1,\n\t\t\texpected: \"cf9f824bce7f5bc63d557b23591f58577f53fe29f974a615bdddbd0140f912f4\",\n\t\t},\n\t\t{\n\t\t\tsize:     3,\n\t\t\texpected: \"1a4a5f0fa267244bf9f74a63fdf2a87eed5e97e4bd104a9e94728c8fb5442177\",\n\t\t},\n\t\t{\n\t\t\tsize:     10,\n\t\t\texpected: \"cd8a40502b0b92bf58e7432a5abb2d8b60121cf2b7966d6ebaf103f907a1bc21\",\n\t\t},\n\t\t{\n\t\t\tsize:     1000,\n\t\t\texpected: \"fa533d2efdf12be26bc410dfa42936ac63361324e35e9b1ff54d422a1dd2388b\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tvar leaves []Hashable\n\t\tfor i := 0; i \u003c test.size; i++ {\n\t\t\tleaves = append(leaves, testData{fmt.Sprintf(\"node_%d\", i)})\n\t\t}\n\n\t\ttree := NewTree(leaves)\n\n\t\tif tree == nil {\n\t\t\tt.Error(\"Merkle tree creation failed\")\n\t\t}\n\n\t\troot := tree.Root()\n\n\t\tif root != test.expected {\n\t\t\tt.Fatalf(\"merkle.Tree.Root(), expected: %s; got: %s\", test.expected, root)\n\t\t}\n\n\t\tfor _, leaf := range leaves {\n\t\t\tproofs, err := tree.Proof(leaf)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(\"failed to proof leaf: %v, on tree: %v\", leaf, test)\n\t\t\t}\n\n\t\t\tok := Verify(root, leaf, proofs)\n\t\t\tif !ok {\n\t\t\t\tt.Fatal(\"failed to verify leaf: %v, on tree: %v\", leaf, tree)\n\t\t\t}\n\t\t}\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"0H+p7euAAYQjJCiFXJ/zj6ca+/BgQTKWlyBA+mk87mMq5D1PK47azhixJPjjf0EvWOj26vdijCN9gGzH41Ac0w=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"microblog","path":"gno.land/p/demo/microblog","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/microblog\"\ngno = \"0.9\"\n"},{"name":"microblog.gno","body":"package microblog\n\nimport (\n\t\"errors\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nvar (\n\tErrNotFound    = errors.New(\"not found\")\n\tStatusNotFound = \"404\"\n)\n\ntype Microblog struct {\n\tTitle  string\n\tPrefix string   // i.e. r/gnoland/blog:\n\tPages  avl.Tree // author (string) -\u003e Page\n}\n\nfunc NewMicroblog(title string, prefix string) (m *Microblog) {\n\treturn \u0026Microblog{\n\t\tTitle:  title,\n\t\tPrefix: prefix,\n\t\tPages:  avl.Tree{},\n\t}\n}\n\nfunc (m *Microblog) GetPages() []*Page {\n\tvar (\n\t\tpages = make([]*Page, m.Pages.Size())\n\t\tindex = 0\n\t)\n\n\tm.Pages.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\tpages[index] = value.(*Page)\n\t\tindex++\n\t\treturn false\n\t})\n\n\tsort.Sort(byLastPosted(pages))\n\n\treturn pages\n}\n\n// NewPost records a new post. rlm must be the caller's own captured\n// cur (asserted via rlm.IsCurrent()); the post's author is set to\n// rlm.Previous().Address() — an external realm cannot forge a post\n// under a different identity.\nfunc (m *Microblog) NewPost(_ int, rlm realm, text string) error {\n\tif !rlm.IsCurrent() {\n\t\treturn errors.New(\"microblog: unauthorized\")\n\t}\n\tauthor := rlm.Previous().Address()\n\tif m.Pages.Get(author.String()) == nil {\n\t\t// make a new page for the new author\n\t\tm.Pages.Set(author.String(), \u0026Page{\n\t\t\tAuthor:    author,\n\t\t\tCreatedAt: time.Now(),\n\t\t})\n\t}\n\n\tpage, err := m.GetPage(author.String())\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn page.NewPost(text)\n}\n\nfunc (m *Microblog) GetPage(author string) (*Page, error) {\n\tsilo := m.Pages.Get(author)\n\tif silo == nil {\n\t\treturn nil, ErrNotFound\n\t}\n\treturn silo.(*Page), nil\n}\n\ntype Page struct {\n\tID         int\n\tAuthor     address\n\tCreatedAt  time.Time\n\tLastPosted time.Time\n\tPosts      avl.Tree // time -\u003e Post\n}\n\n// byLastPosted implements sort.Interface for []Page based on\n// the LastPosted field.\ntype byLastPosted []*Page\n\nfunc (a byLastPosted) Len() int           { return len(a) }\nfunc (a byLastPosted) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }\nfunc (a byLastPosted) Less(i, j int) bool { return a[i].LastPosted.After(a[j].LastPosted) }\n\nfunc (p *Page) NewPost(text string) error {\n\tnow := time.Now()\n\tp.LastPosted = now\n\tp.Posts.Set(ufmt.Sprintf(\"%s%d\", now.Format(time.RFC3339), p.Posts.Size()), \u0026Post{\n\t\tID:        p.Posts.Size(),\n\t\tText:      text,\n\t\tCreatedAt: now,\n\t})\n\treturn nil\n}\n\nfunc (p *Page) GetPosts() []*Post {\n\tposts := make([]*Post, p.Posts.Size())\n\ti := 0\n\tp.Posts.ReverseIterate(\"\", \"\", func(key string, value any) bool {\n\t\tpostParsed := value.(*Post)\n\t\tposts[i] = postParsed\n\t\ti++\n\t\treturn false\n\t})\n\treturn posts\n}\n\n// Post lists the specific update\ntype Post struct {\n\tID        int\n\tCreatedAt time.Time\n\tText      string\n}\n\nfunc (p *Post) String() string {\n\treturn \"\u003e \" + strings.ReplaceAll(p.Text, \"\\n\", \"\\n\u003e\\n\u003e\") + \"\\n\u003e\\n\u003e *\" + p.CreatedAt.Format(time.RFC1123) + \"*\"\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"j0CO9PhcVOenMoPRlOA/Cr1VoxKXzMGdJ0pF/SRFnkQ/+YN/fX7U6gOTNEttcgHW09RW0IoZpQA8ISxguY/PTA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"releases","path":"gno.land/p/demo/releases","files":[{"name":"changelog.gno","body":"package releases\n\nimport (\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/moul/mdtable/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\ntype changelog struct {\n\tname     string\n\treleases []release\n}\n\nfunc NewChangelog(name string) *changelog {\n\treturn \u0026changelog{\n\t\tname:     name,\n\t\treleases: make([]release, 0),\n\t}\n}\n\nfunc (c *changelog) NewRelease(version, url, notes string) {\n\tif latest := c.Latest(); latest != nil {\n\t\tlatest.isLatest = false\n\t}\n\n\trelease := release{\n\t\t// manual\n\t\tversion: version,\n\t\turl:     url,\n\t\tnotes:   notes,\n\n\t\t// internal\n\t\tchangelog: c,\n\t\tisLatest:  true,\n\t}\n\n\tc.releases = append(c.releases, release)\n}\n\nfunc (c changelog) RenderAsTable(max int) string {\n\tout := md.H1(c.name)\n\n\tif len(c.releases) == 0 {\n\t\tout += \"No releases.\"\n\t\treturn out\n\t}\n\n\tout += ufmt.Sprintf(\"See the %s changelog below.\\n\\n\", c.name)\n\ttable := mdtable.Table{\n\t\tHeaders: []string{\"Version\", \"Link\", \"Notes\"},\n\t}\n\n\tif max \u003e len(c.releases) {\n\t\tmax = len(c.releases)\n\t}\n\n\tfor i := len(c.releases) - 1; i \u003e= len(c.releases)-max; i-- {\n\t\tr := c.releases[i]\n\t\ttable.Append([]string{r.version, r.Link(), r.notes})\n\t}\n\n\tout += table.String()\n\treturn out\n}\n\nfunc (c *changelog) Render(path string) string {\n\tif path == \"\" {\n\t\toutput := \"# \" + c.name + \"\\n\\n\"\n\t\tmax := len(c.releases) - 1\n\t\tmin := 0\n\t\tif max-min \u003e 10 {\n\t\t\tmin = max - 10\n\t\t}\n\t\tfor i := max; i \u003e= min; i-- {\n\t\t\trelease := c.releases[i]\n\t\t\toutput += release.Render()\n\t\t}\n\t\treturn output\n\t}\n\n\trelease := c.ByVersion(path)\n\tif release != nil {\n\t\treturn release.Render()\n\t}\n\n\treturn \"no such release\"\n}\n\nfunc (c *changelog) Latest() *release {\n\tif len(c.releases) \u003e 0 {\n\t\tpos := len(c.releases) - 1\n\t\treturn \u0026c.releases[pos]\n\t}\n\treturn nil\n}\n\nfunc (c *changelog) ByVersion(version string) *release {\n\tfor _, release := range c.releases {\n\t\tif release.version == version {\n\t\t\treturn \u0026release\n\t\t}\n\t}\n\treturn nil\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/releases\"\ngno = \"0.9\"\n"},{"name":"release.gno","body":"package releases\n\ntype release struct {\n\t// manual\n\tversion string\n\turl     string\n\tnotes   string\n\n\t// internal\n\tisLatest  bool\n\tchangelog *changelog\n}\n\nfunc (r *release) URL() string     { return r.url }\nfunc (r *release) Version() string { return r.version }\nfunc (r *release) Notes() string   { return r.notes }\nfunc (r *release) IsLatest() bool  { return r.isLatest }\n\nfunc (r *release) Title() string {\n\toutput := r.changelog.name + \" \" + r.version\n\tif r.isLatest {\n\t\toutput += \" (latest)\"\n\t}\n\treturn output\n}\n\nfunc (r *release) Link() string {\n\treturn \"[\" + r.Title() + \"](\" + r.url + \")\"\n}\n\nfunc (r *release) Render() string {\n\toutput := \"\"\n\toutput += \"## \" + r.Link() + \"\\n\\n\"\n\tif r.notes != \"\" {\n\t\toutput += r.notes + \"\\n\\n\"\n\t}\n\treturn output\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"yiexec97GIbLbE1P78CopLzoOfKBlZ90l6hUONlhy9dQ4ZHg+DpfnFQ6WcySJcFjWWhJ//sBryzUtpMkgURqnA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"stack","path":"gno.land/p/demo/stack","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/stack\"\ngno = \"0.9\"\n"},{"name":"stack.gno","body":"package stack\n\ntype Stack struct {\n\ttop    *node\n\tlength int\n}\n\ntype node struct {\n\tvalue any\n\tprev  *node\n}\n\nfunc New() *Stack {\n\treturn \u0026Stack{nil, 0}\n}\n\nfunc (s *Stack) Len() int {\n\treturn s.length\n}\n\nfunc (s *Stack) Top() any {\n\tif s.length == 0 {\n\t\treturn nil\n\t}\n\treturn s.top.value\n}\n\nfunc (s *Stack) Pop() any {\n\tif s.length == 0 {\n\t\treturn nil\n\t}\n\n\tnode := s.top\n\ts.top = node.prev\n\ts.length -= 1\n\treturn node.value\n}\n\nfunc (s *Stack) Push(value any) {\n\tnode := \u0026node{value, s.top}\n\ts.top = node\n\ts.length += 1\n}\n"},{"name":"stack_test.gno","body":"package stack\n\nimport \"testing\"\n\nfunc TestStack(t *testing.T) {\n\ts := New() // Empty stack\n\n\tif s.Len() != 0 {\n\t\tt.Errorf(\"s.Len(): expected 0; got %d\", s.Len())\n\t}\n\n\ts.Push(1)\n\n\tif s.Len() != 1 {\n\t\tt.Errorf(\"s.Len(): expected 1; got %d\", s.Len())\n\t}\n\n\tif top := s.Top(); top.(int) != 1 {\n\t\tt.Errorf(\"s.Top(): expected 1; got %v\", top.(int))\n\t}\n\n\tif elem := s.Pop(); elem.(int) != 1 {\n\t\tt.Errorf(\"s.Pop(): expected 1; got %v\", elem.(int))\n\t}\n\tif s.Len() != 0 {\n\t\tt.Errorf(\"s.Len(): expected 0; got %d\", s.Len())\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"WZf24qftK10MkF4JwtqMDWYB06LaQEu6FPmYgHEXgwMVmoUlVfhw93NZ7Qer9bBEsrMfoF8ZgAONrXCSRJRGzQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"subscription","path":"gno.land/p/demo/subscription","files":[{"name":"doc.gno","body":"// Package subscription provides recurring and lifetime subscription\n// primitives. Payment is pulled through a caller-supplied\n// *allowancesender.AllowanceSender (a cap-token whose construction is\n// gated on banker.IsCanonical, so the lib can rely on Send actually\n// moving real coins). Subscriber identity is decoupled from cap-token\n// authority; the lib does not inspect call-chain shape.\n//\n// Each subscription instance locks its payment destination at\n// construction time (payeeAddr) and pulls a fixed per-period amount\n// from the cap on each Subscribe call. A leaked struct pointer cannot\n// redirect payment.\n//\n// Recurring Subscription:\n//\n//\t// In the consumer realm, at deployment:\n//\tvar recSub = recurring.NewRecurringSubscription(\n//\t    ownerAddr,                          // address authorized to call UpdateAmount\n//\t    time.Hour*24*30,                    // 30-day periods\n//\t    100,                                // 100 ugnot per period\n//\t    unsafe.CurrentRealm().Address(),   // payee = this realm\n//\t)\n//\n//\t// Wrapper exposed to users. The wrapper builds an AllowanceSender\n//\t// from a real banker, defers Close, and forwards the call. Subscribe\n//\t// pulls 100 ugnot from the consumer realm (whose -send envelope\n//\t// the wrapper validated) and records `subscriber` as active until\n//\t// time.Now() + 30 days.\n//\tfunc Subscribe(_ realm, subscriber address) {\n//\t    if !unsafe.PreviousRealm().IsUserCall() { panic(\"EOA only\") }\n//\t    src := banker.NewBanker(banker.BankerTypeRealmSend, cur)\n//\t    al := allowancesender.New(src, unsafe.CurrentRealm().Address(),\n//\t        chain.NewCoins(chain.NewCoin(\"ugnot\", recSub.GetAmount())))\n//\t    defer al.Close()\n//\t    if err := recSub.Subscribe(al, subscriber); err != nil {\n//\t        panic(err)\n//\t    }\n//\t}\n//\n// Lifetime Subscription:\n//\n//\tvar lifeSub = lifetime.NewLifetimeSubscription(\n//\t    ownerAddr,                          // address authorized to call UpdateAmount\n//\t    500,                                // 500 ugnot one-time\n//\t    unsafe.CurrentRealm().Address(),\n//\t)\n//\n//\t// Same wrapper shape as recurring.\n//\n// Both libraries trust whatever address the consumer's wrapper passes\n// as `subscriber` — the wrapper's policy decides who's allowed to\n// register, who can be gifted, etc. Self-Subscribe and gift collapse\n// into the same call with different `subscriber` arguments.\npackage subscription\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/subscription\"\ngno = \"0.9\"\n"},{"name":"subscription.gno","body":"package subscription\n\nimport (\n\t\"gno.land/p/jaekwon/allowancesender/v0\"\n)\n\n// Subscription is the cross-implementation interface. Payment is\n// pulled from a caller-supplied *AllowanceSender; subscriber identity\n// is decoupled from cap-token authority.\ntype Subscription interface {\n\tHasValidSubscription(address) error\n\tSubscribe(al *allowancesender.AllowanceSender, subscriber address) error\n\tUpdateAmount(newAmount int64) error\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"8te/A9QDDSNR4mOc11/1ZyHEbDxCYTEKijn3WYl+IYhJv8eqFlgtHcNVtG+pf7dylBOK3vXmSqlkGuKWJ5qMSA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"lifetime","path":"gno.land/p/demo/subscription/lifetime","files":[{"name":"errors.gno","body":"package lifetime\n\nimport \"errors\"\n\nvar (\n\tErrNoSub             = errors.New(\"lifetime subscription: no active subscription found\")\n\tErrAlreadySub        = errors.New(\"lifetime subscription: this address already has an active lifetime subscription\")\n\tErrNotAuthorized     = errors.New(\"lifetime subscription: action not authorized\")\n\tErrInvalidPayee      = errors.New(\"lifetime subscription: payee address is invalid\")\n\tErrInvalidSubscriber = errors.New(\"lifetime subscription: subscriber address is invalid\")\n)\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/subscription/lifetime\"\ngno = \"0.9\"\n"},{"name":"lifetime.gno","body":"package lifetime\n\nimport (\n\t\"chain\"\n\n\t\"gno.land/p/jaekwon/allowancesender/v0\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/ownable/v0\"\n)\n\n// LifetimeSubscription represents a one-time-payment subscription that\n// grants permanent access. Payment is pulled from a caller-supplied\n// *AllowanceSender cap-token; funds are sent to payeeAddr, which is\n// locked at construction.\ntype LifetimeSubscription struct {\n\townable.Ownable\n\tamount    int64\n\tpayeeAddr address\n\tsubs      *avl.Tree // address.String() -\u003e bool\n}\n\n// NewLifetimeSubscription creates a new lifetime subscription. owner is\n// the address authorized to call UpdateAmount.\nfunc NewLifetimeSubscription(owner address, amount int64, payeeAddr address) *LifetimeSubscription {\n\tif !payeeAddr.IsValid() {\n\t\tpanic(ErrInvalidPayee)\n\t}\n\treturn \u0026LifetimeSubscription{\n\t\tOwnable:   *ownable.NewWithAddress(owner),\n\t\tamount:    amount,\n\t\tpayeeAddr: payeeAddr,\n\t\tsubs:      avl.NewTree(),\n\t}\n}\n\n// Subscribe pulls the one-time payment from al into payeeAddr and\n// records subscriber as having a lifetime subscription. Already-subscribed\n// returns ErrAlreadySub WITHOUT pulling payment.\nfunc (ls *LifetimeSubscription) Subscribe(al *allowancesender.AllowanceSender, subscriber address) error {\n\tif !subscriber.IsValid() {\n\t\treturn ErrInvalidSubscriber\n\t}\n\tif ls.subs.Has(subscriber.String()) {\n\t\treturn ErrAlreadySub\n\t}\n\tal.Send(ls.payeeAddr, chain.NewCoins(chain.NewCoin(\"ugnot\", ls.amount)))\n\tls.subs.Set(subscriber.String(), true)\n\treturn nil\n}\n\n// HasValidSubscription verifies whether subscriber holds a lifetime subscription.\nfunc (ls *LifetimeSubscription) HasValidSubscription(subscriber address) error {\n\tif !ls.subs.Has(subscriber.String()) {\n\t\treturn ErrNoSub\n\t}\n\treturn nil\n}\n\n// UpdateAmount lets the subscription owner change the one-time amount.\n// rlm must be the caller's own captured cur; rlm.Previous().Address()\n// must equal the owner.\nfunc (ls *LifetimeSubscription) UpdateAmount(_ int, rlm realm, newAmount int64) error {\n\tif !rlm.IsCurrent() {\n\t\treturn ErrNotAuthorized\n\t}\n\tif !ls.OwnedBy(rlm.Previous().Address()) {\n\t\treturn ErrNotAuthorized\n\t}\n\tls.amount = newAmount\n\treturn nil\n}\n\n// GetAmount returns the current one-time subscription amount.\nfunc (ls *LifetimeSubscription) GetAmount() int64 {\n\treturn ls.amount\n}\n\n// GetPayee returns the locked payment destination.\nfunc (ls *LifetimeSubscription) GetPayee() address {\n\treturn ls.payeeAddr\n}\n"},{"name":"lifetime_test.gno","body":"package lifetime\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\t\"gno.land/p/jaekwon/allowancesender/v0\"\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\n// cur is a zero-value realm used as a placeholder when forwarding to\n// uassert/urequire dispatch helpers that gained an `rlm realm` param.\n// These tests pass `func()` callbacks (no crossing inside the callback),\n// so rlm is ignored — a nil realm here is safe.\nvar cur realm\nvar (\n\talice = testutils.TestAddress(\"alice\")\n\tbob   = testutils.TestAddress(\"bob\")\n\tpayee = testutils.TestAddress(\"payee\")\n)\n\nfunc TestNewLifetimeSubscription_RejectsInvalidPayee(cur realm, t *testing.T) {\n\tuassert.PanicsWithMessage(t, cur, ErrInvalidPayee.Error(), func() {\n\t\tNewLifetimeSubscription(alice, 1000, address(\"\"))\n\t})\n}\n\nfunc TestSubscribe_RejectsInvalidSubscriber(cur realm, t *testing.T) {\n\tls := NewLifetimeSubscription(alice, 1000, payee)\n\terr := ls.Subscribe(\u0026allowancesender.AllowanceSender{}, address(\"\"))\n\tuassert.ErrorIs(t, err, ErrInvalidSubscriber)\n}\n\nfunc TestHasValidSubscription_NoSub(cur realm, t *testing.T) {\n\tls := NewLifetimeSubscription(alice, 1000, payee)\n\tuassert.ErrorIs(t, ls.HasValidSubscription(alice), ErrNoSub)\n}\n\nfunc TestHasValidSubscription_Active(cur realm, t *testing.T) {\n\tls := NewLifetimeSubscription(alice, 1000, payee)\n\tls.subs.Set(alice.String(), true)\n\tuassert.NoError(t, ls.HasValidSubscription(alice))\n}\n\nfunc TestSubscribe_AlreadySubscribedReturnsErrBeforeSend(cur realm, t *testing.T) {\n\tls := NewLifetimeSubscription(alice, 1000, payee)\n\tls.subs.Set(alice.String(), true)\n\n\terr := ls.Subscribe(\u0026allowancesender.AllowanceSender{}, alice)\n\tuassert.ErrorIs(t, err, ErrAlreadySub)\n}\n\nfunc TestSubscribe_SendPanicLeavesNoRecord(cur realm, t *testing.T) {\n\tls := NewLifetimeSubscription(alice, 1000, payee)\n\n\tfunc() {\n\t\tdefer func() { _ = recover() }()\n\t\tls.Subscribe(\u0026allowancesender.AllowanceSender{}, bob)\n\t}()\n\n\tuassert.ErrorIs(t, ls.HasValidSubscription(bob), ErrNoSub)\n}\n\nfunc TestUpdateAmount_OwnerSucceeds(cur realm, t *testing.T) {\n\tls := NewLifetimeSubscription(alice, 1000, payee)\n\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\tvar err error\n\tfunc(cur realm) { err = ls.UpdateAmount(0, cur, 2000) }(cross(cur))\n\tuassert.NoError(t, err)\n\tuassert.Equal(t, int64(2000), ls.GetAmount())\n}\n\nfunc TestUpdateAmount_NonOwnerFails(cur realm, t *testing.T) {\n\tls := NewLifetimeSubscription(alice, 1000, payee)\n\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\tvar err error\n\tfunc(cur realm) { err = ls.UpdateAmount(0, cur, 3000) }(cross(cur))\n\tuassert.ErrorIs(t, err, ErrNotAuthorized)\n}\n\nfunc TestGetPayee(cur realm, t *testing.T) {\n\tls := NewLifetimeSubscription(alice, 1000, payee)\n\tuassert.Equal(t, payee.String(), ls.GetPayee().String())\n}\n\nvar _ = chain.Coins{}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"7EZSKYyXUPuX47XZyYyMxEwRK0ONi57mHuvXzuw6z5J5doEhpxjEIBIqyVTAyFP6rrR46O1Ml7r4Fz08mE1rJg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"recurring","path":"gno.land/p/demo/subscription/recurring","files":[{"name":"errors.gno","body":"package recurring\n\nimport \"errors\"\n\nvar (\n\tErrNoSub             = errors.New(\"recurring subscription: no active subscription found\")\n\tErrSubExpired        = errors.New(\"recurring subscription: your subscription has expired\")\n\tErrAlreadySub        = errors.New(\"recurring subscription: this address already has an active subscription\")\n\tErrNotAuthorized     = errors.New(\"recurring subscription: action not authorized\")\n\tErrInvalidPayee      = errors.New(\"recurring subscription: payee address is invalid\")\n\tErrInvalidSubscriber = errors.New(\"recurring subscription: subscriber address is invalid\")\n)\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/subscription/recurring\"\ngno = \"0.9\"\n"},{"name":"recurring.gno","body":"package recurring\n\nimport (\n\t\"chain\"\n\t\"time\"\n\n\t\"gno.land/p/jaekwon/allowancesender/v0\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/ownable/v0\"\n)\n\n// RecurringSubscription represents a subscription that requires periodic\n// payments. Payment is pulled from a caller-supplied *AllowanceSender\n// cap-token (which the constructor of this struct trusts via\n// banker.IsCanonical). Funds are sent to payeeAddr, locked at\n// construction so that a leaked struct pointer cannot redirect payment.\ntype RecurringSubscription struct {\n\townable.Ownable\n\tduration  time.Duration\n\tamount    int64\n\tpayeeAddr address   // payment destination; set at construction.\n\tsubs      *avl.Tree // address.String() -\u003e time.Time\n}\n\n// NewRecurringSubscription creates a new recurring subscription. owner is\n// the address authorized to call UpdateAmount. payeeAddr must be valid\n// (typically the constructing realm's own address) and is the fixed\n// destination for every Send pulled by Subscribe.\nfunc NewRecurringSubscription(owner address, duration time.Duration, amount int64, payeeAddr address) *RecurringSubscription {\n\tif !payeeAddr.IsValid() {\n\t\tpanic(ErrInvalidPayee)\n\t}\n\treturn \u0026RecurringSubscription{\n\t\tOwnable:   *ownable.NewWithAddress(owner),\n\t\tduration:  duration,\n\t\tamount:    amount,\n\t\tpayeeAddr: payeeAddr,\n\t\tsubs:      avl.NewTree(),\n\t}\n}\n\n// HasValidSubscription verifies whether subscriber holds an active\n// recurring subscription.\nfunc (rs *RecurringSubscription) HasValidSubscription(subscriber address) error {\n\texpTime := rs.subs.Get(subscriber.String())\n\tif expTime == nil {\n\t\treturn ErrNoSub\n\t}\n\tif time.Now().After(expTime.(time.Time)) {\n\t\treturn ErrSubExpired\n\t}\n\treturn nil\n}\n\n// Subscribe pulls one period's worth of payment from al into payeeAddr\n// and records subscriber as having an active subscription until\n// time.Now() + duration. Already-subscribed-and-active returns\n// ErrAlreadySub WITHOUT pulling payment. Subscriber identity is\n// decoupled from cap-token authority: a single AllowanceSender holder\n// can subscribe any address (gift, self, or third-party).\n//\n// On Send-side failure (closed allowance, cap exceeded, insufficient\n// funds, etc.), al.Send panics — the whole tx reverts and no\n// subscription is recorded. Atomicity covers any half-state.\nfunc (rs *RecurringSubscription) Subscribe(al *allowancesender.AllowanceSender, subscriber address) error {\n\tif !subscriber.IsValid() {\n\t\treturn ErrInvalidSubscriber\n\t}\n\t// Already-sub check first; avoid burning cap-token capacity on a\n\t// duplicate that would just be rejected.\n\tif expTime := rs.subs.Get(subscriber.String()); expTime != nil {\n\t\tif time.Now().Before(expTime.(time.Time)) {\n\t\t\treturn ErrAlreadySub\n\t\t}\n\t}\n\tal.Send(rs.payeeAddr, chain.NewCoins(chain.NewCoin(\"ugnot\", rs.amount)))\n\trs.subs.Set(subscriber.String(), time.Now().Add(rs.duration))\n\treturn nil\n}\n\n// GetExpiration returns the subscription expiration for subscriber.\nfunc (rs *RecurringSubscription) GetExpiration(subscriber address) (time.Time, error) {\n\texpTime := rs.subs.Get(subscriber.String())\n\tif expTime == nil {\n\t\treturn time.Time{}, ErrNoSub\n\t}\n\treturn expTime.(time.Time), nil\n}\n\n// UpdateAmount lets the subscription owner change the per-period amount.\n// rlm must be the caller's own captured cur; rlm.Previous().Address()\n// must equal the owner.\nfunc (rs *RecurringSubscription) UpdateAmount(_ int, rlm realm, newAmount int64) error {\n\tif !rlm.IsCurrent() {\n\t\treturn ErrNotAuthorized\n\t}\n\tif !rs.OwnedBy(rlm.Previous().Address()) {\n\t\treturn ErrNotAuthorized\n\t}\n\trs.amount = newAmount\n\treturn nil\n}\n\n// GetAmount returns the current per-period subscription amount.\nfunc (rs *RecurringSubscription) GetAmount() int64 {\n\treturn rs.amount\n}\n\n// GetPayee returns the locked payment destination.\nfunc (rs *RecurringSubscription) GetPayee() address {\n\treturn rs.payeeAddr\n}\n"},{"name":"recurring_test.gno","body":"package recurring\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/jaekwon/allowancesender/v0\"\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\n// cur is a zero-value realm used as a placeholder when forwarding to\n// uassert/urequire dispatch helpers that gained an `rlm realm` param.\n// These tests pass `func()` callbacks (no crossing inside the callback),\n// so rlm is ignored — a nil realm here is safe.\nvar cur realm\nvar (\n\talice = testutils.TestAddress(\"alice\")\n\tbob   = testutils.TestAddress(\"bob\")\n\tpayee = testutils.TestAddress(\"payee\")\n)\n\n// stubSender is a Send-only stand-in for *AllowanceSender used by these\n// tests. Subscribe accepts a concrete *allowancesender.AllowanceSender,\n// and outside the allowancesender package we cannot construct a working\n// one (the IsCanonical gate rejects synthetic bankers). Tests therefore\n// inject behavior through stubSender by wrapping it as a fake\n// AllowanceSender via the allowancesender package's same-package helpers.\n//\n// Since allowancesender's struct fields are unexported, we can't build\n// one here either. Instead each test uses the empty-struct shortcut\n// `\u0026allowancesender.AllowanceSender{}` which compiles cross-package\n// (no field-name reference) but produces an unusable instance whose\n// Send always panics. That panic propagates out of Subscribe, the tx\n// reverts, and tests assert the panic shape. For success-path tests,\n// we sidestep the AllowanceSender plumbing by constructing the\n// RecurringSubscription, then directly setting subs entries via the\n// in-package field — testing Subscribe's bookkeeping without the cap.\n//\n// This is sufficient for unit-level coverage of the subscription state\n// machine (already-sub gate, expiration, ownership, idempotency).\n// Full payment-path coverage requires integration txtars, which are\n// future work.\n\nfunc TestNewRecurringSubscription_RejectsInvalidPayee(cur realm, t *testing.T) {\n\tuassert.PanicsWithMessage(t, cur, ErrInvalidPayee.Error(), func() {\n\t\tNewRecurringSubscription(alice, time.Hour, 1000, address(\"\"))\n\t})\n}\n\nfunc TestSubscribe_RejectsInvalidSubscriber(cur realm, t *testing.T) {\n\trs := NewRecurringSubscription(alice, time.Hour, 1000, payee)\n\terr := rs.Subscribe(\u0026allowancesender.AllowanceSender{}, address(\"\"))\n\tuassert.ErrorIs(t, err, ErrInvalidSubscriber)\n}\n\nfunc TestHasValidSubscription_NoSub(cur realm, t *testing.T) {\n\trs := NewRecurringSubscription(alice, time.Hour, 1000, payee)\n\terr := rs.HasValidSubscription(alice)\n\tuassert.ErrorIs(t, err, ErrNoSub)\n}\n\nfunc TestHasValidSubscription_Active(cur realm, t *testing.T) {\n\trs := NewRecurringSubscription(alice, time.Hour, 1000, payee)\n\trs.subs.Set(alice.String(), time.Now().Add(time.Hour))\n\tuassert.NoError(t, rs.HasValidSubscription(alice))\n}\n\nfunc TestHasValidSubscription_Expired(cur realm, t *testing.T) {\n\trs := NewRecurringSubscription(alice, time.Hour, 1000, payee)\n\trs.subs.Set(alice.String(), time.Now().Add(-time.Hour))\n\tuassert.ErrorIs(t, rs.HasValidSubscription(alice), ErrSubExpired)\n}\n\n// TestSubscribe_AlreadyActiveReturnsErrBeforeSend exercises the\n// duplicate-detection short-circuit: when the subscriber is already\n// active, Subscribe returns ErrAlreadySub WITHOUT calling al.Send. We\n// pass an unusable empty AllowanceSender; if Send were called it would\n// panic, but the early return prevents that.\nfunc TestSubscribe_AlreadyActiveReturnsErrBeforeSend(cur realm, t *testing.T) {\n\trs := NewRecurringSubscription(alice, time.Hour, 1000, payee)\n\trs.subs.Set(alice.String(), time.Now().Add(time.Hour))\n\n\terr := rs.Subscribe(\u0026allowancesender.AllowanceSender{}, alice)\n\tuassert.ErrorIs(t, err, ErrAlreadySub)\n}\n\n// TestSubscribe_SendPanicLeavesNoRecord exercises the send-side failure:\n// junk AllowanceSender → Send panics → tx revert. Since unit tests don't\n// have tx semantics, the panic propagates. Recover and assert no\n// subscription was recorded.\nfunc TestSubscribe_SendPanicLeavesNoRecord(cur realm, t *testing.T) {\n\trs := NewRecurringSubscription(alice, time.Hour, 1000, payee)\n\n\tfunc() {\n\t\tdefer func() { _ = recover() }()\n\t\trs.Subscribe(\u0026allowancesender.AllowanceSender{}, bob)\n\t}()\n\n\tuassert.ErrorIs(t, rs.HasValidSubscription(bob), ErrNoSub)\n}\n\nfunc TestUpdateAmount_OwnerSucceeds(cur realm, t *testing.T) {\n\trs := NewRecurringSubscription(alice, time.Hour, 1000, payee)\n\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\tvar err error\n\tfunc(cur realm) { err = rs.UpdateAmount(0, cur, 2000) }(cross(cur))\n\tuassert.NoError(t, err)\n\tuassert.Equal(t, int64(2000), rs.GetAmount())\n}\n\nfunc TestUpdateAmount_NonOwnerFails(cur realm, t *testing.T) {\n\trs := NewRecurringSubscription(alice, time.Hour, 1000, payee)\n\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\tvar err error\n\tfunc(cur realm) { err = rs.UpdateAmount(0, cur, 3000) }(cross(cur))\n\tuassert.ErrorIs(t, err, ErrNotAuthorized)\n}\n\nfunc TestGetAmount(cur realm, t *testing.T) {\n\trs := NewRecurringSubscription(alice, time.Hour, 1000, payee)\n\tuassert.Equal(t, int64(1000), rs.GetAmount())\n}\n\nfunc TestGetPayee(cur realm, t *testing.T) {\n\trs := NewRecurringSubscription(alice, time.Hour, 1000, payee)\n\tuassert.Equal(t, payee.String(), rs.GetPayee().String())\n}\n\nfunc TestGetExpiration_NoSub(cur realm, t *testing.T) {\n\trs := NewRecurringSubscription(alice, time.Hour, 1000, payee)\n\t_, err := rs.GetExpiration(alice)\n\tuassert.ErrorIs(t, err, ErrNoSub)\n}\n\nfunc TestGetExpiration_Active(cur realm, t *testing.T) {\n\trs := NewRecurringSubscription(alice, time.Hour, 1000, payee)\n\texpected := time.Now().Add(time.Hour)\n\trs.subs.Set(alice.String(), expected)\n\n\tgot, err := rs.GetExpiration(alice)\n\tuassert.NoError(t, err)\n\tuassert.Equal(t, expected.String(), got.String())\n}\n\n// silence unused-import warning for chain (used in production code paths).\nvar _ = chain.Coins{}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"/p5iMKWHZZgMC6e6S7BR55iGabsyj4xsY1s1yDyXlP1PjymdEy74ybydmdUmmhN8TP9ZfIWwEA7QRc9eMFhWPw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"tamagotchi","path":"gno.land/p/demo/tamagotchi","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/tamagotchi\"\ngno = \"0.9\"\n"},{"name":"tamagotchi.gno","body":"package tamagotchi\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Tamagotchi structure\ntype Tamagotchi struct {\n\tname        string\n\thunger      int\n\thappiness   int\n\thealth      int\n\tage         int\n\tmaxAge      int\n\tsleepy      int\n\tcreated     time.Time\n\tlastUpdated time.Time\n}\n\nfunc New(name string) *Tamagotchi {\n\tnow := time.Now()\n\treturn \u0026Tamagotchi{\n\t\tname:        name,\n\t\thunger:      50,\n\t\thappiness:   50,\n\t\thealth:      50,\n\t\tmaxAge:      100,\n\t\tlastUpdated: now,\n\t\tcreated:     now,\n\t}\n}\n\nfunc (t *Tamagotchi) Name() string {\n\tt.update()\n\treturn t.name\n}\n\nfunc (t *Tamagotchi) Hunger() int {\n\tt.update()\n\treturn t.hunger\n}\n\nfunc (t *Tamagotchi) Happiness() int {\n\tt.update()\n\treturn t.happiness\n}\n\nfunc (t *Tamagotchi) Health() int {\n\tt.update()\n\treturn t.health\n}\n\nfunc (t *Tamagotchi) Age() int {\n\tt.update()\n\treturn t.age\n}\n\nfunc (t *Tamagotchi) Sleepy() int {\n\tt.update()\n\treturn t.sleepy\n}\n\n// Feed method for Tamagotchi\nfunc (t *Tamagotchi) Feed() {\n\tt.update()\n\tif t.dead() {\n\t\treturn\n\t}\n\tt.hunger = bound(t.hunger-10, 0, 100)\n}\n\n// Play method for Tamagotchi\nfunc (t *Tamagotchi) Play() {\n\tt.update()\n\tif t.dead() {\n\t\treturn\n\t}\n\tt.happiness = bound(t.happiness+10, 0, 100)\n}\n\n// Heal method for Tamagotchi\nfunc (t *Tamagotchi) Heal() {\n\tt.update()\n\n\tif t.dead() {\n\t\treturn\n\t}\n\tt.health = bound(t.health+10, 0, 100)\n}\n\nfunc (t Tamagotchi) dead() bool { return t.health == 0 }\n\n// Update applies changes based on the duration since the last update\nfunc (t *Tamagotchi) update() {\n\tif t.dead() {\n\t\treturn\n\t}\n\n\tnow := time.Now()\n\tif t.lastUpdated == now {\n\t\treturn\n\t}\n\n\tduration := now.Sub(t.lastUpdated)\n\telapsedMins := int(duration.Minutes())\n\n\tt.hunger = bound(t.hunger+elapsedMins, 0, 100)\n\tt.happiness = bound(t.happiness-elapsedMins, 0, 100)\n\tt.health = bound(t.health-elapsedMins, 0, 100)\n\tt.sleepy = bound(t.sleepy+elapsedMins, 0, 100)\n\n\t// age is hours since created\n\tt.age = int(now.Sub(t.created).Hours())\n\tif t.age \u003e t.maxAge {\n\t\tt.age = t.maxAge\n\t\tt.health = 0\n\t}\n\tif t.health == 0 {\n\t\tt.sleepy = 0\n\t\tt.happiness = 0\n\t\tt.hunger = 0\n\t}\n\n\tt.lastUpdated = now\n}\n\n// Face returns an ASCII art representation of the Tamagotchi's current state\nfunc (t *Tamagotchi) Face() string {\n\tt.update()\n\treturn t.face()\n}\n\nfunc (t *Tamagotchi) face() string {\n\tswitch {\n\tcase t.health == 0:\n\t\treturn \"😵\" // dead face\n\tcase t.health \u003c 30:\n\t\treturn \"😷\" // sick face\n\tcase t.happiness \u003c 30:\n\t\treturn \"😢\" // sad face\n\tcase t.hunger \u003e 70:\n\t\treturn \"😫\" // hungry face\n\tcase t.sleepy \u003e 70:\n\t\treturn \"😴\" // sleepy face\n\tdefault:\n\t\treturn \"😃\" // happy face\n\t}\n}\n\n// Markdown method for Tamagotchi\nfunc (t *Tamagotchi) Markdown() string {\n\tt.update()\n\treturn ufmt.Sprintf(`# %s %s\n\n* age: %d\n* hunger: %d\n* happiness: %d\n* health: %d\n* sleepy: %d`,\n\t\tt.name, t.Face(),\n\t\tt.age, t.hunger, t.happiness, t.health, t.sleepy,\n\t)\n}\n\nfunc bound(n, min, max int) int {\n\tif n \u003c min {\n\t\treturn min\n\t}\n\tif n \u003e max {\n\t\treturn max\n\t}\n\treturn n\n}\n"},{"name":"z0_filetest.gno","body":"package main\n\nimport (\n\t\"os\"\n\t\"time\"\n\n\t\"gno.land/p/demo/tamagotchi\"\n)\n\nfunc main() {\n\tt := tamagotchi.New(\"Gnome\")\n\n\tprintln(\"\\n-- INITIAL\\n\")\n\tprintln(t.Markdown())\n\n\tprintln(\"\\n-- WAIT 20 minutes\\n\")\n\tos.Sleep(20 * time.Minute)\n\tprintln(t.Markdown())\n\n\tprintln(\"\\n-- FEEDx3, PLAYx2, HEALx4\\n\")\n\tt.Feed()\n\tt.Feed()\n\tt.Feed()\n\tt.Play()\n\tt.Play()\n\tt.Heal()\n\tt.Heal()\n\tt.Heal()\n\tt.Heal()\n\tprintln(t.Markdown())\n\n\tprintln(\"\\n-- WAIT 20 minutes\\n\")\n\tos.Sleep(20 * time.Minute)\n\tprintln(t.Markdown())\n\n\tprintln(\"\\n-- WAIT 20 hours\\n\")\n\tos.Sleep(20 * time.Hour)\n\tprintln(t.Markdown())\n\n\tprintln(\"\\n-- WAIT 20 hours\\n\")\n\tos.Sleep(20 * time.Hour)\n\tprintln(t.Markdown())\n}\n\n// Output:\n//\n// -- INITIAL\n//\n// # Gnome 😃\n//\n// * age: 0\n// * hunger: 50\n// * happiness: 50\n// * health: 50\n// * sleepy: 0\n//\n// -- WAIT 20 minutes\n//\n// # Gnome 😃\n//\n// * age: 0\n// * hunger: 70\n// * happiness: 30\n// * health: 30\n// * sleepy: 20\n//\n// -- FEEDx3, PLAYx2, HEALx4\n//\n// # Gnome 😃\n//\n// * age: 0\n// * hunger: 40\n// * happiness: 50\n// * health: 70\n// * sleepy: 20\n//\n// -- WAIT 20 minutes\n//\n// # Gnome 😃\n//\n// * age: 0\n// * hunger: 60\n// * happiness: 30\n// * health: 50\n// * sleepy: 40\n//\n// -- WAIT 20 hours\n//\n// # Gnome 😵\n//\n// * age: 20\n// * hunger: 0\n// * happiness: 0\n// * health: 0\n// * sleepy: 0\n//\n// -- WAIT 20 hours\n//\n// # Gnome 😵\n//\n// * age: 20\n// * hunger: 0\n// * happiness: 0\n// * health: 0\n// * sleepy: 0\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"HPrplloIVimPQShhesaboKSArIsNEBKurDSFCEnnS9w3xNVBm9zyvlVD11Ig2eVdz+Sa7JOkHr6iTkNSjwcPVw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"todolist","path":"gno.land/p/demo/todolist","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/todolist\"\ngno = \"0.9\"\n"},{"name":"todolist.gno","body":"package todolist\n\nimport (\n\t\"chain/runtime/unsafe\"\n\t\"strconv\"\n\n\t\"gno.land/p/nt/avl/v0\"\n)\n\ntype TodoList struct {\n\tTitle string\n\tTasks *avl.Tree\n\tOwner address\n}\n\ntype Task struct {\n\tTitle string\n\tDone  bool\n}\n\nfunc NewTodoList(title string) *TodoList {\n\treturn \u0026TodoList{\n\t\tTitle: title,\n\t\tTasks: avl.NewTree(),\n\t\tOwner: unsafe.OriginCaller(),\n\t}\n}\n\nfunc NewTask(title string) *Task {\n\treturn \u0026Task{\n\t\tTitle: title,\n\t\tDone:  false,\n\t}\n}\n\nfunc (tl *TodoList) AddTask(id int, task *Task) {\n\ttl.Tasks.Set(strconv.Itoa(id), task)\n}\n\nfunc ToggleTaskStatus(task *Task) {\n\ttask.Done = !task.Done\n}\n\nfunc (tl *TodoList) RemoveTask(taskId string) {\n\ttl.Tasks.Remove(taskId)\n}\n\nfunc (tl *TodoList) GetTasks() []*Task {\n\ttasks := make([]*Task, 0, tl.Tasks.Size())\n\ttl.Tasks.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\ttasks = append(tasks, value.(*Task))\n\t\treturn false\n\t})\n\treturn tasks\n}\n\nfunc (tl *TodoList) GetTodolistOwner() address {\n\treturn tl.Owner\n}\n\nfunc (tl *TodoList) GetTodolistTitle() string {\n\treturn tl.Title\n}\n"},{"name":"todolist_test.gno","body":"package todolist\n\nimport (\n\t\"chain/runtime/unsafe\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nfunc TestNewTodoList(t *testing.T) {\n\ttitle := \"My Todo List\"\n\ttodoList := NewTodoList(title)\n\n\tuassert.Equal(t, title, todoList.GetTodolistTitle())\n\tuassert.Equal(t, 0, len(todoList.GetTasks()))\n\tuassert.Equal(t, unsafe.OriginCaller().String(), todoList.GetTodolistOwner().String())\n}\n\nfunc TestNewTask(t *testing.T) {\n\ttitle := \"My Task\"\n\ttask := NewTask(title)\n\n\tuassert.Equal(t, title, task.Title)\n\tuassert.False(t, task.Done, \"Expected task to be not done, but it is done\")\n}\n\nfunc TestAddTask(t *testing.T) {\n\ttodoList := NewTodoList(\"My Todo List\")\n\ttask := NewTask(\"My Task\")\n\n\ttodoList.AddTask(1, task)\n\n\ttasks := todoList.GetTasks()\n\n\tuassert.Equal(t, 1, len(tasks))\n\tuassert.True(t, tasks[0] == task, \"Task does not match\")\n}\n\nfunc TestToggleTaskStatus(t *testing.T) {\n\ttask := NewTask(\"My Task\")\n\n\tToggleTaskStatus(task)\n\tuassert.True(t, task.Done, \"Expected task to be done, but it is not done\")\n\n\tToggleTaskStatus(task)\n\tuassert.False(t, task.Done, \"Expected task to be done, but it is not done\")\n}\n\nfunc TestRemoveTask(t *testing.T) {\n\ttodoList := NewTodoList(\"My Todo List\")\n\ttask := NewTask(\"My Task\")\n\ttodoList.AddTask(1, task)\n\n\ttodoList.RemoveTask(\"1\")\n\n\ttasks := todoList.GetTasks()\n\tuassert.Equal(t, 0, len(tasks))\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"jwOmyKdp97klRR3q3M9z5AxZEvk2Dikl0Zu/tE/SxzkhOxHvCsHQFNEWouimA7oL+w5m97KXY17IQbI3gLuUSg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"bitset","path":"gno.land/p/jeronimoalbi/bitset","files":[{"name":"README.md","body":"# Bitset Package\n\nPackage implements an arbitrary-size bit set, also known as bit array.\n\nBit sets are useful when you need a compact way to track large sets of boolean flags,\nsuch as permissions, feature toggles, or membership sets, using significantly less\nmemory than a slice of booleans while also supporting fast bulk operations across\nentire sets.\n\n## API\n\n- `New(size uint64) BitSet` - Returns a new BitSet pre-allocated for `size` bits\n- `BitSet.Set(i uint64)` - Turn on the bit at position `i`\n- `BitSet.Clear(i uint64)` - Turn off the bit at position `i`\n- `BitSet.ClearAll()` - Turn off all bits\n- `BitSet.Compact()` - Reclaim memory by removing trailing zero words\n- `BitSet.IsSet(i uint64) bool` - Check whether the bit at position `i` is set\n- `BitSet.Size() int` - Return the total number of bits currently allocated\n- `BitSet.Len() int` - Return the number of set bits\n- `BitSet.And(other BitSet)` - In-place intersection with another set\n- `BitSet.Or(other BitSet)` - In-place union with another set\n- `BitSet.Xor(other BitSet)` - In-place symmetric difference with another set\n- `BitSet.Equal(other BitSet) bool` - Check whether two sets have the same bits set\n- `BitSet.String() string` - Return a binary (MSB-first) representation of the set\n- `BitSet.PaddedString() string` - Return a zero-padded binary (MSB-first) representation of the set\n\n## Usage\n\n[embedmd]:# (filetests/readme_filetest.gno go)\n```go\npackage main\n\nimport \"gno.land/p/jeronimoalbi/bitset\"\n\nconst (\n\tPermRead   = 0\n\tPermWrite  = 1\n\tPermDelete = 2\n\tPermAdmin  = 3\n)\n\nfunc main() {\n\tvar perms bitset.BitSet\n\tperms.Set(PermRead)\n\tperms.Set(PermWrite)\n\n\tprintln(\"Can read:\", perms.IsSet(PermRead))\n\tprintln(\"Can write:\", perms.IsSet(PermWrite))\n\tprintln(\"Can delete:\", perms.IsSet(PermDelete))\n\tprintln(\"Is admin:\", perms.IsSet(PermAdmin))\n\tprintln(\"Permissions set:\", perms.Len())\n}\n\n// Output:\n// Can read: true\n// Can write: true\n// Can delete: false\n// Is admin: false\n// Permissions set: 2\n```\n"},{"name":"bitset.gno","body":"package bitset\n\nimport \"strings\"\n\nconst bitsPerWord = 64\n\n// BitSet implements an arbitrary-size bit array.\ntype BitSet struct {\n\twords []uint64\n}\n\n// New creates a new BitSet pre-allocated to hold at least size bits.\nfunc New(size uint64) BitSet {\n\tif size == 0 {\n\t\treturn BitSet{}\n\t}\n\treturn BitSet{words: make([]uint64, wordIndex(size-1)+1)}\n}\n\n// Set turns on the bit at a given position.\nfunc (b *BitSet) Set(i uint64) {\n\tidx := wordIndex(i)\n\tif idx \u003e= len(b.words) {\n\t\tb.grow(idx + 1)\n\t}\n\tb.words[idx] |= bitMask(i) // Turn bit on\n}\n\n// Clear turns off the bit at a given position.\nfunc (b *BitSet) Clear(i uint64) {\n\tidx := wordIndex(i)\n\tif idx \u003c len(b.words) {\n\t\tb.words[idx] \u0026^= bitMask(i) // Turn bit off\n\t}\n}\n\n// ClearAll turns off all bits.\n// The size of the bitset remains unchanged.\nfunc (b *BitSet) ClearAll() {\n\tfor i := range b.words {\n\t\tb.words[i] = 0\n\t}\n}\n\n// Compact reclaims memory by removing trailing zero words.\nfunc (b *BitSet) Compact() {\n\tend := len(b.words)\n\tfor ; end \u003e 0 \u0026\u0026 b.words[end-1] == 0; end-- {\n\t}\n\twords := make([]uint64, end)\n\tcopy(words, b.words)\n\tb.words = words\n}\n\n// IsSet checks whether the bit at a given position is set.\nfunc (b BitSet) IsSet(i uint64) bool {\n\tidx := wordIndex(i)\n\tif idx \u003e= len(b.words) {\n\t\treturn false // Bit has not been set yet\n\t}\n\treturn b.words[idx]\u0026bitMask(i) != 0\n}\n\n// Size returns the total number of bits (including unset) currently stored.\n// It's the total space allocated within the set in bits.\nfunc (b BitSet) Size() int {\n\treturn len(b.words) * bitsPerWord\n}\n\n// Len returns the number of set bits.\nfunc (b BitSet) Len() int {\n\tvar count int\n\tfor _, w := range b.words {\n\t\tcount += countSetBits(w)\n\t}\n\treturn count\n}\n\n// And performs an in-place AND with other bitset.\n// Bits beyond the other bitset are cleared.\nfunc (b *BitSet) And(other BitSet) {\n\tfor i := range b.words {\n\t\tif i \u003c len(other.words) {\n\t\t\tb.words[i] \u0026= other.words[i]\n\t\t} else {\n\t\t\tb.words[i] = 0\n\t\t}\n\t}\n}\n\n// Or performs an in-place OR (union) with other bitset.\n// Current bitset grows if the other bitset is bigger.\nfunc (b *BitSet) Or(other BitSet) {\n\totherLen := len(other.words)\n\tif otherLen \u003e len(b.words) {\n\t\tb.grow(otherLen)\n\t}\n\n\tfor i := range other.words {\n\t\tb.words[i] |= other.words[i]\n\t}\n}\n\n// Xor performs an in-place XOR with other.\n// Current bitset grows if the other bitset is bigger.\nfunc (b *BitSet) Xor(other BitSet) {\n\totherLen := len(other.words)\n\tif otherLen \u003e len(b.words) {\n\t\tb.grow(otherLen)\n\t}\n\n\tfor i := range other.words {\n\t\tb.words[i] ^= other.words[i]\n\t}\n}\n\n// Equal checks whether other bitset have exactly the same bits set.\nfunc (b BitSet) Equal(other BitSet) bool {\n\tshort, long := b.words, other.words\n\tif len(short) \u003e len(long) {\n\t\tshort, long = long, short\n\t}\n\n\tfor i := range short {\n\t\tif short[i] != long[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\n\t// Any trailing words in the longer set must be zero\n\tfor i := len(short); i \u003c len(long); i++ {\n\t\tif long[i] != 0 {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// PaddedString returns a binary representation of the bitset with zero padding.\n// Representation uses MSB-first binary representation.\nfunc (b BitSet) PaddedString() string {\n\twordCount := len(b.words)\n\tif wordCount == 0 {\n\t\treturn \"\"\n\t}\n\n\tbuf := make([]byte, wordCount*bitsPerWord)\n\tfor i, w := range b.words {\n\t\tbase := (wordCount - 1 - i) * bitsPerWord\n\t\tfor j := 0; j \u003c bitsPerWord; j++ {\n\t\t\tif w\u003e\u003euint64(bitsPerWord-1-j)\u00261 == 1 {\n\t\t\t\tbuf[base+j] = '1'\n\t\t\t} else {\n\t\t\t\tbuf[base+j] = '0'\n\t\t\t}\n\t\t}\n\t}\n\treturn string(buf)\n}\n\n// String returns a binary representation of the bitset.\n// Representation uses MSB-first binary representation.\nfunc (b BitSet) String() string {\n\ts := strings.TrimLeft(b.PaddedString(), \"0\")\n\tif s == \"\" \u0026\u0026 len(b.words) \u003e 0 {\n\t\treturn \"0\"\n\t}\n\treturn s\n}\n\nfunc (b *BitSet) grow(length int) {\n\twords := make([]uint64, length)\n\tcopy(words, b.words)\n\tb.words = words\n}\n\nfunc wordIndex(i uint64) int {\n\treturn int(i / bitsPerWord)\n}\n\nfunc bitMask(i uint64) uint64 {\n\treturn 1 \u003c\u003c (i % bitsPerWord)\n}\n\nfunc countSetBits(x uint64) int {\n\t// Count the number of set bits using Kernighan's algorithm\n\tvar count int\n\tfor x != 0 {\n\t\tx \u0026= x - 1\n\t\tcount++\n\t}\n\treturn count\n}\n"},{"name":"bitset_test.gno","body":"package bitset_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/jeronimoalbi/bitset\"\n)\n\nfunc TestBitSetSet(t *testing.T) {\n\tcases := []struct {\n\t\tname string\n\t\tbits []uint64\n\t\twant string\n\t}{\n\t\t{\n\t\t\tname: \"single bit 0\",\n\t\t\tbits: []uint64{0},\n\t\t\twant: \"1\",\n\t\t},\n\t\t{\n\t\t\tname: \"single bit 5\",\n\t\t\tbits: []uint64{5},\n\t\t\twant: \"100000\",\n\t\t},\n\t\t{\n\t\t\tname: \"multiple bits in same word\",\n\t\t\tbits: []uint64{0, 3, 7},\n\t\t\twant: \"10001001\",\n\t\t},\n\t\t{\n\t\t\tname: \"bit at word boundary\",\n\t\t\tbits: []uint64{63},\n\t\t\twant: \"1000000000000000000000000000000000000000000000000000000000000000\",\n\t\t},\n\t\t{\n\t\t\tname: \"bit across word boundary\",\n\t\t\tbits: []uint64{63, 64},\n\t\t\twant: \"11000000000000000000000000000000000000000000000000000000000000000\",\n\t\t},\n\t\t{\n\t\t\tname: \"set same bit twice\",\n\t\t\tbits: []uint64{2, 2},\n\t\t\twant: \"100\",\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Arrange\n\t\t\tvar b bitset.BitSet\n\n\t\t\t// Act\n\t\t\tfor _, v := range tc.bits {\n\t\t\t\tb.Set(v)\n\t\t\t}\n\n\t\t\t// Assert\n\t\t\tif got := b.String(); got != tc.want {\n\t\t\t\tt.Errorf(\"expected %s, got %s\", tc.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBitSetIsSet(t *testing.T) {\n\tvar (\n\t\tb      bitset.BitSet\n\t\tvalues = []uint64{0, 5, 63, 64, 200}\n\t)\n\n\tfor _, v := range values {\n\t\tb.Set(v)\n\t}\n\n\tfor _, v := range values {\n\t\tif !b.IsSet(v) {\n\t\t\tt.Errorf(\"bit %d should be set\", v)\n\t\t}\n\t}\n\n\tif b.IsSet(999) {\n\t\tt.Error(\"bit 999 should not be set\")\n\t}\n}\n\nfunc TestBitSetClear(t *testing.T) {\n\tvar b bitset.BitSet\n\tb.Set(10)\n\tb.Clear(10)\n\n\tif b.IsSet(10) {\n\t\tt.Error(\"bit 10 should be cleared\")\n\t}\n\n\t// Clearing a bit beyond the slice should not panic\n\tb.Clear(999)\n}\n\nfunc TestBitSetClearAll(t *testing.T) {\n\t// Arrange\n\tvar b bitset.BitSet\n\tb.Set(1)\n\tb.Set(64)\n\tb.Set(200)\n\n\t// Act\n\tb.ClearAll()\n\n\t// Assert\n\tif b.Len() != 0 {\n\t\tt.Errorf(\"expected len 0 after ClearAll, got %d\", b.Len())\n\t}\n\n\tif b.IsSet(1) || b.IsSet(64) || b.IsSet(200) {\n\t\tt.Error(\"no bits should be set after ClearAll\")\n\t}\n}\n\nfunc TestBitSetCompact(t *testing.T) {\n\tcases := []struct {\n\t\tname     string\n\t\tsetup    func() bitset.BitSet\n\t\twantSize int\n\t}{\n\t\t{\n\t\t\tname: \"clear all\",\n\t\t\tsetup: func() bitset.BitSet {\n\t\t\t\tvar b bitset.BitSet\n\t\t\t\tb.Set(1)\n\t\t\t\tb.Set(64)\n\t\t\t\tb.Set(200)\n\t\t\t\tb.ClearAll()\n\t\t\t\treturn b\n\t\t\t},\n\t\t\twantSize: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"trailing zeros\",\n\t\t\tsetup: func() bitset.BitSet {\n\t\t\t\tvar a, other bitset.BitSet\n\t\t\t\ta.Set(1)\n\t\t\t\ta.Set(128)\n\t\t\t\tother.Set(1)\n\t\t\t\ta.And(other)\n\t\t\t\treturn a\n\t\t\t},\n\t\t\twantSize: 64,\n\t\t},\n\t\t{\n\t\t\tname: \"no trailing zeros\",\n\t\t\tsetup: func() bitset.BitSet {\n\t\t\t\tvar b bitset.BitSet\n\t\t\t\tb.Set(1)\n\t\t\t\tb.Set(64)\n\t\t\t\treturn b\n\t\t\t},\n\t\t\twantSize: 128,\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Arrange\n\t\t\tb := tc.setup()\n\n\t\t\t// Act\n\t\t\tb.Compact()\n\n\t\t\t// Assert\n\t\t\tif got := b.Size(); got != tc.wantSize {\n\t\t\t\tt.Errorf(\"expected size %d after Compact, got %d\", tc.wantSize, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBitSetSize(t *testing.T) {\n\tcases := []struct {\n\t\tname  string\n\t\tsetup func() bitset.BitSet\n\t\twant  int\n\t}{\n\t\t{\n\t\t\tname:  \"empty\",\n\t\t\tsetup: func() bitset.BitSet { return bitset.BitSet{} },\n\t\t\twant:  0,\n\t\t},\n\t\t{\n\t\t\tname:  \"new 128\",\n\t\t\tsetup: func() bitset.BitSet { return bitset.New(128) },\n\t\t\twant:  128,\n\t\t},\n\t\t{\n\t\t\tname: \"set bit 0\",\n\t\t\tsetup: func() bitset.BitSet {\n\t\t\t\tvar b bitset.BitSet\n\t\t\t\tb.Set(0)\n\t\t\t\treturn b\n\t\t\t},\n\t\t\twant: 64,\n\t\t},\n\t\t{\n\t\t\tname: \"set bit 200\",\n\t\t\tsetup: func() bitset.BitSet {\n\t\t\t\tvar b bitset.BitSet\n\t\t\t\tb.Set(200)\n\t\t\t\treturn b\n\t\t\t},\n\t\t\twant: 256,\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Arrange\n\t\t\tb := tc.setup()\n\n\t\t\t// Act\n\t\t\tgot := b.Size()\n\n\t\t\t// Assert\n\t\t\tif got != tc.want {\n\t\t\t\tt.Errorf(\"expected size %d, got %d\", tc.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBitSetLen(t *testing.T) {\n\tvar b bitset.BitSet\n\tif b.Len() != 0 {\n\t\tt.Error(\"empty bitset should have len 0\")\n\t}\n\n\tb.Set(0)\n\tb.Set(1)\n\tb.Set(100)\n\tif b.Len() != 3 {\n\t\tt.Errorf(\"expected len 3, got %d\", b.Len())\n\t}\n}\n\nfunc TestBitSetNew(t *testing.T) {\n\tb := bitset.New(0)\n\tif b.Size() != 0 {\n\t\tt.Errorf(\"expected size 0, got %d\", b.Size())\n\t}\n\n\tb = bitset.New(128)\n\tif b.Size() != 128 {\n\t\tt.Errorf(\"expected size 128, got %d\", b.Size())\n\t}\n}\n\nfunc TestBitSetOr(t *testing.T) {\n\tcases := []struct {\n\t\tname    string\n\t\tcurrent []uint64\n\t\tother   []uint64\n\t\twant    string\n\t}{\n\t\t{\n\t\t\tname:    \"empty sets\",\n\t\t\tcurrent: []uint64{},\n\t\t\tother:   []uint64{},\n\t\t\twant:    \"\",\n\t\t},\n\t\t{\n\t\t\tname:    \"equal sets\",\n\t\t\tcurrent: []uint64{1, 4, 8},\n\t\t\tother:   []uint64{1, 4, 8},\n\t\t\twant:    \"100010010\",\n\t\t},\n\t\t{\n\t\t\tname:    \"different sets\",\n\t\t\tcurrent: []uint64{1, 4, 8},\n\t\t\tother:   []uint64{2, 3, 5},\n\t\t\twant:    \"100111110\",\n\t\t},\n\t\t{\n\t\t\tname:    \"current smaller than other\",\n\t\t\tcurrent: []uint64{1, 4},\n\t\t\tother:   []uint64{2, 3, 5, 8},\n\t\t\twant:    \"100111110\",\n\t\t},\n\t\t{\n\t\t\tname:    \"current bigger than other\",\n\t\t\tcurrent: []uint64{2, 3, 5, 8},\n\t\t\tother:   []uint64{1, 4},\n\t\t\twant:    \"100111110\",\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Arrange\n\t\t\tvar a, b bitset.BitSet\n\n\t\t\tfor _, v := range tc.current {\n\t\t\t\ta.Set(v)\n\t\t\t}\n\n\t\t\tfor _, v := range tc.other {\n\t\t\t\tb.Set(v)\n\t\t\t}\n\n\t\t\t// Act\n\t\t\ta.Or(b)\n\n\t\t\t// Assert\n\t\t\tif got := a.String(); got != tc.want {\n\t\t\t\tt.Errorf(\"expected %s, got %s\", tc.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBitSetAnd(t *testing.T) {\n\tcases := []struct {\n\t\tname    string\n\t\tcurrent []uint64\n\t\tother   []uint64\n\t\twant    string\n\t}{\n\t\t{\n\t\t\tname:    \"empty sets\",\n\t\t\tcurrent: []uint64{},\n\t\t\tother:   []uint64{},\n\t\t\twant:    \"\",\n\t\t},\n\t\t{\n\t\t\tname:    \"equal sets\",\n\t\t\tcurrent: []uint64{1, 4, 8},\n\t\t\tother:   []uint64{1, 4, 8},\n\t\t\twant:    \"100010010\",\n\t\t},\n\t\t{\n\t\t\tname:    \"different sets\",\n\t\t\tcurrent: []uint64{1, 4, 8},\n\t\t\tother:   []uint64{2, 3, 5},\n\t\t\twant:    \"0\",\n\t\t},\n\t\t{\n\t\t\tname:    \"current smaller than other\",\n\t\t\tcurrent: []uint64{1, 4},\n\t\t\tother:   []uint64{1, 4, 5, 8},\n\t\t\twant:    \"10010\",\n\t\t},\n\t\t{\n\t\t\tname:    \"current bigger than other\",\n\t\t\tcurrent: []uint64{0, 3, 5, 8},\n\t\t\tother:   []uint64{0, 3},\n\t\t\twant:    \"1001\",\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Arrange\n\t\t\tvar a, b bitset.BitSet\n\n\t\t\tfor _, v := range tc.current {\n\t\t\t\ta.Set(v)\n\t\t\t}\n\n\t\t\tfor _, v := range tc.other {\n\t\t\t\tb.Set(v)\n\t\t\t}\n\n\t\t\t// Act\n\t\t\ta.And(b)\n\n\t\t\t// Assert\n\t\t\tif got := a.String(); got != tc.want {\n\t\t\t\tt.Errorf(\"expected %s, got %s\", tc.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBitSetXor(t *testing.T) {\n\tcases := []struct {\n\t\tname    string\n\t\tcurrent []uint64\n\t\tother   []uint64\n\t\twant    string\n\t}{\n\t\t{\n\t\t\tname:    \"empty sets\",\n\t\t\tcurrent: []uint64{},\n\t\t\tother:   []uint64{},\n\t\t\twant:    \"\",\n\t\t},\n\t\t{\n\t\t\tname:    \"equal sets\",\n\t\t\tcurrent: []uint64{1, 4, 8},\n\t\t\tother:   []uint64{1, 4, 8},\n\t\t\twant:    \"0\",\n\t\t},\n\t\t{\n\t\t\tname:    \"different sets\",\n\t\t\tcurrent: []uint64{1, 3, 8},\n\t\t\tother:   []uint64{2, 3, 5},\n\t\t\twant:    \"100100110\",\n\t\t},\n\t\t{\n\t\t\tname:    \"current smaller than other\",\n\t\t\tcurrent: []uint64{1, 3, 4},\n\t\t\tother:   []uint64{2, 3, 5, 8},\n\t\t\twant:    \"100110110\",\n\t\t},\n\t\t{\n\t\t\tname:    \"current bigger than other\",\n\t\t\tcurrent: []uint64{2, 3, 5, 8},\n\t\t\tother:   []uint64{1, 3},\n\t\t\twant:    \"100100110\",\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Arrange\n\t\t\tvar a, b bitset.BitSet\n\n\t\t\tfor _, v := range tc.current {\n\t\t\t\ta.Set(v)\n\t\t\t}\n\n\t\t\tfor _, v := range tc.other {\n\t\t\t\tb.Set(v)\n\t\t\t}\n\n\t\t\t// Act\n\t\t\ta.Xor(b)\n\n\t\t\t// Assert\n\t\t\tif got := a.String(); got != tc.want {\n\t\t\t\tt.Errorf(\"expected %s, got %s\", tc.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBitSetEqual(t *testing.T) {\n\tcases := []struct {\n\t\tname  string\n\t\tsetup func() (a, b bitset.BitSet)\n\t\twant  bool\n\t}{\n\t\t{\n\t\t\tname:  \"empty\",\n\t\t\tsetup: func() (a, b bitset.BitSet) { return a, b },\n\t\t\twant:  true,\n\t\t},\n\t\t{\n\t\t\tname: \"equal\",\n\t\t\tsetup: func() (a, b bitset.BitSet) {\n\t\t\t\ta.Set(1)\n\t\t\t\ta.Set(100)\n\t\t\t\tb.Set(1)\n\t\t\t\tb.Set(100)\n\t\t\t\treturn a, b\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"equal with different sizes\",\n\t\t\tsetup: func() (a, b bitset.BitSet) {\n\t\t\t\tb = bitset.New(256)\n\t\t\t\tb.Set(5)\n\t\t\t\ta.Set(5)\n\t\t\t\treturn a, b\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"not equal\",\n\t\t\tsetup: func() (a, b bitset.BitSet) {\n\t\t\t\ta.Set(1)\n\t\t\t\tb.Set(1)\n\t\t\t\tb.Set(50)\n\t\t\t\treturn a, b\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Arrange\n\t\t\ta, b := tc.setup()\n\n\t\t\t// Act\n\t\t\tgot := a.Equal(b)\n\n\t\t\t// Assert\n\t\t\tif tc.want == got {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif tc.want {\n\t\t\t\tt.Error(\"bitsets should be equal\")\n\t\t\t} else {\n\t\t\t\tt.Error(\"bitsets should not be equal\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBitSetString(t *testing.T) {\n\tcases := []struct {\n\t\tname   string\n\t\tvalues []uint64\n\t\twant   string\n\t}{\n\t\t{\"empty\", nil, \"\"},\n\t\t{\"single bit\", []uint64{0}, \"1\"},\n\t\t{\"max word value\", []uint64{63}, \"1000000000000000000000000000000000000000000000000000000000000000\"},\n\t\t{\"multiple sequential bits\", []uint64{0, 1, 2, 3}, \"1111\"},\n\t\t{\"multiple non sequential bits\", []uint64{0, 2, 4}, \"10101\"},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Arrange\n\t\t\tvar b bitset.BitSet\n\t\t\tfor _, v := range tc.values {\n\t\t\t\tb.Set(v)\n\t\t\t}\n\n\t\t\t// Act\n\t\t\tgot := b.String()\n\n\t\t\t// Assert\n\t\t\tif got != tc.want {\n\t\t\t\tt.Errorf(\"expected %q, got %q\", tc.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBitSetPaddedString(t *testing.T) {\n\tcases := []struct {\n\t\tname   string\n\t\tvalues []uint64\n\t\twant   string\n\t}{\n\t\t{\"empty\", nil, \"\"},\n\t\t{\"single bit\", []uint64{0}, \"0000000000000000000000000000000000000000000000000000000000000001\"},\n\t\t{\"max word value\", []uint64{63}, \"1000000000000000000000000000000000000000000000000000000000000000\"},\n\t\t{\"multiple sequential bits\", []uint64{0, 1, 2, 3}, \"0000000000000000000000000000000000000000000000000000000000001111\"},\n\t\t{\"multiple non sequential bits\", []uint64{0, 2, 4}, \"0000000000000000000000000000000000000000000000000000000000010101\"},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Arrange\n\t\t\tvar b bitset.BitSet\n\t\t\tfor _, v := range tc.values {\n\t\t\t\tb.Set(v)\n\t\t\t}\n\n\t\t\t// Act\n\t\t\tgot := b.PaddedString()\n\n\t\t\t// Assert\n\t\t\tif got != tc.want {\n\t\t\t\tt.Errorf(\"expected %q, got %q\", tc.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/jeronimoalbi/bitset\"\ngno = \"0.9\"\n"},{"name":"readme_filetest.gno","body":"package main\n\nimport \"gno.land/p/jeronimoalbi/bitset\"\n\nconst (\n\tPermRead   = 0\n\tPermWrite  = 1\n\tPermDelete = 2\n\tPermAdmin  = 3\n)\n\nfunc main() {\n\tvar perms bitset.BitSet\n\tperms.Set(PermRead)\n\tperms.Set(PermWrite)\n\n\tprintln(\"Can read:\", perms.IsSet(PermRead))\n\tprintln(\"Can write:\", perms.IsSet(PermWrite))\n\tprintln(\"Can delete:\", perms.IsSet(PermDelete))\n\tprintln(\"Is admin:\", perms.IsSet(PermAdmin))\n\tprintln(\"Permissions set:\", perms.Len())\n}\n\n// Output:\n// Can read: true\n// Can write: true\n// Can delete: false\n// Is admin: false\n// Permissions set: 2\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"aqqNlp3wwYuSyrNS07IZCZjLsXAOOTONqpodJwtUR48e5c/Hb1w1rLk4CTmvC88vrQosGeJRoYgNe84CC5pYYA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"datasource","path":"gno.land/p/jeronimoalbi/datasource","files":[{"name":"datasource.gno","body":"// Package datasource defines generic interfaces for datasources.\n//\n// Datasources contain a set of records which can optionally be\n// taggable. Tags can optionally be used to filter records by taxonomy.\n//\n// Datasources can help in cases where the data sent during\n// communication between different realms needs to be generic\n// to avoid direct dependencies.\npackage datasource\n\nimport \"errors\"\n\n// ErrInvalidRecord indicates that a datasource contains invalid records.\nvar ErrInvalidRecord = errors.New(\"datasource records is not valid\")\n\ntype (\n\t// Fields defines an interface for read-only fields.\n\tFields interface {\n\t\t// Has checks whether a field exists.\n\t\tHas(name string) bool\n\n\t\t// Get retrieves the value associated with the given field.\n\t\t// Returns nil if the field does not exist.\n\t\tGet(name string) any\n\t}\n\n\t// Record defines a datasource record.\n\tRecord interface {\n\t\t// ID returns the unique record's identifier.\n\t\tID() string\n\n\t\t// String returns a string representation of the record.\n\t\tString() string\n\n\t\t// Fields returns record fields and values.\n\t\tFields() (Fields, error)\n\t}\n\n\t// TaggableRecord defines a datasource record that supports tags.\n\t// Tags can be used to build a taxonomy to filter records by category.\n\tTaggableRecord interface {\n\t\t// Tags returns a list of tags for the record.\n\t\tTags() []string\n\t}\n\n\t// ContentRecord defines a datasource record that can return content.\n\tContentRecord interface {\n\t\t// Content returns the record content.\n\t\tContent() (string, error)\n\t}\n\n\t// Iterator defines an iterator of datasource records.\n\tIterator interface {\n\t\t// Next returns true when a new record is available.\n\t\tNext() bool\n\n\t\t// Err returns any error raised when reading records.\n\t\tErr() error\n\n\t\t// Record returns the current record.\n\t\tRecord() Record\n\t}\n\n\t// Datasource defines a generic datasource.\n\tDatasource interface {\n\t\t// Records returns a new datasource records iterator.\n\t\tRecords(Query) Iterator\n\n\t\t// Size returns the total number of records in the datasource.\n\t\t// When -1 is returned it means datasource doesn't support size.\n\t\tSize() int\n\n\t\t// Record returns a single datasource record.\n\t\tRecord(id string) (Record, error)\n\t}\n)\n\n// NewIterator returns a new record iterator for a datasource query.\nfunc NewIterator(ds Datasource, options ...QueryOption) Iterator {\n\treturn ds.Records(NewQuery(options...))\n}\n\n// QueryRecords return a slice of records for a datasource query.\nfunc QueryRecords(ds Datasource, options ...QueryOption) ([]Record, error) {\n\tvar (\n\t\trecords []Record\n\t\tquery   = NewQuery(options...)\n\t\titer    = ds.Records(query)\n\t)\n\n\tfor i := 0; i \u003c query.Count \u0026\u0026 iter.Next(); i++ {\n\t\tr := iter.Record()\n\t\tif r == nil {\n\t\t\treturn nil, ErrInvalidRecord\n\t\t}\n\n\t\trecords = append(records, r)\n\t}\n\n\tif err := iter.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn records, nil\n}\n"},{"name":"datasource_test.gno","body":"package datasource\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestNewIterator(t *testing.T) {\n\tcases := []struct {\n\t\tname    string\n\t\trecords []Record\n\t\terr     error\n\t}{\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\trecords: []Record{\n\t\t\t\ttestRecord{id: \"1\"},\n\t\t\t\ttestRecord{id: \"2\"},\n\t\t\t\ttestRecord{id: \"3\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"error\",\n\t\t\terr:  errors.New(\"test\"),\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Arrange\n\t\t\tds := testDatasource{\n\t\t\t\trecords: tc.records,\n\t\t\t\terr:     tc.err,\n\t\t\t}\n\n\t\t\t// Act\n\t\t\titer := NewIterator(ds)\n\n\t\t\t// Assert\n\t\t\tif tc.err != nil {\n\t\t\t\tuassert.ErrorIs(t, tc.err, iter.Err())\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tuassert.NoError(t, iter.Err())\n\n\t\t\tfor i := 0; iter.Next(); i++ {\n\t\t\t\tr := iter.Record()\n\t\t\t\turequire.NotEqual(t, nil, r, \"valid record\")\n\t\t\t\turequire.True(t, i \u003c len(tc.records), \"iteration count\")\n\t\t\t\tuassert.Equal(t, tc.records[i].ID(), r.ID())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestQueryRecords(t *testing.T) {\n\tcases := []struct {\n\t\tname        string\n\t\trecords     []Record\n\t\trecordCount int\n\t\toptions     []QueryOption\n\t\terr         error\n\t}{\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\trecords: []Record{\n\t\t\t\ttestRecord{id: \"1\"},\n\t\t\t\ttestRecord{id: \"2\"},\n\t\t\t\ttestRecord{id: \"3\"},\n\t\t\t},\n\t\t\trecordCount: 3,\n\t\t},\n\t\t{\n\t\t\tname:    \"with count\",\n\t\t\toptions: []QueryOption{WithCount(2)},\n\t\t\trecords: []Record{\n\t\t\t\ttestRecord{id: \"1\"},\n\t\t\t\ttestRecord{id: \"2\"},\n\t\t\t\ttestRecord{id: \"3\"},\n\t\t\t},\n\t\t\trecordCount: 2,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid record\",\n\t\t\trecords: []Record{\n\t\t\t\ttestRecord{id: \"1\"},\n\t\t\t\tnil,\n\t\t\t\ttestRecord{id: \"3\"},\n\t\t\t},\n\t\t\terr: ErrInvalidRecord,\n\t\t},\n\t\t{\n\t\t\tname: \"iterator error\",\n\t\t\trecords: []Record{\n\t\t\t\ttestRecord{id: \"1\"},\n\t\t\t\ttestRecord{id: \"3\"},\n\t\t\t},\n\t\t\terr: errors.New(\"test\"),\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Arrange\n\t\t\tds := testDatasource{\n\t\t\t\trecords: tc.records,\n\t\t\t\terr:     tc.err,\n\t\t\t}\n\n\t\t\t// Act\n\t\t\trecords, err := QueryRecords(ds, tc.options...)\n\n\t\t\t// Assert\n\t\t\tif tc.err != nil {\n\t\t\t\tuassert.ErrorIs(t, tc.err, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tuassert.NoError(t, err)\n\n\t\t\turequire.Equal(t, tc.recordCount, len(records), \"record count\")\n\t\t\tfor i, r := range records {\n\t\t\t\turequire.NotEqual(t, nil, r, \"valid record\")\n\t\t\t\tuassert.Equal(t, tc.records[i].ID(), r.ID())\n\t\t\t}\n\t\t})\n\t}\n}\n\ntype testDatasource struct {\n\trecords []Record\n\terr     error\n}\n\nfunc (testDatasource) Size() int                     { return -1 }\nfunc (testDatasource) Record(string) (Record, error) { return nil, nil }\nfunc (ds testDatasource) Records(Query) Iterator     { return \u0026testIter{records: ds.records, err: ds.err} }\n\ntype testRecord struct {\n\tid     string\n\tfields Fields\n\terr    error\n}\n\nfunc (r testRecord) ID() string              { return r.id }\nfunc (r testRecord) String() string          { return \"str\" + r.id }\nfunc (r testRecord) Fields() (Fields, error) { return r.fields, r.err }\n\ntype testIter struct {\n\tindex   int\n\trecords []Record\n\tcurrent Record\n\terr     error\n}\n\nfunc (it testIter) Err() error     { return it.err }\nfunc (it testIter) Record() Record { return it.current }\n\nfunc (it *testIter) Next() bool {\n\tcount := len(it.records)\n\tif it.err != nil || count == 0 || it.index \u003e= count {\n\t\treturn false\n\t}\n\tit.current = it.records[it.index]\n\tit.index++\n\treturn true\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/jeronimoalbi/datasource\"\ngno = \"0.9\"\n"},{"name":"query.gno","body":"package datasource\n\nimport \"gno.land/p/nt/avl/v0\"\n\n// DefaultQueryRecords defines the default number of records returned by queries.\nconst DefaultQueryRecords = 50\n\ntype (\n\t// QueryOption configures datasource queries.\n\tQueryOption func(*Query)\n\n\t// Query contains datasource query options.\n\tQuery struct {\n\t\t// Offset of the first record to return during iteration.\n\t\tOffset int\n\n\t\t// Count contains the number to records that query should return.\n\t\tCount int\n\n\t\t// Tag contains a tag to use as filter for the records.\n\t\tTag string\n\n\t\t// Filters contains optional query filters by field value.\n\t\tFilters avl.Tree\n\t}\n)\n\n// WithOffset configures query to return records starting from an offset.\nfunc WithOffset(offset int) QueryOption {\n\treturn func(q *Query) {\n\t\tq.Offset = offset\n\t}\n}\n\n// WithCount configures the number of records that query returns.\nfunc WithCount(count int) QueryOption {\n\treturn func(q *Query) {\n\t\tif count \u003c 1 {\n\t\t\tcount = DefaultQueryRecords\n\t\t}\n\t\tq.Count = count\n\t}\n}\n\n// ByTag configures query to filter by tag.\nfunc ByTag(tag string) QueryOption {\n\treturn func(q *Query) {\n\t\tq.Tag = tag\n\t}\n}\n\n// WithFilter assigns a new filter argument to a query.\n// This option can be used multiple times if more than one\n// filter has to be given to the query.\nfunc WithFilter(field string, value any) QueryOption {\n\treturn func(q *Query) {\n\t\tq.Filters.Set(field, value)\n\t}\n}\n\n// NewQuery creates a new datasource query.\nfunc NewQuery(options ...QueryOption) Query {\n\t// Construct a fresh Query in the caller's realm; copying from a\n\t// package-level default would propagate the /p/-readonly taint\n\t// and break the apply(\u0026q) options below.\n\tq := Query{Count: DefaultQueryRecords}\n\tfor _, apply := range options {\n\t\tapply(\u0026q)\n\t}\n\treturn q\n}\n"},{"name":"query_test.gno","body":"package datasource\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nfunc TestNewQuery(t *testing.T) {\n\tcases := []struct {\n\t\tname    string\n\t\toptions []QueryOption\n\t\tsetup   func() Query\n\t}{\n\t\t{\n\t\t\tname: \"default\",\n\t\t\tsetup: func() Query {\n\t\t\t\treturn Query{Count: DefaultQueryRecords}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"with offset\",\n\t\t\toptions: []QueryOption{WithOffset(100)},\n\t\t\tsetup: func() Query {\n\t\t\t\treturn Query{\n\t\t\t\t\tOffset: 100,\n\t\t\t\t\tCount:  DefaultQueryRecords,\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"with count\",\n\t\t\toptions: []QueryOption{WithCount(10)},\n\t\t\tsetup: func() Query {\n\t\t\t\treturn Query{Count: 10}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"with invalid count\",\n\t\t\toptions: []QueryOption{WithCount(0)},\n\t\t\tsetup: func() Query {\n\t\t\t\treturn Query{Count: DefaultQueryRecords}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"by tag\",\n\t\t\toptions: []QueryOption{ByTag(\"foo\")},\n\t\t\tsetup: func() Query {\n\t\t\t\treturn Query{\n\t\t\t\t\tTag:   \"foo\",\n\t\t\t\t\tCount: DefaultQueryRecords,\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"with filter\",\n\t\t\toptions: []QueryOption{WithFilter(\"foo\", 42)},\n\t\t\tsetup: func() Query {\n\t\t\t\tq := Query{Count: DefaultQueryRecords}\n\t\t\t\tq.Filters.Set(\"foo\", 42)\n\t\t\t\treturn q\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"with multiple filters\",\n\t\t\toptions: []QueryOption{\n\t\t\t\tWithFilter(\"foo\", 42),\n\t\t\t\tWithFilter(\"bar\", \"baz\"),\n\t\t\t},\n\t\t\tsetup: func() Query {\n\t\t\t\tq := Query{Count: DefaultQueryRecords}\n\t\t\t\tq.Filters.Set(\"foo\", 42)\n\t\t\t\tq.Filters.Set(\"bar\", \"baz\")\n\t\t\t\treturn q\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Arrange\n\t\t\twant := tc.setup()\n\n\t\t\t// Act\n\t\t\tq := NewQuery(tc.options...)\n\n\t\t\t// Assert\n\t\t\tuassert.Equal(t, want.Offset, q.Offset)\n\t\t\tuassert.Equal(t, want.Count, q.Count)\n\t\t\tuassert.Equal(t, want.Tag, q.Tag)\n\t\t\tuassert.Equal(t, want.Filters.Size(), q.Filters.Size())\n\n\t\t\twant.Filters.Iterate(\"\", \"\", func(k string, v any) bool {\n\t\t\t\tgot := q.Filters.Get(k)\n\t\t\t\tuassert.NotNil(t, got)\n\t\t\t\tif got != nil {\n\t\t\t\t\tuassert.Equal(t, fmt.Sprint(v), fmt.Sprint(got))\n\t\t\t\t}\n\t\t\t\treturn false\n\t\t\t})\n\t\t})\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"igmoPGQaQqNRte+Hdq++gMWWAoaIPVFF7UBde69K6r0vfMPqSu1ZtUkATDldVpEAe8GyLRAMJfoj0R4wIYgaGw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","package":{"name":"collection","path":"gno.land/p/moul/collection","files":[{"name":"collection.gno","body":"// Package collection provides a generic collection implementation with support for\n// multiple indexes, including unique indexes and case-insensitive indexes.\n// It is designed to be used with any type and allows efficient lookups using\n// different fields or computed values.\n//\n// Example usage:\n//\n//\t// Define a data type\n//\ttype User struct {\n//\t    Name     string\n//\t    Email    string\n//\t    Age      int\n//\t    Username string\n//\t    Tags     []string\n//\t}\n//\n//\t// Create a new collection\n//\tc := collection.New()\n//\n//\t// Add indexes with different options\n//\tc.AddIndex(\"name\", func(v any) string {\n//\t    return v.(*User).Name\n//\t}, UniqueIndex)\n//\n//\tc.AddIndex(\"email\", func(v any) string {\n//\t    return v.(*User).Email\n//\t}, UniqueIndex|CaseInsensitiveIndex)\n//\n//\tc.AddIndex(\"age\", func(v any) string {\n//\t    return strconv.Itoa(v.(*User).Age)\n//\t}, DefaultIndex)  // Non-unique index\n//\n//\tc.AddIndex(\"username\", func(v any) string {\n//\t    return v.(*User).Username\n//\t}, UniqueIndex|SparseIndex)  // Allow empty usernames\n//\n//\t// For tags, we index all tags for the user\n//\tc.AddIndex(\"tag\", func(v any) []string {\n//\t    return v.(*User).Tags\n//\t}, DefaultIndex)  // Non-unique to allow multiple users with same tag\n//\n//\t// Store an object\n//\tid := c.Set(\u0026User{\n//\t    Name:  \"Alice\",\n//\t    Email: \"alice@example.com\",\n//\t    Age:   30,\n//\t    Tags:  []string{\"admin\", \"moderator\"},  // User can have multiple tags\n//\t})\n//\n//\t// Retrieve by any index\n//\tentry := c.GetFirst(\"email\", \"alice@example.com\")\n//\tadminUsers := c.GetAll(\"tag\", \"admin\")      // Find all users with admin tag\n//\tmodUsers := c.GetAll(\"tag\", \"moderator\")    // Find all users with moderator tag\n//\n// Index options can be combined using the bitwise OR operator.\n// Available options:\n//   - DefaultIndex: Regular index with no special behavior\n//   - UniqueIndex: Ensures values are unique within the index\n//   - CaseInsensitiveIndex: Makes string comparisons case-insensitive\n//   - SparseIndex: Skips indexing empty values (nil or empty string)\n//\n// Example: UniqueIndex|CaseInsensitiveIndex for a case-insensitive unique index\npackage collection\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n)\n\n// New creates a new Collection instance with an initialized ID index.\n// The ID index is a special unique index that is always present and\n// serves as the primary key for all objects in the collection.\nfunc New() *Collection {\n\tc := \u0026Collection{\n\t\tindexes: make(map[string]*Index),\n\t\tidGen:   seqid.ID(0),\n\t}\n\t// Initialize _id index\n\tc.indexes[IDIndex] = \u0026Index{\n\t\toptions: UniqueIndex,\n\t\ttree:    avl.NewTree(),\n\t}\n\treturn c\n}\n\n// Collection represents a collection of objects with multiple indexes\ntype Collection struct {\n\tindexes map[string]*Index\n\tidGen   seqid.ID\n}\n\nconst (\n\t// IDIndex is the reserved name for the primary key index\n\tIDIndex = \"_id\"\n)\n\n// IndexOption represents configuration options for an index using bit flags\ntype IndexOption uint64\n\nconst (\n\t// DefaultIndex is a basic index with no special options\n\tDefaultIndex IndexOption = 0\n\n\t// UniqueIndex ensures no duplicate values are allowed\n\tUniqueIndex IndexOption = 1 \u003c\u003c iota\n\n\t// CaseInsensitiveIndex automatically converts string values to lowercase\n\tCaseInsensitiveIndex\n\n\t// SparseIndex only indexes non-empty values\n\tSparseIndex\n)\n\n// Index represents an index with its configuration and data.\n// The index function can return either:\n//   - string: for single-value indexes\n//   - []string: for multi-value indexes where one object can be indexed under multiple keys\n//\n// The backing tree stores either a single ID or []string for multiple IDs per key.\ntype Index struct {\n\tfn      any\n\toptions IndexOption\n\ttree    avl.ITree\n}\n\n// AddIndex adds a new index to the collection with the specified options\n//\n// Parameters:\n//   - name: the unique name of the index (e.g., \"tags\")\n//   - indexFn: a function that extracts either a string or []string from an object\n//   - options: bit flags for index configuration (e.g., UniqueIndex)\nfunc (c *Collection) AddIndex(name string, indexFn any, options IndexOption) {\n\tif name == IDIndex {\n\t\tpanic(\"_id is a reserved index name\")\n\t}\n\tc.indexes[name] = \u0026Index{\n\t\tfn:      indexFn,\n\t\toptions: options,\n\t\ttree:    avl.NewTree(),\n\t}\n}\n\n// storeIndex handles how we store an ID in the index tree\nfunc (idx *Index) store(key string, idStr string) {\n\tstored := idx.tree.Get(key)\n\tif stored == nil {\n\t\t// First entry for this key\n\t\tidx.tree.Set(key, idStr)\n\t\treturn\n\t}\n\n\t// Handle existing entries\n\tswitch existing := stored.(type) {\n\tcase string:\n\t\tif existing == idStr {\n\t\t\treturn // Already stored\n\t\t}\n\t\t// Convert to array\n\t\tidx.tree.Set(key, []string{existing, idStr})\n\tcase []string:\n\t\t// Check if ID already exists\n\t\tfor _, id := range existing {\n\t\t\tif id == idStr {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\t// Append new ID\n\t\tidx.tree.Set(key, append(existing, idStr))\n\t}\n}\n\n// removeIndex handles how we remove an ID from the index tree\nfunc (idx *Index) remove(key string, idStr string) {\n\tstored := idx.tree.Get(key)\n\tif stored == nil {\n\t\treturn\n\t}\n\n\tswitch existing := stored.(type) {\n\tcase string:\n\t\tif existing == idStr {\n\t\t\tidx.tree.Remove(key)\n\t\t}\n\tcase []string:\n\t\tnewIds := make([]string, 0, len(existing))\n\t\tfor _, id := range existing {\n\t\t\tif id != idStr {\n\t\t\t\tnewIds = append(newIds, id)\n\t\t\t}\n\t\t}\n\t\tif len(newIds) == 0 {\n\t\t\tidx.tree.Remove(key)\n\t\t} else if len(newIds) == 1 {\n\t\t\tidx.tree.Set(key, newIds[0])\n\t\t} else {\n\t\t\tidx.tree.Set(key, newIds)\n\t\t}\n\t}\n}\n\n// generateKeys extracts one or more keys from an object for a given index.\nfunc generateKeys(idx *Index, obj any) ([]string, bool) {\n\tif obj == nil {\n\t\treturn nil, false\n\t}\n\n\tswitch fnTyped := idx.fn.(type) {\n\tcase func(any) string:\n\t\t// Single-value index\n\t\tkey := fnTyped(obj)\n\t\treturn []string{key}, true\n\tcase func(any) []string:\n\t\t// Multi-value index\n\t\tkeys := fnTyped(obj)\n\t\treturn keys, true\n\tdefault:\n\t\tpanic(\"invalid index function type\")\n\t}\n}\n\n// Set adds or updates an object in the collection.\n// Returns a positive ID if successful.\n// Returns 0 if:\n//   - The object is nil\n//   - A uniqueness constraint would be violated\n//   - Index generation fails for any index\nfunc (c *Collection) Set(obj any) uint64 {\n\tif obj == nil {\n\t\treturn 0\n\t}\n\n\t// Generate new ID\n\tid := c.idGen.Next()\n\tidStr := id.String()\n\n\t// Check uniqueness constraints first\n\tfor name, idx := range c.indexes {\n\t\tif name == IDIndex {\n\t\t\tcontinue\n\t\t}\n\t\tkeys, ok := generateKeys(idx, obj)\n\t\tif !ok {\n\t\t\treturn 0\n\t\t}\n\n\t\tfor _, key := range keys {\n\t\t\t// Skip empty values for sparse indexes\n\t\t\tif idx.options\u0026SparseIndex != 0 \u0026\u0026 key == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif idx.options\u0026CaseInsensitiveIndex != 0 {\n\t\t\t\tkey = strings.ToLower(key)\n\t\t\t}\n\t\t\t// Only check uniqueness for unique + single-value indexes\n\t\t\t// (UniqueIndex is ambiguous; skipping that scenario)\n\t\t\tif idx.options\u0026UniqueIndex != 0 {\n\t\t\t\tif idx.tree.Has(key) {\n\t\t\t\t\treturn 0\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Store in _id index first (the actual object)\n\tc.indexes[IDIndex].tree.Set(idStr, obj)\n\n\t// Store in all other indexes\n\tfor name, idx := range c.indexes {\n\t\tif name == IDIndex {\n\t\t\tcontinue\n\t\t}\n\t\tkeys, ok := generateKeys(idx, obj)\n\t\tif !ok {\n\t\t\t// Rollback: remove from _id index\n\t\t\tc.indexes[IDIndex].tree.Remove(idStr)\n\t\t\treturn 0\n\t\t}\n\n\t\tfor _, key := range keys {\n\t\t\tif idx.options\u0026SparseIndex != 0 \u0026\u0026 key == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif idx.options\u0026CaseInsensitiveIndex != 0 {\n\t\t\t\tkey = strings.ToLower(key)\n\t\t\t}\n\t\t\tidx.store(key, idStr)\n\t\t}\n\t}\n\n\treturn uint64(id)\n}\n\n// Get retrieves entries matching the given key in the specified index.\n// Returns an iterator over the matching entries.\nfunc (c *Collection) Get(indexName string, key string) EntryIterator {\n\tidx, exists := c.indexes[indexName]\n\tif !exists {\n\t\treturn EntryIterator{err: errors.New(\"index not found: \" + indexName)}\n\t}\n\n\tif idx.options\u0026CaseInsensitiveIndex != 0 {\n\t\tkey = strings.ToLower(key)\n\t}\n\n\tif indexName == IDIndex {\n\t\t// For ID index, validate the ID format first\n\t\t_, err := seqid.FromString(key)\n\t\tif err != nil {\n\t\t\treturn EntryIterator{err: err}\n\t\t}\n\t}\n\n\treturn EntryIterator{\n\t\tcollection: c,\n\t\tindexName:  indexName,\n\t\tkey:        key,\n\t}\n}\n\n// GetFirst returns the first matching entry or nil if none found\nfunc (c *Collection) GetFirst(indexName, key string) *Entry {\n\titer := c.Get(indexName, key)\n\tif iter.Next() {\n\t\treturn iter.Value()\n\t}\n\treturn nil\n}\n\n// Delete removes an object by its ID and returns true if something was deleted\nfunc (c *Collection) Delete(id uint64) bool {\n\tidStr := seqid.ID(id).String()\n\n\t// Get the object first to clean up other indexes\n\tobj := c.indexes[IDIndex].tree.Get(idStr)\n\tif obj == nil {\n\t\treturn false\n\t}\n\n\t// Remove from all indexes\n\tfor name, idx := range c.indexes {\n\t\tif name == IDIndex {\n\t\t\tidx.tree.Remove(idStr)\n\t\t\tcontinue\n\t\t}\n\t\tkeys, ok := generateKeys(idx, obj)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, key := range keys {\n\t\t\tif idx.options\u0026CaseInsensitiveIndex != 0 {\n\t\t\t\tkey = strings.ToLower(key)\n\t\t\t}\n\t\t\tidx.remove(key, idStr)\n\t\t}\n\t}\n\treturn true\n}\n\n// Update updates an existing object and returns true if successful\n// Returns true if the update was successful.\n// Returns false if:\n//   - The object is nil\n//   - The ID doesn't exist\n//   - A uniqueness constraint would be violated\n//   - Index generation fails for any index\n//\n// If the update fails, the collection remains unchanged.\nfunc (c *Collection) Update(id uint64, obj any) bool {\n\tif obj == nil {\n\t\treturn false\n\t}\n\tidStr := seqid.ID(id).String()\n\toldObj := c.indexes[IDIndex].tree.Get(idStr)\n\tif oldObj == nil {\n\t\treturn false\n\t}\n\n\t// Check unique constraints\n\tfor name, idx := range c.indexes {\n\t\tif name == IDIndex {\n\t\t\tcontinue\n\t\t}\n\n\t\tif idx.options\u0026UniqueIndex != 0 {\n\t\t\tnewKeys, newOk := generateKeys(idx, obj)\n\t\t\t_, oldOk := generateKeys(idx, oldObj)\n\t\t\tif !newOk || !oldOk {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\tfor _, newKey := range newKeys {\n\t\t\t\tif idx.options\u0026CaseInsensitiveIndex != 0 {\n\t\t\t\t\tnewKey = strings.ToLower(newKey)\n\t\t\t\t}\n\n\t\t\t\tfound := idx.tree.Get(newKey)\n\t\t\t\tif found != nil {\n\t\t\t\t\tif storedID, ok := found.(string); !ok || storedID != idStr {\n\t\t\t\t\t\treturn false\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Store old index entries for potential rollback\n\toldEntries := make(map[string][]string)\n\tfor name, idx := range c.indexes {\n\t\tif name == IDIndex {\n\t\t\tcontinue\n\t\t}\n\t\toldKeys, ok := generateKeys(idx, oldObj)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tvar adjusted []string\n\t\tfor _, okey := range oldKeys {\n\t\t\tif idx.options\u0026CaseInsensitiveIndex != 0 {\n\t\t\t\tokey = strings.ToLower(okey)\n\t\t\t}\n\t\t\t// Remove the oldObj from the index right away\n\t\t\tidx.remove(okey, idStr)\n\t\t\tadjusted = append(adjusted, okey)\n\t\t}\n\t\toldEntries[name] = adjusted\n\t}\n\n\t// Update the object in the _id index\n\tc.indexes[IDIndex].tree.Set(idStr, obj)\n\n\t// Add new index entries\n\tfor name, idx := range c.indexes {\n\t\tif name == IDIndex {\n\t\t\tcontinue\n\t\t}\n\t\tnewKeys, ok := generateKeys(idx, obj)\n\t\tif !ok {\n\t\t\t// Rollback: restore old object and old index entries\n\t\t\tc.indexes[IDIndex].tree.Set(idStr, oldObj)\n\t\t\tfor idxName, keys := range oldEntries {\n\t\t\t\tfor _, oldKey := range keys {\n\t\t\t\t\tc.indexes[idxName].store(oldKey, idStr)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t}\n\t\tfor _, nkey := range newKeys {\n\t\t\tif idx.options\u0026CaseInsensitiveIndex != 0 {\n\t\t\t\tnkey = strings.ToLower(nkey)\n\t\t\t}\n\t\t\tidx.store(nkey, idStr)\n\t\t}\n\t}\n\n\treturn true\n}\n\n// GetAll retrieves all entries matching the given key in the specified index.\nfunc (c *Collection) GetAll(indexName string, key string) []Entry {\n\tidx, exists := c.indexes[indexName]\n\tif !exists {\n\t\treturn nil\n\t}\n\n\tif idx.options\u0026CaseInsensitiveIndex != 0 {\n\t\tkey = strings.ToLower(key)\n\t}\n\n\tif indexName == IDIndex {\n\t\tif obj := idx.tree.Get(key); obj != nil {\n\t\t\treturn []Entry{{ID: key, Obj: obj}}\n\t\t}\n\t\treturn nil\n\t}\n\n\tidData := idx.tree.Get(key)\n\tif idData == nil {\n\t\treturn nil\n\t}\n\n\t// Handle both single and multi-value cases based on the actual data type\n\tswitch stored := idData.(type) {\n\tcase []string:\n\t\tresult := make([]Entry, 0, len(stored))\n\t\tfor _, idStr := range stored {\n\t\t\tif obj := c.indexes[IDIndex].tree.Get(idStr); obj != nil {\n\t\t\t\tresult = append(result, Entry{ID: idStr, Obj: obj})\n\t\t\t}\n\t\t}\n\t\treturn result\n\tcase string:\n\t\tif obj := c.indexes[IDIndex].tree.Get(stored); obj != nil {\n\t\t\treturn []Entry{{ID: stored, Obj: obj}}\n\t\t}\n\t}\n\treturn nil\n}\n\n// GetIndex returns the underlying tree for an index\nfunc (c *Collection) GetIndex(name string) avl.ITree {\n\tidx, exists := c.indexes[name]\n\tif !exists {\n\t\treturn nil\n\t}\n\treturn idx.tree\n}\n"},{"name":"collection_test.gno","body":"package collection\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\ntype Person struct {\n\tName     string\n\tAge      int\n\tEmail    string\n\tUsername string\n\tTags     []string\n}\n\nfunc (p Person) String() string {\n\treturn ufmt.Sprintf(\"name=%s age=%d email=%s username=%s tags=%s\",\n\t\tp.Name, p.Age, p.Email, p.Username, strings.Join(p.Tags, \",\"))\n}\n\n// TestOperation represents a single operation in a test sequence\ntype TestOperation struct {\n\top      string // \"set\" or \"update\"\n\tperson  *Person\n\tid      uint64 // for updates\n\twantID  uint64\n\twantErr bool\n}\n\n// TestCase represents a complete test case with setup and operations\ntype TestCase struct {\n\tname       string\n\tsetupIndex func(*Collection)\n\toperations []TestOperation\n}\n\nfunc TestBasicOperations(t *testing.T) {\n\tc := New()\n\n\t// Add indexes\n\tc.AddIndex(\"name\", func(v any) string {\n\t\treturn v.(*Person).Name\n\t}, UniqueIndex)\n\tc.AddIndex(\"age\", func(v any) string {\n\t\treturn strconv.Itoa(v.(*Person).Age)\n\t}, DefaultIndex)\n\n\t// Test basic Set and Get\n\tp1 := \u0026Person{Name: \"Alice\", Age: 30, Email: \"alice@test.com\"}\n\tid1 := c.Set(p1)\n\tif id1 == 0 {\n\t\tt.Error(\"Failed to set first object\")\n\t}\n\n\t// Get by ID\n\titer := c.Get(IDIndex, seqid.ID(id1).String())\n\tif !iter.Next() {\n\t\tt.Error(\"Failed to get object by ID\")\n\t}\n\tentry := iter.Value()\n\tif entry.Obj.(*Person).Name != \"Alice\" {\n\t\tt.Error(\"Got wrong object\")\n\t}\n}\n\nfunc TestUniqueConstraints(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tsetup  func(*Collection) uint64\n\t\twantID bool\n\t}{\n\t\t{\n\t\t\tname: \"First person\",\n\t\t\tsetup: func(c *Collection) uint64 {\n\t\t\t\treturn c.Set(\u0026Person{Name: \"Alice\"})\n\t\t\t},\n\t\t\twantID: true,\n\t\t},\n\t\t{\n\t\t\tname: \"Duplicate name\",\n\t\t\tsetup: func(c *Collection) uint64 {\n\t\t\t\tc.Set(\u0026Person{Name: \"Alice\"})\n\t\t\t\treturn c.Set(\u0026Person{Name: \"Alice\"})\n\t\t\t},\n\t\t\twantID: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Same age (non-unique index)\",\n\t\t\tsetup: func(c *Collection) uint64 {\n\t\t\t\tc.AddIndex(\"age\", func(v any) string {\n\t\t\t\t\treturn strconv.Itoa(v.(*Person).Age)\n\t\t\t\t}, DefaultIndex)\n\t\t\t\tc.Set(\u0026Person{Name: \"Alice\", Age: 30})\n\t\t\t\treturn c.Set(\u0026Person{Name: \"Bob\", Age: 30})\n\t\t\t},\n\t\t\twantID: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := New()\n\t\t\tc.AddIndex(\"name\", func(v any) string {\n\t\t\t\treturn v.(*Person).Name\n\t\t\t}, UniqueIndex)\n\n\t\t\tid := tt.setup(c)\n\t\t\tif (id != 0) != tt.wantID {\n\t\t\t\tt.Errorf(\"Set() got id = %v, want non-zero: %v\", id, tt.wantID)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUpdates(t *testing.T) {\n\tc := New()\n\tc.AddIndex(\"name\", func(v any) string {\n\t\treturn v.(*Person).Name\n\t}, UniqueIndex)\n\tc.AddIndex(\"username\", func(v any) string {\n\t\treturn strings.ToLower(v.(*Person).Username)\n\t}, UniqueIndex|CaseInsensitiveIndex)\n\n\t// Initial setup\n\tp1 := \u0026Person{Name: \"Alice\", Username: \"alice123\"}\n\tp2 := \u0026Person{Name: \"Bob\", Username: \"bob456\"}\n\n\tid1 := c.Set(p1)\n\tc.Set(p2)\n\n\ttests := []struct {\n\t\tname      string\n\t\tid        uint64\n\t\tnewPerson *Person\n\t\twantRet   bool\n\t}{\n\t\t{\n\t\t\tname:      \"Update to non-conflicting values\",\n\t\t\tid:        id1,\n\t\t\tnewPerson: \u0026Person{Name: \"Alice2\", Username: \"alice1234\"},\n\t\t\twantRet:   true,\n\t\t},\n\t\t{\n\t\t\tname:      \"Update to conflicting username\",\n\t\t\tid:        id1,\n\t\t\tnewPerson: \u0026Person{Name: \"Alice2\", Username: \"bob456\"},\n\t\t\twantRet:   false,\n\t\t},\n\t\t{\n\t\t\tname:      \"Update non-existent ID\",\n\t\t\tid:        99999,\n\t\t\tnewPerson: \u0026Person{Name: \"Test\", Username: \"test\"},\n\t\t\twantRet:   false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgotID := c.Update(tt.id, tt.newPerson)\n\t\t\tif gotID != tt.wantRet {\n\t\t\t\tt.Errorf(\"Update() got = %v, want %v\", gotID, tt.wantRet)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDelete(t *testing.T) {\n\tc := New()\n\tc.AddIndex(\"name\", func(v any) string {\n\t\treturn v.(*Person).Name\n\t}, UniqueIndex)\n\n\tp1 := \u0026Person{Name: \"Alice\"}\n\tid1 := c.Set(p1)\n\n\ttests := []struct {\n\t\tname    string\n\t\tid      uint64\n\t\twantRet bool\n\t}{\n\t\t{\n\t\t\tname:    \"Delete existing object\",\n\t\t\tid:      id1,\n\t\t\twantRet: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"Delete non-existent object\",\n\t\t\tid:      99999,\n\t\t\twantRet: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"Delete already deleted object\",\n\t\t\tid:      id1,\n\t\t\twantRet: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgotID := c.Delete(tt.id)\n\t\t\tif gotID != tt.wantRet {\n\t\t\t\tt.Errorf(\"Delete() got = %v, want %v\", gotID, tt.wantRet)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEdgeCases(t *testing.T) {\n\tc := New()\n\tc.AddIndex(\"name\", func(v any) string {\n\t\treturn v.(*Person).Name\n\t}, UniqueIndex)\n\n\ttests := []struct {\n\t\tname      string\n\t\toperation func() bool\n\t\twantPanic bool\n\t}{\n\t\t{\n\t\t\tname: \"Set nil object\",\n\t\t\toperation: func() bool {\n\t\t\t\treturn c.Set(nil) != 0\n\t\t\t},\n\t\t\twantPanic: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Set wrong type\",\n\t\t\toperation: func() bool {\n\t\t\t\treturn c.Set(\"not a person\") != 0\n\t\t\t},\n\t\t\twantPanic: true,\n\t\t},\n\t\t{\n\t\t\tname: \"Update with nil\",\n\t\t\toperation: func() bool {\n\t\t\t\tid := c.Set(\u0026Person{Name: \"Test\"})\n\t\t\t\treturn c.Update(id, nil)\n\t\t\t},\n\t\t\twantPanic: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Get with invalid index name\",\n\t\t\toperation: func() bool {\n\t\t\t\titer := c.Get(\"invalid_index\", \"key\")\n\t\t\t\tif iter.Empty() {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\tentry := iter.Value()\n\t\t\t\tif entry == nil {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\t_, err := seqid.FromString(entry.ID)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\treturn true\n\t\t\t},\n\t\t\twantPanic: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar got bool\n\t\t\tpanicked := false\n\n\t\t\tfunc() {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t\tpanicked = true\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t\tgot = tt.operation()\n\t\t\t}()\n\n\t\t\tif panicked != tt.wantPanic {\n\t\t\t\tt.Errorf(\"Operation panicked = %v, want panic = %v\", panicked, tt.wantPanic)\n\t\t\t}\n\t\t\tif !panicked \u0026\u0026 got != false {\n\t\t\t\tt.Errorf(\"Operation returned %v, want 0\", got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIndexTypes(t *testing.T) {\n\tc := New()\n\n\t// Test different types of indexes\n\tc.AddIndex(\"composite\", func(v any) string {\n\t\tp := v.(*Person)\n\t\treturn p.Name + \":\" + strconv.Itoa(p.Age)\n\t}, UniqueIndex)\n\n\tc.AddIndex(\"case_insensitive\", func(v any) string {\n\t\treturn strings.ToLower(v.(*Person).Username)\n\t}, UniqueIndex|CaseInsensitiveIndex)\n\n\t// Test composite index\n\tp1 := \u0026Person{Name: \"Alice\", Age: 30, Username: \"Alice123\"}\n\tid1 := c.Set(p1)\n\tif id1 == 0 {\n\t\tt.Error(\"Failed to set object with composite index\")\n\t}\n\n\t// Test case-insensitive index\n\tp2 := \u0026Person{Name: \"Bob\", Age: 25, Username: \"alice123\"}\n\tid2 := c.Set(p2)\n\tif id2 != 0 {\n\t\tt.Error(\"Case-insensitive index failed to prevent duplicate\")\n\t}\n}\n\nfunc TestIndexOptions(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tsetup   func(*Collection) uint64\n\t\twantID  bool\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"Unique case-sensitive index\",\n\t\t\tsetup: func(c *Collection) uint64 {\n\t\t\t\tc.AddIndex(\"username\", func(v any) string {\n\t\t\t\t\treturn v.(*Person).Username\n\t\t\t\t}, UniqueIndex)\n\n\t\t\t\tc.Set(\u0026Person{Username: \"Alice\"})\n\t\t\t\treturn c.Set(\u0026Person{Username: \"Alice\"}) // Should fail\n\t\t\t},\n\t\t\twantID: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Unique case-insensitive index\",\n\t\t\tsetup: func(c *Collection) uint64 {\n\t\t\t\tc.AddIndex(\"email\", func(v any) string {\n\t\t\t\t\treturn v.(*Person).Email\n\t\t\t\t}, UniqueIndex|CaseInsensitiveIndex)\n\n\t\t\t\tc.Set(\u0026Person{Email: \"test@example.com\"})\n\t\t\t\treturn c.Set(\u0026Person{Email: \"TEST@EXAMPLE.COM\"}) // Should fail\n\t\t\t},\n\t\t\twantID: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Default index\",\n\t\t\tsetup: func(c *Collection) uint64 {\n\t\t\t\tc.AddIndex(\"age\", func(v any) string {\n\t\t\t\t\treturn strconv.Itoa(v.(*Person).Age)\n\t\t\t\t}, DefaultIndex)\n\n\t\t\t\t// First person with age 30\n\t\t\t\tid1 := c.Set(\u0026Person{Age: 30})\n\t\t\t\tif id1 == 0 {\n\t\t\t\t\tt.Error(\"Failed to set first person\")\n\t\t\t\t}\n\n\t\t\t\t// Second person with same age should succeed\n\t\t\t\treturn c.Set(\u0026Person{Age: 30})\n\t\t\t},\n\t\t\twantID: true,\n\t\t},\n\t\t{\n\t\t\tname: \"Multiple options\",\n\t\t\tsetup: func(c *Collection) uint64 {\n\t\t\t\tc.AddIndex(\"name\", func(v any) string {\n\t\t\t\t\treturn v.(*Person).Name\n\t\t\t\t}, UniqueIndex|CaseInsensitiveIndex|SparseIndex)\n\n\t\t\t\tc.Set(\u0026Person{Name: \"Alice\"})\n\t\t\t\treturn c.Set(\u0026Person{Name: \"ALICE\"}) // Should fail\n\t\t\t},\n\t\t\twantID: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := New() // Create new collection for each test\n\t\t\tid := tt.setup(c)\n\t\t\tif (id != 0) != tt.wantID {\n\t\t\t\tt.Errorf(\"got id = %v, want non-zero: %v\", id, tt.wantID)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestConcurrentOperations(t *testing.T) {\n\tc := New()\n\tc.AddIndex(\"name\", func(v any) string {\n\t\treturn v.(*Person).Name\n\t}, UniqueIndex)\n\n\tp1 := \u0026Person{Name: \"Alice\"}\n\tid1 := c.Set(p1)\n\titer := c.Get(\"_id\", seqid.ID(id1).String())\n\tsuccess := c.Update(id1, \u0026Person{Name: \"Alice2\"})\n\n\tif iter.Empty() || !success {\n\t\tt.Error(\"Concurrent operations failed\")\n\t}\n}\n\nfunc TestSparseIndexBehavior(t *testing.T) {\n\tc := New()\n\tc.AddIndex(\"optional_field\", func(v any) string {\n\t\treturn v.(*Person).Username\n\t}, SparseIndex)\n\n\ttests := []struct {\n\t\tname   string\n\t\tperson *Person\n\t\twantID bool\n\t}{\n\t\t{\n\t\t\tname:   \"Empty optional field\",\n\t\t\tperson: \u0026Person{Name: \"Alice\", Email: \"alice@test.com\"},\n\t\t\twantID: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"Populated optional field\",\n\t\t\tperson: \u0026Person{Name: \"Bob\", Email: \"bob@test.com\", Username: \"bobby\"},\n\t\t\twantID: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"Multiple empty fields\",\n\t\t\tperson: \u0026Person{Name: \"Charlie\"},\n\t\t\twantID: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tid := c.Set(tt.person)\n\t\t\tif (id != 0) != tt.wantID {\n\t\t\t\tt.Errorf(\"Set() got id = %v, want non-zero: %v\", id, tt.wantID)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIndexKeyGeneration(t *testing.T) {\n\tc := New()\n\tc.AddIndex(\"composite\", func(v any) string {\n\t\tp := v.(*Person)\n\t\treturn p.Name + \":\" + strconv.Itoa(p.Age)\n\t}, UniqueIndex)\n\n\ttests := []struct {\n\t\tname   string\n\t\tperson *Person\n\t\twantID bool\n\t}{\n\t\t{\n\t\t\tname:   \"Valid composite key\",\n\t\t\tperson: \u0026Person{Name: \"Alice\", Age: 30},\n\t\t\twantID: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"Duplicate composite key\",\n\t\t\tperson: \u0026Person{Name: \"Alice\", Age: 30},\n\t\t\twantID: false,\n\t\t},\n\t\t{\n\t\t\tname:   \"Different composite key\",\n\t\t\tperson: \u0026Person{Name: \"Alice\", Age: 31},\n\t\t\twantID: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tid := c.Set(tt.person)\n\t\t\tif (id != 0) != tt.wantID {\n\t\t\t\tt.Errorf(\"Set() got id = %v, want non-zero: %v\", id, tt.wantID)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetIndex(t *testing.T) {\n\tc := New()\n\tc.AddIndex(\"name\", func(v any) string {\n\t\treturn v.(*Person).Name\n\t}, UniqueIndex)\n\n\ttests := []struct {\n\t\tname      string\n\t\tindexName string\n\t\twantNil   bool\n\t}{\n\t\t{\n\t\t\tname:      \"Get existing index\",\n\t\t\tindexName: \"name\",\n\t\t\twantNil:   false,\n\t\t},\n\t\t{\n\t\t\tname:      \"Get _id index\",\n\t\t\tindexName: IDIndex,\n\t\t\twantNil:   false,\n\t\t},\n\t\t{\n\t\t\tname:      \"Get non-existent index\",\n\t\t\tindexName: \"invalid\",\n\t\t\twantNil:   true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttree := c.GetIndex(tt.indexName)\n\t\t\tif (tree == nil) != tt.wantNil {\n\t\t\t\tt.Errorf(\"GetIndex() got nil = %v, want nil = %v\", tree == nil, tt.wantNil)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAddIndexPanic(t *testing.T) {\n\tc := New()\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Error(\"Expected panic when adding _id index\")\n\t\t}\n\t}()\n\n\tc.AddIndex(IDIndex, func(v any) string {\n\t\treturn \"\"\n\t}, DefaultIndex)\n}\n\nfunc TestCaseInsensitiveOperations(t *testing.T) {\n\tc := New()\n\tc.AddIndex(\"email\", func(v any) string {\n\t\treturn v.(*Person).Email\n\t}, UniqueIndex|CaseInsensitiveIndex)\n\n\tp := \u0026Person{Email: \"Test@Example.com\"}\n\tid := c.Set(p)\n\n\ttests := []struct {\n\t\tname      string\n\t\tkey       string\n\t\twantObj   bool\n\t\toperation string // \"get\" or \"getAll\"\n\t\twantCount int\n\t}{\n\t\t{\"Get exact match\", \"Test@Example.com\", true, \"get\", 1},\n\t\t{\"Get different case\", \"test@example.COM\", true, \"get\", 1},\n\t\t{\"Get non-existent\", \"other@example.com\", false, \"get\", 0},\n\t\t{\"GetAll exact match\", \"Test@Example.com\", true, \"getAll\", 1},\n\t\t{\"GetAll different case\", \"test@example.COM\", true, \"getAll\", 1},\n\t\t{\"GetAll non-existent\", \"other@example.com\", false, \"getAll\", 0},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.operation == \"get\" {\n\t\t\t\titer := c.Get(\"email\", tt.key)\n\t\t\t\tif iter.Empty() {\n\t\t\t\t\tif tt.wantObj {\n\t\t\t\t\t\tt.Error(\"Expected iterator to not be empty\")\n\t\t\t\t\t}\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\thasValue := iter.Next()\n\t\t\t\tif hasValue != tt.wantObj {\n\t\t\t\t\tt.Errorf(\"Get() got object = %v, want object = %v\", hasValue, tt.wantObj)\n\t\t\t\t}\n\t\t\t\tif hasValue {\n\t\t\t\t\tentry := iter.Value()\n\t\t\t\t\tif entry.ID != seqid.ID(id).String() {\n\t\t\t\t\t\tt.Errorf(\"Get() got id = %v, want id = %v\", entry.ID, seqid.ID(id).String())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tentries := c.GetAll(\"email\", tt.key)\n\t\t\t\tif len(entries) != tt.wantCount {\n\t\t\t\t\tt.Errorf(\"GetAll() returned %d entries, want %d\", len(entries), tt.wantCount)\n\t\t\t\t}\n\t\t\t\tif tt.wantCount \u003e 0 {\n\t\t\t\t\tentry := entries[0]\n\t\t\t\t\tif entry.ID != seqid.ID(id).String() {\n\t\t\t\t\t\tt.Errorf(\"GetAll() returned ID %s, want %s\", entry.ID, seqid.ID(id).String())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetInvalidID(t *testing.T) {\n\tc := New()\n\titer := c.Get(IDIndex, \"not-a-valid-id\")\n\tif !iter.Empty() {\n\t\tt.Errorf(\"Get() with invalid ID format got an entry, want nil\")\n\t}\n}\n\nfunc TestGetAll(t *testing.T) {\n\tc := New()\n\tc.AddIndex(\"tags\", func(v any) []string {\n\t\treturn v.(*Person).Tags\n\t}, DefaultIndex)\n\tc.AddIndex(\"age\", func(v any) string {\n\t\treturn strconv.Itoa(v.(*Person).Age)\n\t}, DefaultIndex)\n\tc.AddIndex(\"name\", func(v any) string {\n\t\treturn v.(*Person).Name\n\t}, UniqueIndex)\n\n\t// Create test data\n\tpeople := []*Person{\n\t\t{Name: \"Alice\", Age: 30, Tags: []string{\"dev\", \"go\"}},\n\t\t{Name: \"Bob\", Age: 30, Tags: []string{\"dev\", \"python\"}},\n\t\t{Name: \"Charlie\", Age: 25, Tags: []string{\"dev\", \"rust\"}},\n\t}\n\n\tids := make([]uint64, len(people))\n\tfor i, p := range people {\n\t\tids[i] = c.Set(p)\n\t\tif ids[i] == 0 {\n\t\t\tt.Fatalf(\"Failed to set person %s\", p.Name)\n\t\t}\n\t}\n\n\ttests := []struct {\n\t\tname      string\n\t\tindexName string\n\t\tkey       string\n\t\twantCount int\n\t}{\n\t\t{\n\t\t\tname:      \"Multi-value index with multiple matches\",\n\t\t\tindexName: \"tags\",\n\t\t\tkey:       \"dev\",\n\t\t\twantCount: 3,\n\t\t},\n\t\t{\n\t\t\tname:      \"Multi-value index with single match\",\n\t\t\tindexName: \"tags\",\n\t\t\tkey:       \"go\",\n\t\t\twantCount: 1,\n\t\t},\n\t\t{\n\t\t\tname:      \"Multi-value index with no matches\",\n\t\t\tindexName: \"tags\",\n\t\t\tkey:       \"java\",\n\t\t\twantCount: 0,\n\t\t},\n\t\t{\n\t\t\tname:      \"Single-value non-unique index with multiple matches\",\n\t\t\tindexName: \"age\",\n\t\t\tkey:       \"30\",\n\t\t\twantCount: 2,\n\t\t},\n\t\t{\n\t\t\tname:      \"Single-value unique index\",\n\t\t\tindexName: \"name\",\n\t\t\tkey:       \"Alice\",\n\t\t\twantCount: 1,\n\t\t},\n\t\t{\n\t\t\tname:      \"Non-existent index\",\n\t\t\tindexName: \"invalid\",\n\t\t\tkey:       \"value\",\n\t\t\twantCount: 0,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\titer := c.Get(tt.indexName, tt.key)\n\t\t\tcount := 0\n\t\t\tfor iter.Next() {\n\t\t\t\tentry := iter.Value()\n\t\t\t\tif entry.ID == \"\" {\n\t\t\t\t\tt.Error(\"Got entry with empty ID\")\n\t\t\t\t}\n\t\t\t\tif entry.Obj == nil {\n\t\t\t\t\tt.Error(\"Got entry with nil Obj\")\n\t\t\t\t}\n\t\t\t\tcount++\n\t\t\t}\n\t\t\tif count != tt.wantCount {\n\t\t\t\tt.Errorf(\"Got %d entries, want %d\", count, tt.wantCount)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIndexOperations(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tsetup   func(*Collection) (uint64, error)\n\t\tverify  func(*Collection, uint64) error\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"Basic set and get\",\n\t\t\tsetup: func(c *Collection) (uint64, error) {\n\t\t\t\tc.AddIndex(\"name\", func(v any) string {\n\t\t\t\t\treturn v.(*Person).Name\n\t\t\t\t}, UniqueIndex)\n\t\t\t\treturn c.Set(\u0026Person{Name: \"Alice\", Age: 30}), nil\n\t\t\t},\n\t\t\tverify: func(c *Collection, id uint64) error {\n\t\t\t\titer := c.Get(IDIndex, seqid.ID(id).String())\n\t\t\t\tif !iter.Next() {\n\t\t\t\t\treturn errors.New(\"failed to get object by ID\")\n\t\t\t\t}\n\t\t\t\tentry := iter.Value()\n\t\t\t\tif entry.Obj.(*Person).Name != \"Alice\" {\n\t\t\t\t\treturn errors.New(\"got wrong object\")\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Composite index\",\n\t\t\tsetup: func(c *Collection) (uint64, error) {\n\t\t\t\tc.AddIndex(\"composite\", func(v any) string {\n\t\t\t\t\tp := v.(*Person)\n\t\t\t\t\treturn p.Name + \":\" + strconv.Itoa(p.Age)\n\t\t\t\t}, UniqueIndex)\n\t\t\t\treturn c.Set(\u0026Person{Name: \"Alice\", Age: 30}), nil\n\t\t\t},\n\t\t\tverify: func(c *Collection, id uint64) error {\n\t\t\t\titer := c.Get(\"composite\", \"Alice:30\")\n\t\t\t\tif !iter.Next() {\n\t\t\t\t\treturn errors.New(\"failed to get object by composite index\")\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t// Add more test cases combining unique scenarios from original tests\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := New()\n\t\t\tid, err := tt.setup(c)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"setup error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err == nil {\n\t\t\t\tif err := tt.verify(c, id); err != nil {\n\t\t\t\t\tt.Errorf(\"verification failed: %v\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMultiValueIndexes(t *testing.T) {\n\tc := New()\n\tc.AddIndex(\"tags\", func(v any) []string {\n\t\treturn v.(*Person).Tags\n\t}, DefaultIndex)\n\n\ttests := []struct {\n\t\tname      string\n\t\tsetup     []*Person\n\t\tsearchTag string\n\t\twantCount int\n\t}{\n\t\t{\n\t\t\tname: \"Multiple tags, multiple matches\",\n\t\t\tsetup: []*Person{\n\t\t\t\t{Name: \"Alice\", Tags: []string{\"dev\", \"go\"}},\n\t\t\t\t{Name: \"Bob\", Tags: []string{\"dev\", \"python\"}},\n\t\t\t\t{Name: \"Charlie\", Tags: []string{\"dev\", \"rust\"}},\n\t\t\t},\n\t\t\tsearchTag: \"dev\",\n\t\t\twantCount: 3,\n\t\t},\n\t\t{\n\t\t\tname: \"Single tag match\",\n\t\t\tsetup: []*Person{\n\t\t\t\t{Name: \"Alice\", Tags: []string{\"dev\", \"go\"}},\n\t\t\t\t{Name: \"Bob\", Tags: []string{\"dev\", \"python\"}},\n\t\t\t},\n\t\t\tsearchTag: \"go\",\n\t\t\twantCount: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"No matches\",\n\t\t\tsetup: []*Person{\n\t\t\t\t{Name: \"Alice\", Tags: []string{\"dev\", \"go\"}},\n\t\t\t\t{Name: \"Bob\", Tags: []string{\"dev\", \"python\"}},\n\t\t\t},\n\t\t\tsearchTag: \"java\",\n\t\t\twantCount: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"Empty tags\",\n\t\t\tsetup: []*Person{\n\t\t\t\t{Name: \"Alice\", Tags: []string{}},\n\t\t\t\t{Name: \"Bob\", Tags: nil},\n\t\t\t},\n\t\t\tsearchTag: \"dev\",\n\t\t\twantCount: 0,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := New()\n\t\t\tc.AddIndex(\"tags\", func(v any) []string {\n\t\t\t\treturn v.(*Person).Tags\n\t\t\t}, DefaultIndex)\n\n\t\t\t// Setup test data\n\t\t\tfor _, p := range tt.setup {\n\t\t\t\tif id := c.Set(p); id == 0 {\n\t\t\t\t\tt.Fatalf(\"Failed to set person %s\", p.Name)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Test Get operation\n\t\t\titer := c.Get(\"tags\", tt.searchTag)\n\t\t\tcount := 0\n\t\t\tfor iter.Next() {\n\t\t\t\tcount++\n\t\t\t}\n\t\t\tif count != tt.wantCount {\n\t\t\t\tt.Errorf(\"Get() got %d matches, want %d\", count, tt.wantCount)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetOperations(t *testing.T) {\n\tc := New()\n\tc.AddIndex(\"name\", func(v any) string {\n\t\treturn v.(*Person).Name\n\t}, UniqueIndex)\n\tc.AddIndex(\"age\", func(v any) string {\n\t\treturn strconv.Itoa(v.(*Person).Age)\n\t}, DefaultIndex)\n\n\t// Setup test data\n\ttestPeople := []*Person{\n\t\t{Name: \"Alice\", Age: 30},\n\t\t{Name: \"Bob\", Age: 30},\n\t\t{Name: \"Charlie\", Age: 25},\n\t}\n\n\tids := make([]uint64, len(testPeople))\n\tfor i, p := range testPeople {\n\t\tids[i] = c.Set(p)\n\t\tif ids[i] == 0 {\n\t\t\tt.Fatalf(\"Failed to set person %s\", p.Name)\n\t\t}\n\t}\n\n\ttests := []struct {\n\t\tname      string\n\t\tindexName string\n\t\tkey       string\n\t\twantCount int\n\t\twantErr   bool\n\t}{\n\t\t{\n\t\t\tname:      \"Get by ID\",\n\t\t\tindexName: IDIndex,\n\t\t\tkey:       seqid.ID(ids[0]).String(),\n\t\t\twantCount: 1,\n\t\t\twantErr:   false,\n\t\t},\n\t\t{\n\t\t\tname:      \"Get by unique index\",\n\t\t\tindexName: \"name\",\n\t\t\tkey:       \"Alice\",\n\t\t\twantCount: 1,\n\t\t\twantErr:   false,\n\t\t},\n\t\t{\n\t\t\tname:      \"Get by non-unique index\",\n\t\t\tindexName: \"age\",\n\t\t\tkey:       \"30\",\n\t\t\twantCount: 2,\n\t\t\twantErr:   false,\n\t\t},\n\t\t{\n\t\t\tname:      \"Get with invalid index\",\n\t\t\tindexName: \"invalid_index\",\n\t\t\tkey:       \"value\",\n\t\t\twantCount: 0,\n\t\t\twantErr:   true,\n\t\t},\n\t\t{\n\t\t\tname:      \"Get with invalid ID format\",\n\t\t\tindexName: IDIndex,\n\t\t\tkey:       \"not-a-valid-id\",\n\t\t\twantCount: 0,\n\t\t\twantErr:   true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\titer := c.Get(tt.indexName, tt.key)\n\t\t\tif iter.Empty() {\n\t\t\t\tif !tt.wantErr {\n\t\t\t\t\tt.Errorf(\"Get() returned empty iterator, wanted %d results\", tt.wantCount)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tcount := 0\n\t\t\tfor iter.Next() {\n\t\t\t\tentry := iter.Value()\n\t\t\t\tif entry.ID == \"\" {\n\t\t\t\t\tt.Error(\"Got entry with empty ID\")\n\t\t\t\t}\n\t\t\t\tif entry.Obj == nil {\n\t\t\t\t\tt.Error(\"Got entry with nil Obj\")\n\t\t\t\t}\n\t\t\t\tcount++\n\t\t\t}\n\n\t\t\tif count != tt.wantCount {\n\t\t\t\tt.Errorf(\"Get() returned %d results, want %d\", count, tt.wantCount)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEntryString(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tentry    *Entry\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"Nil entry\",\n\t\t\tentry:    nil,\n\t\t\texpected: \"\u003cnil\u003e\",\n\t\t},\n\t\t{\n\t\t\tname: \"Person entry\",\n\t\t\tentry: \u0026Entry{\n\t\t\t\tID:  \"123\",\n\t\t\t\tObj: \u0026Person{Name: \"Alice\", Age: 30},\n\t\t\t},\n\t\t\texpected: `Entry{ID: 123, Obj: name=Alice age=30 email= username= tags=}`,\n\t\t},\n\t\t{\n\t\t\tname: \"Entry with nil object\",\n\t\t\tentry: \u0026Entry{\n\t\t\t\tID:  \"456\",\n\t\t\t\tObj: nil,\n\t\t\t},\n\t\t\texpected: `Entry{ID: 456, Obj: \u003cnil\u003e}`,\n\t\t},\n\t\t{\n\t\t\tname: \"Entry with complete person\",\n\t\t\tentry: \u0026Entry{\n\t\t\t\tID: \"789\",\n\t\t\t\tObj: \u0026Person{\n\t\t\t\t\tName:     \"Bob\",\n\t\t\t\t\tAge:      25,\n\t\t\t\t\tEmail:    \"bob@example.com\",\n\t\t\t\t\tUsername: \"bobby\",\n\t\t\t\t\tTags:     []string{\"dev\", \"go\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: `Entry{ID: 789, Obj: name=Bob age=25 email=bob@example.com username=bobby tags=dev,go}`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := tt.entry.String()\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"Entry.String() = %q, want %q\", got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n"},{"name":"entry.gno","body":"package collection\n\nimport \"gno.land/p/nt/ufmt/v0\"\n\n// Entry represents a single object in the collection with its ID\ntype Entry struct {\n\tID  string\n\tObj any\n}\n\n// String returns a string representation of the Entry\nfunc (e *Entry) String() string {\n\tif e == nil {\n\t\treturn \"\u003cnil\u003e\"\n\t}\n\treturn ufmt.Sprintf(\"Entry{ID: %s, Obj: %v}\", e.ID, e.Obj)\n}\n\n// EntryIterator provides iteration over collection entries\ntype EntryIterator struct {\n\tcollection *Collection\n\tindexName  string\n\tkey        string\n\tcurrentID  string\n\tcurrentObj any\n\terr        error\n\tclosed     bool\n\n\t// For multi-value cases\n\tids        []string\n\tcurrentIdx int\n}\n\nfunc (ei *EntryIterator) Close() error {\n\tei.closed = true\n\tei.currentID = \"\"\n\tei.currentObj = nil\n\tei.ids = nil\n\treturn nil\n}\n\nfunc (ei *EntryIterator) Next() bool {\n\tif ei == nil || ei.closed || ei.err != nil {\n\t\treturn false\n\t}\n\n\t// Handle ID index specially\n\tif ei.indexName == IDIndex {\n\t\tif ei.currentID != \"\" { // We've already returned the single value\n\t\t\treturn false\n\t\t}\n\t\tobj := ei.collection.indexes[IDIndex].tree.Get(ei.key)\n\t\tif obj == nil {\n\t\t\treturn false\n\t\t}\n\t\tei.currentID = ei.key\n\t\tei.currentObj = obj\n\t\treturn true\n\t}\n\n\t// Get the index\n\tidx, exists := ei.collection.indexes[ei.indexName]\n\tif !exists {\n\t\treturn false\n\t}\n\n\t// Initialize ids slice if needed\n\tif ei.ids == nil {\n\t\tidData := idx.tree.Get(ei.key)\n\t\tif idData == nil {\n\t\t\treturn false\n\t\t}\n\n\t\tswitch stored := idData.(type) {\n\t\tcase []string:\n\t\t\tei.ids = stored\n\t\t\tei.currentIdx = -1\n\t\tcase string:\n\t\t\tei.ids = []string{stored}\n\t\t\tei.currentIdx = -1\n\t\tdefault:\n\t\t\treturn false\n\t\t}\n\t}\n\n\t// Move to next ID\n\tei.currentIdx++\n\tif ei.currentIdx \u003e= len(ei.ids) {\n\t\treturn false\n\t}\n\n\t// Fetch the actual object\n\tei.currentID = ei.ids[ei.currentIdx]\n\tobj := ei.collection.indexes[IDIndex].tree.Get(ei.currentID)\n\tif obj == nil {\n\t\t// Skip invalid entries\n\t\treturn ei.Next()\n\t}\n\tei.currentObj = obj\n\treturn true\n}\n\nfunc (ei *EntryIterator) Error() error {\n\treturn ei.err\n}\n\nfunc (ei *EntryIterator) Value() *Entry {\n\tif ei == nil || ei.closed || ei.currentID == \"\" {\n\t\treturn nil\n\t}\n\treturn \u0026Entry{\n\t\tID:  ei.currentID,\n\t\tObj: ei.currentObj,\n\t}\n}\n\nfunc (ei *EntryIterator) Empty() bool {\n\tif ei == nil || ei.closed || ei.err != nil {\n\t\treturn true\n\t}\n\n\t// Handle ID index specially\n\tif ei.indexName == IDIndex {\n\t\treturn !ei.collection.indexes[IDIndex].tree.Has(ei.key)\n\t}\n\n\t// Get the index\n\tidx, exists := ei.collection.indexes[ei.indexName]\n\tif !exists {\n\t\treturn true\n\t}\n\n\t// Check if key exists in index\n\tidData := idx.tree.Get(ei.key)\n\tif idData == nil {\n\t\treturn true\n\t}\n\n\t// Check if there are any valid IDs\n\tswitch stored := idData.(type) {\n\tcase []string:\n\t\treturn len(stored) == 0\n\tcase string:\n\t\treturn stored == \"\"\n\tdefault:\n\t\treturn true\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/collection\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"TUAwne/B0AdKc2VOSp5fgc/l4nH74M5zmT+9RL1jEn0YQbDZ3tN+7n1CTrDsazsMTCtjpXhBqsFroV+LQP5tng=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"list","path":"gno.land/p/nt/avl/list/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/avl/list/v0\"\ngno = \"0.9\"\n"},{"name":"list.gno","body":"// Package list implements a dynamic list data structure backed by an AVL 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 of specific size\n//\tl = list.Make(3, \"default\")  // creates [default, default, default]\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/avl/rotree/v0\"\n\t\"gno.land/p/nt/avl/v0\"\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\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 an AVL tree\ntype List struct {\n\ttree  avl.Tree\n\tidGen seqid.ID\n}\n\n// Len returns the number of elements in the list.\n//\n// Example:\n//\n//\tvar l list.List\n//\tl.Append(1, 2, 3)\n//\tprintln(l.Len()) // Output: 3\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.\n//\n// Example:\n//\n//\tvar l list.List\n//\tl.Append(1)        // adds single value\n//\tl.Append(2, 3, 4)  // adds multiple values\n//\tprintln(l.Len()) // Output: 4\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.\n// Returns nil if index is out of bounds.\n//\n// Example:\n//\n//\tvar l list.List\n//\tl.Append(1, 2, 3)\n//\tprintln(l.Get(1))    // Output: 2\n//\tprintln(l.Get(-1))   // Output: nil\n//\tprintln(l.Get(999))  // Output: nil\nfunc (l *List) Get(index int) any {\n\tif index \u003c 0 || index \u003e= l.tree.Size() {\n\t\treturn nil\n\t}\n\t_, value := l.tree.GetByIndex(index)\n\treturn value\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).\n//\n// Example:\n//\n//\tvar l list.List\n//\tl.Append(1, 2, 3)\n//\n//\tl.Set(1, 42)      // updates existing index\n//\tprintln(l.Get(1)) // Output: 42\n//\n//\tl.Set(3, 4)       // appends at end\n//\tprintln(l.Get(3)) // Output: 4\n//\n//\tl.Set(-1, 5)      // invalid index\n//\tprintln(l.Len()) // Output: 4 (list unchanged)\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.\n//\n// Example:\n//\n//\tvar l list.List\n//\tl.Append(1, 2, 3)\n//\n//\tval, ok := l.Delete(1)\n//\tprintln(val, ok)  // Output: 2 true\n//\tprintln(l.Len())  // Output: 2\n//\n//\tval, ok = l.Delete(-1)\n//\tprintln(val, ok)  // Output: nil false\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.\n//\n// Example:\n//\n//\tvar l list.List\n//\tl.Append(1, 2, 3, 4, 5)\n//\n//\tprintln(l.Slice(1, 4))   // Output: [2 3 4]\n//\tprintln(l.Slice(-1, 2))  // Output: [1 2]\n//\tprintln(l.Slice(3, 999)) // Output: [4 5]\n//\tprintln(l.Slice(3, 2))   // Output: nil\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.\n//\n// Example:\n//\n//\tvar l list.List\n//\tl.Append(1, 2, 3)\n//\n//\tclone := l.Clone()\n//\tclone.Set(0, 42)\n//\n//\tprintln(l.Get(0))    // Output: 1\n//\tprintln(clone.Get(0)) // Output: 42\nfunc (l *List) Clone() *List {\n\tnewList := \u0026List{\n\t\ttree:  avl.Tree{},\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.\n//\n// Example:\n//\n//\tvar l list.List\n//\tl.Append(1, 2, 3, 4, 5)\n//\n//\tdeleted := l.DeleteRange(1, 4)\n//\tprintln(deleted)     // Output: 3\n//\tprintln(l.Range(0, l.Len())) // Output: [1 5]\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 AVL tree.\n//\n// Example:\n//\n//\tvar l list.List\n//\tl.Append(1, 2, 3)\n//\n//\trotree := l.Tree()\n//\trotree.ReverseIterateByOffset(0, rotree.Size(), func(key string, value any) bool {\n//\t  println(value) // Output: 3 2 1\n//\t  return false\n//\t})\nfunc (l *List) Tree() *rotree.ReadOnlyTree {\n\treturn rotree.Wrap(\u0026l.tree, nil)\n}\n"},{"name":"list_test.gno","body":"package list\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nfunc TestList_Basic(t *testing.T) {\n\tvar l List\n\n\t// Test empty list\n\tif l.Len() != 0 {\n\t\tt.Errorf(\"new list should be empty, got len %d\", l.Len())\n\t}\n\n\t// Test append and length\n\tl.Append(1, 2, 3)\n\tif l.Len() != 3 {\n\t\tt.Errorf(\"expected len 3, got %d\", l.Len())\n\t}\n\n\t// Test get\n\tif v := l.Get(0); v != 1 {\n\t\tt.Errorf(\"expected 1 at index 0, got %v\", v)\n\t}\n\tif v := l.Get(1); v != 2 {\n\t\tt.Errorf(\"expected 2 at index 1, got %v\", v)\n\t}\n\tif v := l.Get(2); v != 3 {\n\t\tt.Errorf(\"expected 3 at index 2, got %v\", v)\n\t}\n\n\t// Test out of bounds\n\tif v := l.Get(-1); v != nil {\n\t\tt.Errorf(\"expected nil for negative index, got %v\", v)\n\t}\n\tif v := l.Get(3); v != nil {\n\t\tt.Errorf(\"expected nil for out of bounds index, got %v\", v)\n\t}\n}\n\nfunc TestList_Set(t *testing.T) {\n\tvar l List\n\tl.Append(1, 2, 3)\n\n\t// Test valid set within bounds\n\tif ok := l.Set(1, 42); !ok {\n\t\tt.Error(\"Set should return true for valid index\")\n\t}\n\tif v := l.Get(1); v != 42 {\n\t\tt.Errorf(\"expected 42 after Set, got %v\", v)\n\t}\n\n\t// Test set at size (append)\n\tif ok := l.Set(3, 4); !ok {\n\t\tt.Error(\"Set should return true when appending at size\")\n\t}\n\tif v := l.Get(3); v != 4 {\n\t\tt.Errorf(\"expected 4 after Set at size, got %v\", v)\n\t}\n\n\t// Test invalid sets\n\tif ok := l.Set(-1, 10); ok {\n\t\tt.Error(\"Set should return false for negative index\")\n\t}\n\tif ok := l.Set(5, 10); ok {\n\t\tt.Error(\"Set should return false for index \u003e size\")\n\t}\n\n\t// Verify list state hasn't changed after invalid operations\n\texpected := []any{1, 42, 3, 4}\n\tfor i, want := range expected {\n\t\tif got := l.Get(i); got != want {\n\t\t\tt.Errorf(\"index %d = %v; want %v\", i, got, want)\n\t\t}\n\t}\n}\n\nfunc TestList_Delete(t *testing.T) {\n\tvar l List\n\tl.Append(1, 2, 3)\n\n\t// Test valid delete\n\tif v, ok := l.Delete(1); !ok || v != 2 {\n\t\tt.Errorf(\"Delete(1) = %v, %v; want 2, true\", v, ok)\n\t}\n\tif l.Len() != 2 {\n\t\tt.Errorf(\"expected len 2 after delete, got %d\", l.Len())\n\t}\n\tif v := l.Get(1); v != 3 {\n\t\tt.Errorf(\"expected 3 at index 1 after delete, got %v\", v)\n\t}\n\n\t// Test invalid delete\n\tif v, ok := l.Delete(-1); ok || v != nil {\n\t\tt.Errorf(\"Delete(-1) = %v, %v; want nil, false\", v, ok)\n\t}\n\tif v, ok := l.Delete(2); ok || v != nil {\n\t\tt.Errorf(\"Delete(2) = %v, %v; want nil, false\", v, ok)\n\t}\n}\n\nfunc TestList_Slice(t *testing.T) {\n\tvar l List\n\tl.Append(1, 2, 3, 4, 5)\n\n\t// Test valid ranges\n\tvalues := l.Slice(1, 4)\n\texpected := []any{2, 3, 4}\n\tif !sliceEqual(values, expected) {\n\t\tt.Errorf(\"Slice(1,4) = %v; want %v\", values, expected)\n\t}\n\n\t// Test edge cases\n\tif values := l.Slice(-1, 2); !sliceEqual(values, []any{1, 2}) {\n\t\tt.Errorf(\"Slice(-1,2) = %v; want [1 2]\", values)\n\t}\n\tif values := l.Slice(3, 10); !sliceEqual(values, []any{4, 5}) {\n\t\tt.Errorf(\"Slice(3,10) = %v; want [4 5]\", values)\n\t}\n\tif values := l.Slice(3, 2); values != nil {\n\t\tt.Errorf(\"Slice(3,2) = %v; want nil\", values)\n\t}\n}\n\nfunc TestList_ForEach(t *testing.T) {\n\tvar l List\n\tl.Append(1, 2, 3)\n\n\tsum := 0\n\tl.ForEach(func(index int, value any) bool {\n\t\tsum += value.(int)\n\t\treturn false\n\t})\n\n\tif sum != 6 {\n\t\tt.Errorf(\"ForEach sum = %d; want 6\", sum)\n\t}\n\n\t// Test early termination\n\tcount := 0\n\tl.ForEach(func(index int, value any) bool {\n\t\tcount++\n\t\treturn true // stop after first item\n\t})\n\n\tif count != 1 {\n\t\tt.Errorf(\"ForEach early termination count = %d; want 1\", count)\n\t}\n}\n\nfunc TestList_Clone(t *testing.T) {\n\tvar l List\n\tl.Append(1, 2, 3)\n\n\tclone := l.Clone()\n\n\t// Test same length\n\tif clone.Len() != l.Len() {\n\t\tt.Errorf(\"clone.Len() = %d; want %d\", clone.Len(), l.Len())\n\t}\n\n\t// Test same values\n\tfor i := 0; i \u003c l.Len(); i++ {\n\t\tif clone.Get(i) != l.Get(i) {\n\t\t\tt.Errorf(\"clone.Get(%d) = %v; want %v\", i, clone.Get(i), l.Get(i))\n\t\t}\n\t}\n\n\t// Test independence\n\tl.Set(0, 42)\n\tif clone.Get(0) == l.Get(0) {\n\t\tt.Error(\"clone should be independent of original\")\n\t}\n\n\t// Test that appending to both after clone works independently.\n\tvar l2 List\n\tl2.Append(\"a\", \"b\", \"c\")\n\tclone2 := l2.Clone()\n\n\tl2.Append(\"d\", \"e\")\n\tclone2.Append(\"x\", \"y\")\n\n\tif l2.Len() != 5 {\n\t\tt.Errorf(\"original after append: Len() = %d; want 5\", l2.Len())\n\t}\n\tif clone2.Len() != 5 {\n\t\tt.Errorf(\"clone after append: Len() = %d; want 5\", clone2.Len())\n\t}\n\torigExpected := []any{\"a\", \"b\", \"c\", \"d\", \"e\"}\n\tcloneExpected := []any{\"a\", \"b\", \"c\", \"x\", \"y\"}\n\tfor i := 0; i \u003c 5; i++ {\n\t\tif l2.Get(i) != origExpected[i] {\n\t\t\tt.Errorf(\"original.Get(%d) = %v; want %v\", i, l2.Get(i), origExpected[i])\n\t\t}\n\t\tif clone2.Get(i) != cloneExpected[i] {\n\t\t\tt.Errorf(\"clone.Get(%d) = %v; want %v\", i, clone2.Get(i), cloneExpected[i])\n\t\t}\n\t}\n}\n\nfunc TestList_DeleteRange(t *testing.T) {\n\tvar l List\n\tl.Append(1, 2, 3, 4, 5)\n\n\t// Test valid range delete\n\tdeleted := l.DeleteRange(1, 4)\n\tif deleted != 3 {\n\t\tt.Errorf(\"DeleteRange(1,4) deleted %d elements; want 3\", deleted)\n\t}\n\tif l.Len() != 2 {\n\t\tt.Errorf(\"after DeleteRange(1,4) len = %d; want 2\", l.Len())\n\t}\n\texpected := []any{1, 5}\n\tfor i, want := range expected {\n\t\tif got := l.Get(i); got != want {\n\t\t\tt.Errorf(\"after DeleteRange(1,4) index %d = %v; want %v\", i, got, want)\n\t\t}\n\t}\n\n\t// Test edge cases\n\tl = List{}\n\tl.Append(1, 2, 3)\n\n\t// Delete with negative start\n\tif deleted := l.DeleteRange(-1, 2); deleted != 2 {\n\t\tt.Errorf(\"DeleteRange(-1,2) deleted %d elements; want 2\", deleted)\n\t}\n\n\t// Delete with end \u003e length\n\tl = List{}\n\tl.Append(1, 2, 3)\n\tif deleted := l.DeleteRange(1, 5); deleted != 2 {\n\t\tt.Errorf(\"DeleteRange(1,5) deleted %d elements; want 2\", deleted)\n\t}\n\n\t// Delete invalid range\n\tif deleted := l.DeleteRange(2, 1); deleted != 0 {\n\t\tt.Errorf(\"DeleteRange(2,1) deleted %d elements; want 0\", deleted)\n\t}\n\n\t// Delete empty range\n\tif deleted := l.DeleteRange(1, 1); deleted != 0 {\n\t\tt.Errorf(\"DeleteRange(1,1) deleted %d elements; want 0\", deleted)\n\t}\n}\n\nfunc TestList_Tree(t *testing.T) {\n\tvar l List\n\tl.Append(1, 2, 3)\n\n\trotree := l.Tree()\n\texpected := 3\n\n\t// Reverse iterate through the ReadOnlyTree\n\trotree.ReverseIterateByOffset(0, rotree.Size(), func(key string, value any) bool {\n\t\tintValue := value.(int)\n\t\tif intValue != expected {\n\t\t\tt.Errorf(\"ReadOnlyTree expected %d, got %d\", expected, intValue)\n\t\t}\n\t\texpected--\n\t\treturn false\n\t})\n}\n\nfunc TestList_EmptyOperations(t *testing.T) {\n\tvar l List\n\n\t// Operations on empty list\n\tif v := l.Get(0); v != nil {\n\t\tt.Errorf(\"Get(0) on empty list = %v; want nil\", v)\n\t}\n\n\t// Set should work at index 0 for empty list (append case)\n\tif ok := l.Set(0, 1); !ok {\n\t\tt.Error(\"Set(0,1) on empty list = false; want true\")\n\t}\n\tif v := l.Get(0); v != 1 {\n\t\tt.Errorf(\"Get(0) after Set = %v; want 1\", v)\n\t}\n\n\tl = List{} // Reset to empty list\n\tif v, ok := l.Delete(0); ok || v != nil {\n\t\tt.Errorf(\"Delete(0) on empty list = %v, %v; want nil, false\", v, ok)\n\t}\n\tif values := l.Slice(0, 1); values != nil {\n\t\tt.Errorf(\"Range(0,1) on empty list = %v; want nil\", values)\n\t}\n}\n\nfunc TestList_DifferentTypes(t *testing.T) {\n\tvar l List\n\n\t// Test with different types\n\tl.Append(42, \"hello\", true, 3.14)\n\n\tif v := l.Get(0).(int); v != 42 {\n\t\tt.Errorf(\"Get(0) = %v; want 42\", v)\n\t}\n\tif v := l.Get(1).(string); v != \"hello\" {\n\t\tt.Errorf(\"Get(1) = %v; want 'hello'\", v)\n\t}\n\tif v := l.Get(2).(bool); !v {\n\t\tt.Errorf(\"Get(2) = %v; want true\", v)\n\t}\n\tif v := l.Get(3).(float64); v != 3.14 {\n\t\tt.Errorf(\"Get(3) = %v; want 3.14\", v)\n\t}\n}\n\nfunc TestList_LargeOperations(t *testing.T) {\n\tvar l List\n\n\t// Test with larger number of elements\n\tn := 1000\n\tfor i := 0; i \u003c n; i++ {\n\t\tl.Append(i)\n\t}\n\n\tif l.Len() != n {\n\t\tt.Errorf(\"Len() = %d; want %d\", l.Len(), n)\n\t}\n\n\t// Test range on large list\n\tvalues := l.Slice(n-3, n)\n\texpected := []any{n - 3, n - 2, n - 1}\n\tif !sliceEqual(values, expected) {\n\t\tt.Errorf(\"Range(%d,%d) = %v; want %v\", n-3, n, values, expected)\n\t}\n\n\t// Test large range deletion\n\tdeleted := l.DeleteRange(100, 900)\n\tif deleted != 800 {\n\t\tt.Errorf(\"DeleteRange(100,900) = %d; want 800\", deleted)\n\t}\n\tif l.Len() != 200 {\n\t\tt.Errorf(\"Len() after large delete = %d; want 200\", l.Len())\n\t}\n}\n\nfunc TestList_ChainedOperations(t *testing.T) {\n\tvar l List\n\n\t// Test sequence of operations\n\tl.Append(1, 2, 3)\n\tl.Delete(1)\n\tl.Append(4)\n\tl.Set(1, 5)\n\n\texpected := []any{1, 5, 4}\n\tfor i, want := range expected {\n\t\tif got := l.Get(i); got != want {\n\t\t\tt.Errorf(\"index %d = %v; want %v\", i, got, want)\n\t\t}\n\t}\n}\n\nfunc TestList_RangeEdgeCases(t *testing.T) {\n\tvar l List\n\tl.Append(1, 2, 3, 4, 5)\n\n\t// Test various edge cases for Range\n\tcases := []struct {\n\t\tstart, end int\n\t\twant       []any\n\t}{\n\t\t{-10, 2, []any{1, 2}},\n\t\t{3, 10, []any{4, 5}},\n\t\t{0, 0, nil},\n\t\t{5, 5, nil},\n\t\t{4, 3, nil},\n\t\t{-1, -1, nil},\n\t}\n\n\tfor _, tc := range cases {\n\t\tgot := l.Slice(tc.start, tc.end)\n\t\tif !sliceEqual(got, tc.want) {\n\t\t\tt.Errorf(\"Slice(%d,%d) = %v; want %v\", tc.start, tc.end, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestList_IndexConsistency(t *testing.T) {\n\tvar l List\n\n\t// Initial additions\n\tl.Append(1, 2, 3, 4, 5) // [1,2,3,4,5]\n\n\t// Delete from middle\n\tl.Delete(2) // [1,2,4,5]\n\n\t// Add more elements\n\tl.Append(6, 7) // [1,2,4,5,6,7]\n\n\t// Delete range from middle\n\tl.DeleteRange(1, 4) // [1,6,7]\n\n\t// Add more elements\n\tl.Append(8, 9, 10) // [1,6,7,8,9,10]\n\n\t// Verify sequence is continuous\n\texpected := []any{1, 6, 7, 8, 9, 10}\n\tfor i, want := range expected {\n\t\tif got := l.Get(i); got != want {\n\t\t\tt.Errorf(\"index %d = %v; want %v\", i, got, want)\n\t\t}\n\t}\n\n\t// Verify no extra elements exist\n\tif l.Len() != len(expected) {\n\t\tt.Errorf(\"length = %d; want %d\", l.Len(), len(expected))\n\t}\n\n\t// Verify all indices are accessible\n\tallValues := l.Slice(0, l.Len())\n\tif !sliceEqual(allValues, expected) {\n\t\tt.Errorf(\"Slice(0, Len()) = %v; want %v\", allValues, expected)\n\t}\n\n\t// Verify no gaps in iteration\n\tvar iteratedValues []any\n\tvar indices []int\n\tl.ForEach(func(index int, value any) bool {\n\t\titeratedValues = append(iteratedValues, value)\n\t\tindices = append(indices, index)\n\t\treturn false\n\t})\n\n\t// Check values from iteration\n\tif !sliceEqual(iteratedValues, expected) {\n\t\tt.Errorf(\"ForEach values = %v; want %v\", iteratedValues, expected)\n\t}\n\n\t// Check indices are sequential\n\tfor i, idx := range indices {\n\t\tif idx != i {\n\t\t\tt.Errorf(\"ForEach index %d = %d; want %d\", i, idx, i)\n\t\t}\n\t}\n}\n\nfunc TestList_RecursiveSafety(t *testing.T) {\n\t// Create a new list\n\tl := \u0026List{}\n\n\t// Add some initial values\n\tl.Append(\"id1\")\n\tl.Append(\"id2\")\n\tl.Append(\"id3\")\n\n\t// Test deep list traversal\n\tfound := false\n\tl.ForEach(func(i int, v any) bool {\n\t\tif str, ok := v.(string); ok {\n\t\t\tif str == \"id2\" {\n\t\t\t\tfound = true\n\t\t\t\treturn true // stop iteration\n\t\t\t}\n\t\t}\n\t\treturn false // continue iteration\n\t})\n\n\tif !found {\n\t\tt.Error(\"Failed to find expected value in list\")\n\t}\n\n\tshort := testing.Short()\n\n\t// Test recursive safety by performing multiple operations\n\tfor i := 0; i \u003c 1000; i++ {\n\t\t// Add new value\n\t\tl.Append(ufmt.Sprintf(\"id%d\", i+4))\n\n\t\tif !short {\n\t\t\t// Search for a value\n\t\t\tvar lastFound bool\n\t\t\tl.ForEach(func(j int, v any) bool {\n\t\t\t\tif str, ok := v.(string); ok {\n\t\t\t\t\tif str == ufmt.Sprintf(\"id%d\", i+3) {\n\t\t\t\t\t\tlastFound = true\n\t\t\t\t\t\treturn true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn false\n\t\t\t})\n\n\t\t\tif !lastFound {\n\t\t\t\tt.Errorf(\"Failed to find value id%d after insertion\", i+3)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Verify final length\n\texpectedLen := 1003 // 3 initial + 1000 added\n\tif l.Len() != expectedLen {\n\t\tt.Errorf(\"Expected length %d, got %d\", expectedLen, l.Len())\n\t}\n\n\tif short {\n\t\tt.Skip(\"skipping extended recursive safety test in short mode\")\n\t}\n}\n\n// Helper function to compare slices\nfunc sliceEqual(a, b []any) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i := range a {\n\t\tif a[i] != b[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"lwpSxRdQZGTWdwuyCjvG1f5DD35fvyKhh7I/T6A18rs2NxMjaXMX7pWmpRXew/dvd/ePlOw0Sxcsx3oobt7JxQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"datastore","path":"gno.land/p/jeronimoalbi/datastore","files":[{"name":"datastore.gno","body":"package datastore\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/avl/v0\"\n)\n\n// ErrStorageExists indicates that a storage exists with the same name.\nvar ErrStorageExists = errors.New(\"a storage with the same name exists\")\n\n// Datastore is a store that can contain multiple named storages.\n// A storage is a collection of records.\n//\n// Example usage:\n//\n//\t// Create an empty storage to store user records\n//\tvar db Datastore\n//\tstorage := db.CreateStorage(\"users\")\n//\n//\t// Get a storage that has been created before\n//\tstorage = db.GetStorage(\"profiles\")\ntype Datastore struct {\n\tstorages avl.Tree // string(name) -\u003e *Storage\n}\n\n// CreateStorage creates a new named storage within the data store.\nfunc (ds *Datastore) CreateStorage(name string, options ...StorageOption) *Storage {\n\tif ds.storages.Has(name) {\n\t\treturn nil\n\t}\n\n\ts := NewStorage(name, options...)\n\tds.storages.Set(name, \u0026s)\n\treturn \u0026s\n}\n\n// HasStorage checks if data store contains a storage with a specific name.\nfunc (ds Datastore) HasStorage(name string) bool {\n\treturn ds.storages.Has(name)\n}\n\n// GetStorage returns a storage that has been created with a specific name.\n// It returns nil when a storage with the specified name is not found.\nfunc (ds Datastore) GetStorage(name string) *Storage {\n\tif v := ds.storages.Get(name); v != nil {\n\t\treturn v.(*Storage)\n\t}\n\treturn nil\n}\n"},{"name":"datastore_test.gno","body":"package datastore\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestDatastoreCreateStorage(t *testing.T) {\n\tcases := []struct {\n\t\tname        string\n\t\tstorageName string\n\t\tmustFail    bool\n\t\tsetup       func(*Datastore)\n\t}{\n\t\t{\n\t\t\tname:        \"success\",\n\t\t\tstorageName: \"users\",\n\t\t},\n\t\t{\n\t\t\tname:        \"storage exists\",\n\t\t\tstorageName: \"users\",\n\t\t\tmustFail:    true,\n\t\t\tsetup: func(db *Datastore) {\n\t\t\t\tdb.CreateStorage(\"users\")\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar db Datastore\n\t\t\tif tc.setup != nil {\n\t\t\t\ttc.setup(\u0026db)\n\t\t\t}\n\n\t\t\tstorage := db.CreateStorage(tc.storageName)\n\n\t\t\tif tc.mustFail {\n\t\t\t\tuassert.Equal(t, nil, storage)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.NotEqual(t, nil, storage, \"storage created\")\n\t\t\tuassert.Equal(t, tc.storageName, storage.Name())\n\t\t\tuassert.True(t, db.HasStorage(tc.storageName))\n\t\t})\n\t}\n}\n\nfunc TestDatastoreHasStorage(t *testing.T) {\n\tvar (\n\t\tdb   Datastore\n\t\tname = \"users\"\n\t)\n\n\tuassert.False(t, db.HasStorage(name))\n\n\tdb.CreateStorage(name)\n\tuassert.True(t, db.HasStorage(name))\n}\n\nfunc TestDatastoreGetStorage(t *testing.T) {\n\tvar (\n\t\tdb   Datastore\n\t\tname = \"users\"\n\t)\n\n\tstorage := db.GetStorage(name)\n\tuassert.Equal(t, nil, storage)\n\n\tdb.CreateStorage(name)\n\n\tstorage = db.GetStorage(name)\n\turequire.NotEqual(t, nil, storage, \"storage found\")\n\tuassert.Equal(t, name, storage.Name())\n}\n"},{"name":"doc.gno","body":"// Package datastore provides support to store multiple collections of records.\n//\n// It supports the definition of multiple storages, where each one is a collection\n// of records. Records can have any number of user defined fields which are added\n// dynamically when values are set on a record. These fields can also be renamed\n// or removed.\n//\n// Storages have support for simple schemas that allows users to pre-define fields\n// which can optionally have a default value also defined. Default values are\n// assigned to new records on creation.\n//\n// User defined schemas can optionally be strict, which means that records from a\n// storage using the schema can only assign values to the pre-defined set of fields.\n// In which case, assigning a value to an unknown field would result on an error.\n//\n// Package also support the definition of custom record indexes. Indexes are used\n// by storages to search and iterate records.\n// The default index is the ID index but custom single and multi value indexes can\n// be defined.\n//\n// WARNING: Using this package to store your realm data must be carefully considered.\n// The fact that record fields are not strictly typed and can be renamed or removed\n// could lead to issues if not careful when coding your realm(s). So it's recommended\n// that you consider other alternatives first, like alternative patterns or solutions\n// provided by the blockchain to deal with data, types and data migration for example.\n//\n// Example usage:\n//\n//\tvar db datastore.Datastore\n//\n//\t// Define a unique case insensitive index for user emails\n//\temailIdx := datastore.NewIndex(\"email\", func(r datastore.Record) string {\n//\t  return r.MustGet(\"email\").(string)\n//\t}).Unique().CaseInsensitive()\n//\n//\t// Create a new storage for user records\n//\tstorage := db.CreateStorage(\"users\", datastore.WithIndex(emailIdx))\n//\n//\t// Add a user with a single \"email\" field\n//\tuser := storage.NewRecord()\n//\tuser.Set(\"email\", \"foo@bar.org\")\n//\n//\t// Save to assing user ID and update indexes\n//\tuser.Save()\n//\n//\t// Find user by email using the custom index\n//\tuser, _ = storage.Get(emailIdx.Name(), \"foo@bar.org\")\n//\n//\t// Find user by ID\n//\tuser, _ = storage.GetByID(user.ID())\n//\n//\t// Search user's profile by email in another existing storage\n//\tstorage = db.GetStorage(\"profiles\")\n//\temail := user.MustGet(\"email\").(string)\n//\tprofile, found := storage.Get(\"user\", email)\n//\tif !found {\n//\t  panic(\"profile not found\")\n//\t}\n//\n//\t// Delete the profile from the storage and update indexes\n//\tstorage.Delete(profile.ID())\n//\n// Example query usage:\n//\n//\tvar db datastore.Datastore\n//\n//\t// Create a query with a custom offset and size\n//\tstorage := db.GetStorage(\"users\")\n//\trecordset, err := storage.Query(datastore.WithOffset(100), datastore.WithSize(50))\n//\tif err != nil {\n//\t  panic(err)\n//\t}\n//\n//\t// Get all query results\n//\tvar records []Record\n//\trecordset.Iterate(func(r datastore.Record) bool {\n//\t  records = append(records, r)\n//\t  return false\n//\t})\n//\n// Example query using a custom index usage:\n//\n//\tvar db datastore.Datastore\n//\n//\t// Create a query to get records using a custom pre-defined index\n//\tstorage := db.GetStorage(\"posts\")\n//\trecordset, err := storage.Query(datastore.UseIndex(\"tags\", \"tagname\"))\n//\tif err != nil {\n//\t  panic(err)\n//\t}\n//\n//\t// Get all query results\n//\tvar records []Record\n//\trecordset.Iterate(func(r datastore.Record) bool {\n//\t  records = append(records, r)\n//\t  return false\n//\t})\npackage datastore\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/jeronimoalbi/datastore\"\ngno = \"0.9\"\n"},{"name":"index.gno","body":"package datastore\n\nimport (\n\t\"gno.land/p/moul/collection\"\n)\n\n// DefaultIndexOptions defines the default options for new indexes.\nconst DefaultIndexOptions = collection.DefaultIndex | collection.SparseIndex\n\ntype (\n\t// IndexFn defines a type for single value indexing functions.\n\t// This type of function extracts a single string value from\n\t// a record that is then used to index it.\n\tIndexFn func(Record) string\n\n\t// IndexMultiValueFn defines a type for multi value indexing functions.\n\t// This type of function extracts multiple string values from a\n\t// record that are then used to index it.\n\tIndexMultiValueFn func(Record) []string\n\n\t// Index defines a type for custom user defined storage indexes.\n\t// Storages are by default indexed by the auto geneated record ID\n\t// but can additionally be indexed by other custom record fields.\n\tIndex struct {\n\t\tname    string\n\t\toptions collection.IndexOption\n\t\tfn      interface{}\n\t}\n)\n\n// NewIndex creates a new single value index.\n//\n// Usage example:\n//\n//\t// Index a User record by email\n//\tidx := NewIndex(\"email\", func(r Record) string {\n//\t  return r.MustGet(\"email\").(string)\n//\t})\nfunc NewIndex(name string, fn IndexFn) Index {\n\treturn Index{\n\t\tname:    name,\n\t\toptions: DefaultIndexOptions,\n\t\tfn: func(v interface{}) string {\n\t\t\treturn fn(v.(Record))\n\t\t},\n\t}\n}\n\n// NewMultiValueIndex creates a new multi value index.\n//\n// Usage example:\n//\n//\t// Index a Post record by tag\n//\tidx := NewMultiValueIndex(\"tag\", func(r Record) []string {\n//\t  return r.MustGet(\"tags\").([]string)\n//\t})\nfunc NewMultiValueIndex(name string, fn IndexMultiValueFn) Index {\n\treturn Index{\n\t\tname:    name,\n\t\toptions: DefaultIndexOptions,\n\t\tfn: func(v interface{}) []string {\n\t\t\treturn fn(v.(Record))\n\t\t},\n\t}\n}\n\n// Name returns index's name.\nfunc (idx Index) Name() string {\n\treturn idx.name\n}\n\n// Options returns current index options.\n// These options define the index behavior regarding case sensitivity and uniquenes.\nfunc (idx Index) Options() collection.IndexOption {\n\treturn idx.options\n}\n\n// Func returns the function that storage collections apply\n// to each record to get the value to use for indexing it.\nfunc (idx Index) Func() interface{} {\n\treturn idx.fn\n}\n\n// Unique returns a copy of the index that indexes record values as unique values.\n// Returned index contains previous options plus the unique one.\nfunc (idx Index) Unique() Index {\n\tif idx.options\u0026collection.UniqueIndex == 0 {\n\t\tidx.options |= collection.UniqueIndex\n\t}\n\treturn idx\n}\n\n// CaseInsensitive returns a copy of the index that indexes record values ignoring casing.\n// Returned index contains previous options plus the case insensitivity one.\nfunc (idx Index) CaseInsensitive() Index {\n\tif idx.options\u0026collection.CaseInsensitiveIndex == 0 {\n\t\tidx.options |= collection.CaseInsensitiveIndex\n\t}\n\treturn idx\n}\n"},{"name":"index_test.gno","body":"package datastore\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/moul/collection\"\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nfunc TestNewIndex(t *testing.T) {\n\tcases := []struct {\n\t\tname    string\n\t\toptions collection.IndexOption\n\t\tsetup   func(Index) Index\n\t}{\n\t\t{\n\t\t\tname:    \"default\",\n\t\t\toptions: DefaultIndexOptions,\n\t\t},\n\t\t{\n\t\t\tname:    \"unique\",\n\t\t\toptions: DefaultIndexOptions | collection.UniqueIndex,\n\t\t\tsetup:   func(idx Index) Index { return idx.Unique() },\n\t\t},\n\t\t{\n\t\t\tname:    \"case insensitive\",\n\t\t\toptions: DefaultIndexOptions | collection.CaseInsensitiveIndex,\n\t\t\tsetup:   func(idx Index) Index { return idx.CaseInsensitive() },\n\t\t},\n\t\t{\n\t\t\tname:    \"unique case insensitive\",\n\t\t\toptions: DefaultIndexOptions | collection.CaseInsensitiveIndex | collection.UniqueIndex,\n\t\t\tsetup:   func(idx Index) Index { return idx.CaseInsensitive().Unique() },\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tname := \"foo\"\n\t\t\tidx := NewIndex(name, func(Record) string { return \"\" })\n\n\t\t\tif tc.setup != nil {\n\t\t\t\tidx = tc.setup(idx)\n\t\t\t}\n\n\t\t\tuassert.Equal(t, name, idx.Name())\n\t\t\tuassert.Equal(t, uint64(tc.options), uint64(idx.Options()))\n\n\t\t\t_, ok := idx.Func().(func(interface{}) string)\n\t\t\tuassert.True(t, ok)\n\t\t})\n\t}\n}\n\nfunc TestNewMultiIndex(t *testing.T) {\n\tcases := []struct {\n\t\tname    string\n\t\toptions collection.IndexOption\n\t\tsetup   func(Index) Index\n\t}{\n\t\t{\n\t\t\tname:    \"default\",\n\t\t\toptions: DefaultIndexOptions,\n\t\t},\n\t\t{\n\t\t\tname:    \"unique\",\n\t\t\toptions: DefaultIndexOptions | collection.UniqueIndex,\n\t\t\tsetup:   func(idx Index) Index { return idx.Unique() },\n\t\t},\n\t\t{\n\t\t\tname:    \"case insensitive\",\n\t\t\toptions: DefaultIndexOptions | collection.CaseInsensitiveIndex,\n\t\t\tsetup:   func(idx Index) Index { return idx.CaseInsensitive() },\n\t\t},\n\t\t{\n\t\t\tname:    \"unique case insensitive\",\n\t\t\toptions: DefaultIndexOptions | collection.CaseInsensitiveIndex | collection.UniqueIndex,\n\t\t\tsetup:   func(idx Index) Index { return idx.CaseInsensitive().Unique() },\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tname := \"foo\"\n\t\t\tidx := NewMultiValueIndex(name, func(Record) []string { return nil })\n\n\t\t\tif tc.setup != nil {\n\t\t\t\tidx = tc.setup(idx)\n\t\t\t}\n\n\t\t\tuassert.Equal(t, name, idx.Name())\n\t\t\tuassert.Equal(t, uint64(tc.options), uint64(idx.Options()))\n\n\t\t\t_, ok := idx.Func().(func(interface{}) []string)\n\t\t\tuassert.True(t, ok)\n\t\t})\n\t}\n}\n"},{"name":"query.gno","body":"package datastore\n\nimport (\n\t\"gno.land/p/moul/collection\"\n)\n\nvar defaultQuery = Query{indexName: collection.IDIndex}\n\n// Query contains arguments for querying a storage.\ntype Query struct {\n\toffset    int\n\tsize      int\n\tindexName string\n\tindexKey  string\n}\n\n// Offset returns the position of the first record to return.\n// The minimum offset value is 0.\nfunc (q Query) Offset() int {\n\treturn q.offset\n}\n\n// Size returns the maximum number of records a query returns.\nfunc (q Query) Size() int {\n\treturn q.size\n}\n\n// IndexName returns the name of the storage index being used for the query.\nfunc (q Query) IndexName() string {\n\treturn q.indexName\n}\n\n// IndexKey return the index key value to locate the records.\n// An empty string is returned when all indexed records match the query.\nfunc (q Query) IndexKey() string {\n\treturn q.indexKey\n}\n\n// IsEmpty checks if the query is empty.\n// Empty queries return no records.\nfunc (q Query) IsEmpty() bool {\n\treturn q.indexName == \"\"\n}\n"},{"name":"query_options.gno","body":"package datastore\n\nimport (\n\t\"errors\"\n\t\"strings\"\n)\n\nvar (\n\tErrEmptyQueryIndexName = errors.New(\"query index name is empty\")\n\tErrInvalidQueryOffset  = errors.New(\"minimum allowed query offset is 0\")\n\tErrInvalidQuerySize    = errors.New(\"minimum allowed query size is 1\")\n)\n\n// QueryOption configures queries.\ntype QueryOption func(*Query) error\n\n// WithOffset assigns the offset or position of the first record that query must return.\n// The minimum allowed offset is 0.\nfunc WithOffset(offset int) QueryOption {\n\treturn func(q *Query) error {\n\t\tif offset \u003c 0 {\n\t\t\treturn ErrInvalidQueryOffset\n\t\t}\n\n\t\tq.offset = offset\n\t\treturn nil\n\t}\n}\n\n// WithSize assigns the maximum number of records that query can return.\n// The minimum allowed size is 1.\nfunc WithSize(size int) QueryOption {\n\treturn func(q *Query) error {\n\t\tif size \u003c 1 {\n\t\t\treturn ErrInvalidQuerySize\n\t\t}\n\n\t\tq.size = size\n\t\treturn nil\n\t}\n}\n\n// UseIndex assigns the index that the query must use to get the records.\n// Using an index requires a key value to locate the records within the index.\nfunc UseIndex(name, key string) QueryOption {\n\treturn func(q *Query) error {\n\t\tq.indexName = strings.TrimSpace(name)\n\t\tif q.indexName == \"\" {\n\t\t\treturn ErrEmptyQueryIndexName\n\t\t}\n\n\t\tq.indexKey = key\n\t\treturn nil\n\t}\n}\n"},{"name":"record.gno","body":"package datastore\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/moul/collection\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n)\n\n// ErrUndefinedField indicates that a field in not defined in a record's schema.\nvar ErrUndefinedField = errors.New(\"undefined field\")\n\ntype (\n\t// Record stores values for one or more fields.\n\tRecord interface {\n\t\tReadOnlyRecord\n\n\t\t// Set assings a value to a record field.\n\t\t// If the field doesn't exist it's created if the underlying schema allows it.\n\t\t// Storage schema can optionally be strict in which case no new fields other than\n\t\t// the ones that were previously defined are allowed.\n\t\tSet(field string, value interface{}) error\n\n\t\t// Save assigns an ID to newly created records and update storage indexes.\n\t\tSave() bool\n\t}\n\n\t// ReadOnlyRecord defines an interface for read-only records.\n\tReadOnlyRecord interface {\n\t\t// ID returns record's ID\n\t\tID() uint64\n\n\t\t// Key returns a string representation of the record's ID.\n\t\t// It's used to be able to search records within the ID index.\n\t\tKey() string\n\n\t\t// Type returns the record's type.\n\t\tType() string\n\n\t\t// Fields returns the list of the record's field names.\n\t\tFields() []string\n\n\t\t// IsEmpty checks if the record has no values.\n\t\tIsEmpty() bool\n\n\t\t// HasField checks if the record has a specific field.\n\t\tHasField(name string) bool\n\n\t\t// Get returns the value of a record's field.\n\t\tGet(field string) (value interface{}, found bool)\n\n\t\t// MustGet returns the value of a record's field or panics when the field is not found.\n\t\tMustGet(field string) interface{}\n\t}\n\n\t// RecordIterFn defines a type for record iteration functions.\n\tRecordIterFn func(Record) (stop bool)\n\n\t// Recordset defines an interface that allows iterating multiple records.\n\tRecordset interface {\n\t\t// Iterate iterates records in order.\n\t\tIterate(fn RecordIterFn) (stopped bool)\n\n\t\t// ReverseIterate iterates records in reverse order.\n\t\tReverseIterate(fn RecordIterFn) (stopped bool)\n\n\t\t// Size returns the number of records in the recordset.\n\t\tSize() int\n\t}\n)\n\ntype record struct {\n\tid         uint64\n\tschema     *Schema\n\tcollection *collection.Collection\n\tvalues     avl.Tree // string(field index) -\u003e interface{}\n}\n\n// ID returns record's ID\nfunc (r record) ID() uint64 {\n\treturn r.id\n}\n\n// Key returns a string representation of the record's ID.\n// It's used to be able to search records within the ID index.\nfunc (r record) Key() string {\n\treturn seqid.ID(r.id).String()\n}\n\n// Type returns the record's type.\nfunc (r record) Type() string {\n\treturn r.schema.Name()\n}\n\n// Fields returns the list of the record's field names.\nfunc (r record) Fields() []string {\n\treturn r.schema.Fields()\n}\n\n// IsEmpty checks if the record has no values.\nfunc (r record) IsEmpty() bool {\n\treturn r.values.Size() == 0\n}\n\n// HasField checks if the record has a specific field.\nfunc (r record) HasField(name string) bool {\n\treturn r.schema.HasField(name)\n}\n\n// Set assings a value to a record field.\n// If the field doesn't exist it's created if the underlying schema allows it.\n// Storage schema can optionally be strict in which case no new fields other than\n// the ones that were previously defined are allowed.\nfunc (r *record) Set(field string, value interface{}) error {\n\ti := r.schema.GetFieldIndex(field)\n\tif i == -1 {\n\t\tif r.schema.IsStrict() {\n\t\t\treturn ErrUndefinedField\n\t\t}\n\n\t\ti, _ = r.schema.AddField(field, nil)\n\t}\n\n\tkey := castIntToKey(i)\n\tr.values.Set(key, value)\n\treturn nil\n}\n\n// Get returns the value of a record's field.\nfunc (r record) Get(field string) (value interface{}, found bool) {\n\ti := r.schema.GetFieldIndex(field)\n\tif i == -1 {\n\t\treturn nil, false\n\t}\n\n\tkey := castIntToKey(i)\n\treturn r.values.Get(key), r.values.Has(key)\n}\n\n// MustGet returns the value of a record's field or panics when the field is not found.\nfunc (r record) MustGet(field string) interface{} {\n\tv, found := r.Get(field)\n\tif !found {\n\t\tpanic(\"field not found: \" + field)\n\t}\n\treturn v\n}\n\n// Save assigns an ID to newly created records and update storage indexes.\nfunc (r *record) Save() bool {\n\tif r.id == 0 {\n\t\tr.id = r.collection.Set(r)\n\t\treturn r.id != 0\n\t}\n\treturn r.collection.Update(r.id, r)\n}\n\ntype recordset struct {\n\tquery   Query\n\trecords avl.ITree\n\tkeys    []string\n\tsize    int\n}\n\n// Iterate iterates records in order.\nfunc (rs recordset) Iterate(fn RecordIterFn) (stopped bool) {\n\tif rs.isUsingCustomIndex() {\n\t\tfor _, k := range rs.keys {\n\t\t\tv := rs.records.Get(k)\n\t\t\tif v == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif fn(v.(Record)) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\n\t\treturn false\n\t}\n\n\toffset := rs.query.Offset()\n\tcount := rs.query.Size()\n\tif count == 0 {\n\t\tcount = rs.records.Size()\n\t}\n\n\treturn rs.records.IterateByOffset(offset, count, func(_ string, v interface{}) bool {\n\t\treturn fn(v.(Record))\n\t})\n}\n\n// ReverseIterate iterates records in reverse order.\nfunc (rs recordset) ReverseIterate(fn RecordIterFn) (stopped bool) {\n\tif rs.isUsingCustomIndex() {\n\t\tfor i := len(rs.keys) - 1; i \u003e= 0; i-- {\n\t\t\tv := rs.records.Get(rs.keys[i])\n\t\t\tif v == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif fn(v.(Record)) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\n\t\treturn false\n\t}\n\n\toffset := rs.query.Offset()\n\tcount := rs.query.Size()\n\tif count == 0 {\n\t\tcount = rs.records.Size()\n\t}\n\n\treturn rs.records.ReverseIterateByOffset(offset, count, func(_ string, v interface{}) bool {\n\t\treturn fn(v.(Record))\n\t})\n}\n\n// Size returns the number of records in the recordset.\nfunc (rs recordset) Size() int {\n\treturn rs.size\n}\n\nfunc (rs recordset) isUsingCustomIndex() bool {\n\treturn rs.query.IndexName() != collection.IDIndex\n}\n"},{"name":"record_test.gno","body":"package datastore\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nvar (\n\t_ Record    = (*record)(nil)\n\t_ Recordset = (*recordset)(nil)\n)\n\nfunc TestRecordDefaults(t *testing.T) {\n\t// Arrange\n\tstorage := NewStorage(\"foo\")\n\n\t// Act\n\tr := storage.NewRecord()\n\n\t// Assert\n\tuassert.Equal(t, uint64(0), r.ID())\n\tuassert.Equal(t, \"0000000\", r.Key())\n\tuassert.Equal(t, \"Foo\", r.Type())\n\tuassert.Equal(t, nil, r.Fields())\n\tuassert.True(t, r.IsEmpty())\n}\n\nfunc TestRecordHasField(t *testing.T) {\n\tstorage := NewStorage(\"foo\")\n\tstorage.Schema().AddField(\"foo\", nil)\n\n\tr := storage.NewRecord()\n\n\tuassert.True(t, r.HasField(\"foo\"))\n\tuassert.False(t, r.HasField(\"undefined\"))\n}\n\nfunc TestRecordSet(t *testing.T) {\n\tcases := []struct {\n\t\tname        string\n\t\toptions     []SchemaOption\n\t\tfield       string\n\t\tfieldsCount int\n\t\tvalue       int\n\t\terr         error\n\t}{\n\t\t{\n\t\t\tname:        \"first new field\",\n\t\t\tfield:       \"test\",\n\t\t\tfieldsCount: 1,\n\t\t\tvalue:       42,\n\t\t},\n\t\t{\n\t\t\tname: \"new extra field\",\n\t\t\toptions: []SchemaOption{\n\t\t\t\tWithField(\"foo\"),\n\t\t\t\tWithField(\"bar\"),\n\t\t\t},\n\t\t\tfield:       \"test\",\n\t\t\tfieldsCount: 3,\n\t\t\tvalue:       42,\n\t\t},\n\t\t{\n\t\t\tname: \"existing field\",\n\t\t\toptions: []SchemaOption{\n\t\t\t\tWithField(\"test\"),\n\t\t\t},\n\t\t\tfield:       \"test\",\n\t\t\tfieldsCount: 1,\n\t\t\tvalue:       42,\n\t\t},\n\t\t{\n\t\t\tname:        \"undefined field\",\n\t\t\toptions:     []SchemaOption{Strict()},\n\t\t\tfield:       \"test\",\n\t\t\tfieldsCount: 1,\n\t\t\tvalue:       42,\n\t\t\terr:         ErrUndefinedField,\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Arrange\n\t\t\ts := NewSchema(\"Foo\", tc.options...)\n\t\t\tstorage := NewStorage(\"foo\", WithSchema(s))\n\t\t\tr := storage.NewRecord()\n\n\t\t\t// Act\n\t\t\terr := r.Set(tc.field, tc.value)\n\n\t\t\t// Assert\n\t\t\tif tc.err != nil {\n\t\t\t\turequire.ErrorIs(t, err, tc.err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.NoError(t, err)\n\t\t\tuassert.True(t, r.HasField(\"test\"))\n\t\t\tuassert.False(t, r.IsEmpty())\n\t\t\tuassert.Equal(t, tc.fieldsCount, len(r.Fields()))\n\t\t})\n\t}\n}\n\nfunc TestRecordGet(t *testing.T) {\n\tstorage := NewStorage(\"foo\")\n\tr := storage.NewRecord()\n\tr.Set(\"foo\", \"bar\")\n\tr.Set(\"test\", 42)\n\n\tv, found := r.Get(\"test\")\n\turequire.True(t, found, \"get setted value\")\n\n\tgot, ok := v.(int)\n\turequire.True(t, ok, \"setted value type\")\n\tuassert.Equal(t, 42, got)\n\n\t_, found = r.Get(\"unknown\")\n\tuassert.False(t, found)\n}\n\nfunc TestRecordSave(t *testing.T) {\n\tindex := NewIndex(\"name\", func(r Record) string {\n\t\treturn r.MustGet(\"name\").(string)\n\t}).Unique().CaseInsensitive()\n\n\tstorage := NewStorage(\"foo\", WithIndex(index))\n\tcases := []struct {\n\t\tname            string\n\t\tid              uint64\n\t\tfieldValue, key string\n\t\tstorageSize     int\n\t\tsetup           func(Storage) Record\n\t}{\n\t\t{\n\t\t\tname:        \"create first record\",\n\t\t\tid:          1,\n\t\t\tkey:         \"0000001\",\n\t\t\tfieldValue:  \"foo\",\n\t\t\tstorageSize: 1,\n\t\t\tsetup:       func(s Storage) Record { return s.NewRecord() },\n\t\t},\n\t\t{\n\t\t\tname:        \"create second record\",\n\t\t\tid:          2,\n\t\t\tkey:         \"0000002\",\n\t\t\tfieldValue:  \"bar\",\n\t\t\tstorageSize: 2,\n\t\t\tsetup:       func(s Storage) Record { return s.NewRecord() },\n\t\t},\n\t\t{\n\t\t\tname:        \"update second record\",\n\t\t\tid:          2,\n\t\t\tkey:         \"0000002\",\n\t\t\tfieldValue:  \"baz\",\n\t\t\tstorageSize: 2,\n\t\t\tsetup: func(s Storage) Record {\n\t\t\t\tr, _ := storage.Get(index.Name(), \"bar\")\n\t\t\t\treturn r\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tr := tc.setup(storage)\n\t\t\tr.Set(\"name\", tc.fieldValue)\n\n\t\t\t_, found := storage.Get(index.Name(), tc.fieldValue)\n\t\t\turequire.False(t, found, \"record not found\")\n\t\t\turequire.True(t, r.Save(), \"save success\")\n\t\t\tuassert.Equal(t, tc.storageSize, storage.Size())\n\n\t\t\tr, found = storage.Get(index.Name(), tc.fieldValue)\n\t\t\turequire.True(t, found, \"record found\")\n\t\t\tuassert.Equal(t, tc.id, r.ID())\n\t\t\tuassert.Equal(t, tc.key, r.Key())\n\t\t\tuassert.Equal(t, tc.fieldValue, r.MustGet(\"name\"))\n\t\t})\n\t}\n}\n\nfunc TestRecordsetIterate(t *testing.T) {\n\tcases := []struct {\n\t\tname      string\n\t\trecordIDs []uint64\n\t\tsetup     func(*Storage)\n\t}{\n\t\t{\n\t\t\tname:      \"single record\",\n\t\t\trecordIDs: []uint64{1},\n\t\t\tsetup: func(s *Storage) {\n\t\t\t\ts.NewRecord().Save()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"two records\",\n\t\t\trecordIDs: []uint64{1, 2},\n\t\t\tsetup: func(s *Storage) {\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\ts.NewRecord().Save()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"empty recordset\",\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Arrange\n\t\t\tstorage := NewStorage(\"foo\")\n\t\t\tif tc.setup != nil {\n\t\t\t\ttc.setup(\u0026storage)\n\t\t\t}\n\n\t\t\tvar (\n\t\t\t\trecords []Record\n\t\t\t\trs      = storage.MustQuery()\n\t\t\t)\n\n\t\t\t// Act\n\t\t\trs.Iterate(func(r Record) bool {\n\t\t\t\trecords = append(records, r)\n\t\t\t\treturn false\n\t\t\t})\n\n\t\t\t// Assert\n\t\t\turequire.Equal(t, len(tc.recordIDs), len(records), \"results count\")\n\t\t\tfor i, r := range records {\n\t\t\t\tuassert.Equal(t, tc.recordIDs[i], r.ID())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRecordsetReverseIterate(t *testing.T) {\n\tcases := []struct {\n\t\tname      string\n\t\trecordIDs []uint64\n\t\tsetup     func(*Storage)\n\t}{\n\t\t{\n\t\t\tname:      \"single record\",\n\t\t\trecordIDs: []uint64{1},\n\t\t\tsetup: func(s *Storage) {\n\t\t\t\ts.NewRecord().Save()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"two records\",\n\t\t\trecordIDs: []uint64{2, 1},\n\t\t\tsetup: func(s *Storage) {\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\ts.NewRecord().Save()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"empty recordser\",\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Arrange\n\t\t\tstorage := NewStorage(\"foo\")\n\t\t\tif tc.setup != nil {\n\t\t\t\ttc.setup(\u0026storage)\n\t\t\t}\n\n\t\t\tvar (\n\t\t\t\trecords []Record\n\t\t\t\trs      = storage.MustQuery()\n\t\t\t)\n\n\t\t\t// Act\n\t\t\trs.ReverseIterate(func(r Record) bool {\n\t\t\t\trecords = append(records, r)\n\t\t\t\treturn false\n\t\t\t})\n\n\t\t\t// Assert\n\t\t\turequire.Equal(t, len(tc.recordIDs), len(records), \"results count\")\n\t\t\tfor i, r := range records {\n\t\t\t\tuassert.Equal(t, tc.recordIDs[i], r.ID())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRecordsetSize(t *testing.T) {\n\tcases := []struct {\n\t\tname  string\n\t\tsize  int\n\t\tsetup func(*Storage)\n\t}{\n\t\t{\n\t\t\tname: \"single record\",\n\t\t\tsize: 1,\n\t\t\tsetup: func(s *Storage) {\n\t\t\t\ts.NewRecord().Save()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"two records\",\n\t\t\tsize: 2,\n\t\t\tsetup: func(s *Storage) {\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\ts.NewRecord().Save()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"empty recordser\",\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Arrange\n\t\t\tstorage := NewStorage(\"foo\")\n\t\t\tif tc.setup != nil {\n\t\t\t\ttc.setup(\u0026storage)\n\t\t\t}\n\n\t\t\trs := storage.MustQuery()\n\n\t\t\t// Act\n\t\t\tsize := rs.Size()\n\n\t\t\t// Assert\n\t\t\tuassert.Equal(t, tc.size, size)\n\t\t})\n\t}\n}\n"},{"name":"schema.gno","body":"package datastore\n\nimport (\n\t\"encoding/binary\"\n\n\t\"gno.land/p/nt/avl/list/v0\"\n\t\"gno.land/p/nt/avl/v0\"\n)\n\n// TODO: Support versioning\n\n// Schema contains information about fields and default field values.\n// It also offers the possibility to configure it as static to indicate\n// that only configured fields should be allowed.\ntype Schema struct {\n\tname     string\n\tstrict   bool\n\tfields   list.List // int(field index) -\u003e string(field name)\n\tdefaults avl.Tree  // string(field index) -\u003e interface{}\n}\n\n// NewSchema creates a new schema.\nfunc NewSchema(name string, options ...SchemaOption) *Schema {\n\ts := \u0026Schema{name: name}\n\tfor _, apply := range options {\n\t\tapply(s)\n\t}\n\treturn s\n}\n\n// Name returns schema's name.\nfunc (s Schema) Name() string {\n\treturn s.name\n}\n\n// Fields returns the list field names that are defined in the schema.\nfunc (s Schema) Fields() []string {\n\tfields := make([]string, s.fields.Len())\n\ts.fields.ForEach(func(i int, v interface{}) bool {\n\t\tfields[i] = v.(string)\n\t\treturn false\n\t})\n\treturn fields\n}\n\n// Size returns the number of fields the schema has.\nfunc (s Schema) Size() int {\n\treturn s.fields.Len()\n}\n\n// IsStrict check if the schema is configured as a strict one.\nfunc (s Schema) IsStrict() bool {\n\treturn s.strict\n}\n\n// HasField check is a field has been defined in the schema.\nfunc (s Schema) HasField(name string) bool {\n\treturn s.GetFieldIndex(name) \u003e= 0\n}\n\n// AddField adds a new field to the schema.\n// A default field value can be specified, otherwise `defaultValue` must be nil.\nfunc (s *Schema) AddField(name string, defaultValue interface{}) (index int, added bool) {\n\tif s.HasField(name) {\n\t\treturn -1, false\n\t}\n\n\ts.fields.Append(name)\n\tindex = s.fields.Len() - 1\n\tif defaultValue != nil {\n\t\tkey := castIntToKey(index)\n\t\ts.defaults.Set(key, defaultValue)\n\t}\n\treturn index, true\n}\n\n// GetFieldIndex returns the index number of a schema field.\n//\n// Field index indicates the order the field has within the schema.\n// When defined fields are added they get an index starting from\n// field index 0.\n//\n// Fields are internally referenced by index number instead of the name\n// to be able to rename fields easily.\nfunc (s Schema) GetFieldIndex(name string) int {\n\tindex := -1\n\ts.fields.ForEach(func(i int, v interface{}) bool {\n\t\tif name != v.(string) {\n\t\t\treturn false\n\t\t}\n\n\t\tindex = i\n\t\treturn true\n\t})\n\treturn index\n}\n\n// GetFieldName returns the name of a field for a specific field index.\nfunc (s Schema) GetFieldName(index int) (name string, found bool) {\n\tv := s.fields.Get(index)\n\tif v == nil {\n\t\treturn \"\", false\n\t}\n\treturn v.(string), true\n}\n\n// GetDefault returns the default value for a field.\nfunc (s Schema) GetDefault(name string) (value interface{}, found bool) {\n\ti := s.GetFieldIndex(name)\n\tif i == -1 {\n\t\treturn nil, false\n\t}\n\treturn s.GetDefaultByIndex(i)\n}\n\n// GetDefaultByIndex returns the default value for a field by it's index.\nfunc (s Schema) GetDefaultByIndex(index int) (value interface{}, found bool) {\n\tkey := castIntToKey(index)\n\tv := s.defaults.Get(key)\n\tif v == nil {\n\t\treturn nil, false\n\t}\n\n\tif fn, ok := v.(func() interface{}); ok {\n\t\treturn fn(), true\n\t}\n\treturn v, true\n}\n\n// RenameField renames a field.\nfunc (s *Schema) RenameField(name, newName string) (renamed bool) {\n\tif s.HasField(newName) {\n\t\treturn false\n\t}\n\n\ti := s.GetFieldIndex(name)\n\tif i == -1 {\n\t\treturn false\n\t}\n\n\ts.fields.Set(i, newName)\n\treturn true\n}\n\nfunc castIntToKey(i int) string {\n\tbuf := make([]byte, 8)\n\tbinary.BigEndian.PutUint64(buf, uint64(i))\n\treturn string(buf)\n}\n"},{"name":"schema_options.gno","body":"package datastore\n\nimport \"strings\"\n\n// StorageOption configures schemas.\ntype SchemaOption func(*Schema)\n\n// WithField assign a new field to the schema definition.\nfunc WithField(name string) SchemaOption {\n\treturn func(s *Schema) {\n\t\tname = strings.TrimSpace(name)\n\t\tif name != \"\" {\n\t\t\ts.fields.Append(name)\n\t\t}\n\t}\n}\n\n// WithDefaultField assign a new field with a default value to the schema definition.\n// Default value is assigned to newly created records asociated to to schema.\nfunc WithDefaultField(name string, value interface{}) SchemaOption {\n\treturn func(s *Schema) {\n\t\tname = strings.TrimSpace(name)\n\t\tif name != \"\" {\n\t\t\ts.fields.Append(name)\n\n\t\t\tkey := castIntToKey(s.fields.Len() - 1)\n\t\t\ts.defaults.Set(key, value)\n\t\t}\n\t}\n}\n\n// Strict configures the schema as a strict one.\n// By default schemas should allow the creation of any user defined field,\n// making them strict limits the allowed record fields to the ones pre-defined\n// in the schema. Fields are pre-defined using `WithField`, `WithDefaultField`\n// or by calling `Schema.AddField()`.\nfunc Strict() SchemaOption {\n\treturn func(s *Schema) {\n\t\ts.strict = true\n\t}\n}\n"},{"name":"schema_test.gno","body":"package datastore\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestSchemaNew(t *testing.T) {\n\tcases := []struct {\n\t\tname    string\n\t\toptions []SchemaOption\n\t\tfields  []string\n\t\tstrict  bool\n\t}{\n\t\t{\n\t\t\tname: \"default\",\n\t\t},\n\t\t{\n\t\t\tname:    \"strict\",\n\t\t\toptions: []SchemaOption{Strict()},\n\t\t\tstrict:  true,\n\t\t},\n\t\t{\n\t\t\tname: \"with fields\",\n\t\t\toptions: []SchemaOption{\n\t\t\t\tWithField(\"foo\"),\n\t\t\t\tWithField(\"bar\"),\n\t\t\t\tWithDefaultField(\"baz\", 42),\n\t\t\t},\n\t\t\tfields: []string{\"foo\", \"bar\", \"baz\"},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\ts := NewSchema(\"Foo\", tc.options...)\n\n\t\t\tuassert.Equal(t, \"Foo\", s.Name())\n\t\t\tuassert.Equal(t, tc.strict, s.IsStrict())\n\t\t\turequire.Equal(t, len(tc.fields), s.Size(), \"field count\")\n\n\t\t\tfor i, name := range s.Fields() {\n\t\t\t\tuassert.Equal(t, tc.fields[i], name)\n\t\t\t\tuassert.True(t, s.HasField(name))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSchemaAddField(t *testing.T) {\n\tcases := []struct {\n\t\tname       string\n\t\toptions    []SchemaOption\n\t\tfieldName  string\n\t\tfieldIndex int\n\t\tfields     []string\n\t\tsuccess    bool\n\t}{\n\t\t{\n\t\t\tname:       \"new only field\",\n\t\t\tfieldName:  \"foo\",\n\t\t\tfieldIndex: 0,\n\t\t\tfields:     []string{\"foo\"},\n\t\t\tsuccess:    true,\n\t\t},\n\t\t{\n\t\t\tname: \"new existing fields\",\n\t\t\toptions: []SchemaOption{\n\t\t\t\tWithField(\"foo\"),\n\t\t\t\tWithField(\"bar\"),\n\t\t\t},\n\t\t\tfieldName:  \"baz\",\n\t\t\tfieldIndex: 2,\n\t\t\tfields:     []string{\"foo\", \"bar\", \"baz\"},\n\t\t\tsuccess:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"duplicated field\",\n\t\t\toptions:    []SchemaOption{WithField(\"foo\")},\n\t\t\tfieldName:  \"foo\",\n\t\t\tfieldIndex: -1,\n\t\t\tfields:     []string{\"foo\"},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\ts := NewSchema(\"Foo\", tc.options...)\n\n\t\t\tindex, added := s.AddField(tc.fieldName, nil)\n\n\t\t\tif tc.success {\n\t\t\t\tuassert.Equal(t, tc.fieldIndex, index)\n\t\t\t\tuassert.True(t, added)\n\t\t\t} else {\n\t\t\t\tuassert.Equal(t, -1, index)\n\t\t\t\tuassert.False(t, added)\n\t\t\t}\n\n\t\t\turequire.Equal(t, len(tc.fields), s.Size(), \"field count\")\n\n\t\t\tfor i, name := range s.Fields() {\n\t\t\t\tuassert.Equal(t, tc.fields[i], name)\n\t\t\t\tuassert.True(t, s.HasField(name))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSchemaGetFieldIndex(t *testing.T) {\n\ts := NewSchema(\"Foo\")\n\ts.AddField(\"foo\", nil)\n\ts.AddField(\"bar\", nil)\n\ts.AddField(\"baz\", nil)\n\n\tuassert.Equal(t, 0, s.GetFieldIndex(\"foo\"))\n\tuassert.Equal(t, 1, s.GetFieldIndex(\"bar\"))\n\tuassert.Equal(t, 2, s.GetFieldIndex(\"baz\"))\n\n\tuassert.Equal(t, -1, s.GetFieldIndex(\"\"))\n\tuassert.Equal(t, -1, s.GetFieldIndex(\"unknown\"))\n}\n\nfunc TestSchemaGetFieldName(t *testing.T) {\n\ts := NewSchema(\"Foo\")\n\ts.AddField(\"foo\", nil)\n\ts.AddField(\"bar\", nil)\n\ts.AddField(\"baz\", nil)\n\n\tname, found := s.GetFieldName(0)\n\tuassert.Equal(t, \"foo\", name)\n\tuassert.True(t, found)\n\n\tname, found = s.GetFieldName(1)\n\tuassert.Equal(t, \"bar\", name)\n\tuassert.True(t, found)\n\n\tname, found = s.GetFieldName(2)\n\tuassert.Equal(t, \"baz\", name)\n\tuassert.True(t, found)\n\n\tname, found = s.GetFieldName(404)\n\tuassert.Equal(t, \"\", name)\n\tuassert.False(t, found)\n}\n\nfunc TestSchemaGetDefault(t *testing.T) {\n\ts := NewSchema(\"Foo\")\n\ts.AddField(\"foo\", nil)\n\ts.AddField(\"bar\", 42)\n\n\t_, found := s.GetDefault(\"foo\")\n\tuassert.False(t, found)\n\n\tv, found := s.GetDefault(\"bar\")\n\tuassert.True(t, found)\n\n\tgot, ok := v.(int)\n\turequire.True(t, ok, \"default field value\")\n\tuassert.Equal(t, 42, got)\n}\n\nfunc TestSchemaGetDefaultByIndex(t *testing.T) {\n\ts := NewSchema(\"Foo\")\n\ts.AddField(\"foo\", nil)\n\ts.AddField(\"bar\", 42)\n\n\t_, found := s.GetDefaultByIndex(0)\n\tuassert.False(t, found)\n\n\t_, found = s.GetDefaultByIndex(404)\n\tuassert.False(t, found)\n\n\tv, found := s.GetDefaultByIndex(1)\n\tuassert.True(t, found)\n\n\tgot, ok := v.(int)\n\turequire.True(t, ok, \"default field value\")\n\tuassert.Equal(t, 42, got)\n}\n\nfunc TestSchemaRenameField(t *testing.T) {\n\ts := NewSchema(\"Foo\")\n\ts.AddField(\"foo\", nil)\n\ts.AddField(\"bar\", nil)\n\n\trenamed := s.RenameField(\"foo\", \"bar\")\n\tuassert.False(t, renamed)\n\n\trenamed = s.RenameField(\"\", \"baz\")\n\tuassert.False(t, renamed)\n\n\trenamed = s.RenameField(\"foo\", \"\")\n\tuassert.True(t, renamed)\n\n\trenamed = s.RenameField(\"\", \"foo\")\n\tuassert.True(t, renamed)\n\n\trenamed = s.RenameField(\"foo\", \"foobar\")\n\tuassert.True(t, renamed)\n\n\turequire.Equal(t, 2, s.Size(), \"field count\")\n\tfields := []string{\"foobar\", \"bar\"}\n\tfor i, name := range s.Fields() {\n\t\tuassert.Equal(t, fields[i], name)\n\t\tuassert.True(t, s.HasField(name))\n\t}\n}\n"},{"name":"storage.gno","body":"package datastore\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\n\t\"gno.land/p/moul/collection\"\n\t\"gno.land/p/nt/seqid/v0\"\n)\n\n// NewStorage creates a new records storage.\nfunc NewStorage(name string, options ...StorageOption) Storage {\n\ts := Storage{\n\t\tname:       name,\n\t\tcollection: collection.New(),\n\t\tschema:     NewSchema(strings.Title(name)),\n\t}\n\n\tfor _, apply := range options {\n\t\tapply(\u0026s)\n\t}\n\treturn s\n}\n\n// Storage stores a collection of records.\n//\n// By default it searches records by record ID but it allows\n// using custom user defined indexes for other record fields.\n//\n// When a storage is created it defines a default schema that\n// keeps track of record fields. Storage can be optionally\n// created with a user defined schema in cases where the number\n// of fields has to be pre-defined or when new records must have\n// one or more fields initialized to default values.\ntype Storage struct {\n\tname       string\n\tcollection *collection.Collection\n\tschema     *Schema\n}\n\n// Name returns storage's name.\nfunc (s Storage) Name() string {\n\treturn s.name\n}\n\n// Collection returns the undelying collection used by the\n// storage to store all records.\nfunc (s Storage) Collection() *collection.Collection {\n\treturn s.collection\n}\n\n// Schema returns the schema being used to track record fields.\nfunc (s Storage) Schema() *Schema {\n\treturn s.schema\n}\n\n// Size returns the number of records that the storage have.\nfunc (s Storage) Size() int {\n\treturn s.collection.GetIndex(collection.IDIndex).Size()\n}\n\n// NewRecord creates a new storage record.\n//\n// If a custom schema with default field values is assigned to\n// storage it's used to assign initial default values when new\n// records are created.\n//\n// Creating a new record doesn't assign an ID to it, a new ID\n// is generated and assigned to the record when it's saved for\n// the first time.\nfunc (s Storage) NewRecord() Record {\n\tr := \u0026record{\n\t\tschema:     s.schema,\n\t\tcollection: s.collection,\n\t}\n\n\t// Assign default record values if the schema defines them\n\tfor i, name := range s.schema.Fields() {\n\t\tif v, found := s.schema.GetDefaultByIndex(i); found {\n\t\t\tr.Set(name, v)\n\t\t}\n\t}\n\treturn r\n}\n\n// Query returns a recordset that matches the query parameters.\n// By default query selects records using the ID index.\n//\n// Example usage:\n//\n//\t// Get 50 records starting from the one at position 100\n//\trs, _ := storage.Query(\n//\t\tWithOffset(100),\n//\t\tWithSize(50),\n//\t)\n//\n//\t// Iterate records to create a new slice\n//\tvar records []Record\n//\trs.Iterate(func (r Record) bool {\n//\t\trecords = append(records, r)\n//\t\treturn false\n//\t})\nfunc (s Storage) Query(options ...QueryOption) (Recordset, error) {\n\t// Initialize the recordset for the query\n\trs := recordset{\n\t\tquery:   defaultQuery,\n\t\trecords: s.collection.GetIndex(collection.IDIndex),\n\t}\n\n\tfor _, apply := range options {\n\t\tif err := apply(\u0026rs.query); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tindexName := rs.query.IndexName()\n\tif indexName != collection.IDIndex {\n\t\t// When using a custom index get the keys to get records from the ID index\n\t\tkeys, err := s.getIndexRecordsKeys(indexName, rs.query.IndexKey())\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// Adjust the number of keys to match available query options\n\t\tif offset := rs.query.Offset(); offset \u003e 0 {\n\t\t\tif offset \u003e len(keys) {\n\t\t\t\tkeys = nil\n\t\t\t} else {\n\t\t\t\tkeys = keys[offset:]\n\t\t\t}\n\t\t}\n\n\t\tif size := rs.query.Size(); size \u003e 0 \u0026\u0026 size \u003c len(keys) {\n\t\t\tkeys = keys[:size]\n\t\t}\n\n\t\trs.keys = keys\n\t\trs.size = len(keys)\n\t} else {\n\t\t// When using the default ID index init size with the total number of records\n\t\trs.size = rs.records.Size()\n\n\t\t// Adjust recordset size to match available query options\n\t\tif offset := rs.query.Offset(); offset \u003e 0 {\n\t\t\tif offset \u003e rs.size {\n\t\t\t\trs.size = 0\n\t\t\t} else {\n\t\t\t\trs.size -= offset\n\t\t\t}\n\t\t}\n\n\t\tif size := rs.query.Size(); size \u003e 0 \u0026\u0026 size \u003c rs.size {\n\t\t\trs.size = size\n\t\t}\n\t}\n\n\treturn rs, nil\n}\n\n// MustQuery returns a recordset that matches the query parameters or panics on error.\n// By default query selects records using the ID index.\n//\n// Example usage:\n//\n//\t// Get 50 records starting from the one at position 100\n//\tvar records []Record\n//\tstorage.MustQuery(\n//\t\tWithOffset(100),\n//\t\tWithSize(50),\n//\t).Iterate(func (r Record) bool {\n//\t\trecords = append(records, r)\n//\t\treturn false\n//\t})\nfunc (s Storage) MustQuery(options ...QueryOption) Recordset {\n\trs, err := s.Query(options...)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn rs\n}\n\n// Get returns the first record found for a key within a storage index.\n//\n// This is a convenience method to get a single record. A multi index will\n// always return the first record value for the specified key in this case.\n// To get multiple records create a query using a custom index and key value\n// or use the underlying storage collection.\nfunc (s Storage) Get(indexName, indexKey string) (_ Record, found bool) {\n\titer := s.collection.Get(indexName, indexKey)\n\tif iter.Next() {\n\t\treturn iter.Value().Obj.(Record), true\n\t}\n\treturn nil, false\n}\n\n// GetByID returns a record whose ID matches the specified ID.\nfunc (s Storage) GetByID(id uint64) (_ Record, found bool) {\n\titer := s.collection.Get(collection.IDIndex, seqid.ID(id).String())\n\tif iter.Next() {\n\t\treturn iter.Value().Obj.(Record), true\n\t}\n\treturn nil, false\n}\n\n// Delete deletes a record from the storage.\nfunc (s Storage) Delete(id uint64) bool {\n\treturn s.collection.Delete(id)\n}\n\nfunc (s Storage) getIndexRecordsKeys(indexName, indexKey string) ([]string, error) {\n\tidx := s.collection.GetIndex(indexName)\n\tif idx == nil {\n\t\treturn nil, errors.New(\"storage index for query not found: \" + indexName)\n\t}\n\n\tvar keys []string\n\tif v := idx.Get(indexKey); v != nil {\n\t\tkeys = castIfaceToRecordKeys(v)\n\t\tif keys == nil {\n\t\t\treturn nil, errors.New(\"unexpected storage index key format\")\n\t\t}\n\t}\n\treturn keys, nil\n}\n\nfunc castIfaceToRecordKeys(v interface{}) []string {\n\tswitch k := v.(type) {\n\tcase []string:\n\t\treturn k\n\tcase string:\n\t\treturn []string{k}\n\t}\n\treturn nil\n}\n"},{"name":"storage_options.gno","body":"package datastore\n\n// StorageOption configures storages.\ntype StorageOption func(*Storage)\n\n// WithSchema assigns a schema to the storage.\nfunc WithSchema(s *Schema) StorageOption {\n\treturn func(st *Storage) {\n\t\tif s != nil {\n\t\t\tst.schema = s\n\t\t}\n\t}\n}\n\n// WithIndex assigns an index to the storage.\nfunc WithIndex(i Index) StorageOption {\n\treturn func(st *Storage) {\n\t\tst.collection.AddIndex(i.name, i.fn, i.options)\n\t}\n}\n"},{"name":"storage_test.gno","body":"package datastore\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestStorageDefaults(t *testing.T) {\n\tname := \"foo\"\n\tstorage := NewStorage(name)\n\n\tuassert.Equal(t, name, storage.Name())\n\tuassert.NotEqual(t, nil, storage.Collection())\n\tuassert.Equal(t, 0, storage.Size())\n\n\ts := storage.Schema()\n\tuassert.NotEqual(t, nil, s)\n\tuassert.Equal(t, strings.Title(name), s.Name())\n}\n\nfunc TestStorageNewRecord(t *testing.T) {\n\tfield := \"status\"\n\tdefaultValue := \"testing\"\n\ts := NewSchema(\"Foo\", WithDefaultField(field, defaultValue))\n\tstorage := NewStorage(\"foo\", WithSchema(s))\n\n\tr := storage.NewRecord()\n\turequire.NotEqual(t, nil, r, \"new record is not nil\")\n\tuassert.Equal(t, uint64(0), r.ID())\n\tuassert.Equal(t, storage.Schema().Name(), r.Type())\n\n\tv, found := r.Get(field)\n\turequire.True(t, found, \"default value found\")\n\n\tgot, ok := v.(string)\n\turequire.True(t, ok, \"default value type\")\n\tuassert.Equal(t, defaultValue, got)\n}\n\nfunc TestStorageQuery(t *testing.T) {\n\tindex := NewIndex(\"tag\", func(r Record) string {\n\t\tif v, found := r.Get(\"tag\"); found {\n\t\t\treturn v.(string)\n\t\t}\n\t\treturn \"\"\n\t})\n\n\tcases := []struct {\n\t\tname    string\n\t\toptions []QueryOption\n\t\tresults []uint64\n\t\tsetup   func() *Storage\n\t\terrMsg  string\n\t}{\n\t\t{\n\t\t\tname:    \"default query\",\n\t\t\tresults: []uint64{1, 2},\n\t\t\tsetup: func() *Storage {\n\t\t\t\ts := NewStorage(\"foo\")\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\treturn \u0026s\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"with size\",\n\t\t\tresults: []uint64{1},\n\t\t\toptions: []QueryOption{WithSize(1)},\n\t\t\tsetup: func() *Storage {\n\t\t\t\ts := NewStorage(\"foo\")\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\treturn \u0026s\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"with offset\",\n\t\t\tresults: []uint64{2},\n\t\t\toptions: []QueryOption{WithOffset(1)},\n\t\t\tsetup: func() *Storage {\n\t\t\t\ts := NewStorage(\"foo\")\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\treturn \u0026s\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"with offset overflow\",\n\t\t\toptions: []QueryOption{WithOffset(4)},\n\t\t\tsetup: func() *Storage {\n\t\t\t\ts := NewStorage(\"foo\")\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\treturn \u0026s\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"with size and offset\",\n\t\t\tresults: []uint64{2, 3},\n\t\t\toptions: []QueryOption{WithSize(2), WithOffset(1)},\n\t\t\tsetup: func() *Storage {\n\t\t\t\ts := NewStorage(\"foo\")\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\treturn \u0026s\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"custom index\",\n\t\t\toptions: []QueryOption{UseIndex(\"tag\", \"A\")},\n\t\t\tresults: []uint64{1, 3},\n\t\t\tsetup: func() *Storage {\n\t\t\t\ts := NewStorage(\"foo\", WithIndex(index))\n\n\t\t\t\tr := s.NewRecord()\n\t\t\t\tr.Set(\"tag\", \"A\")\n\t\t\t\tr.Save()\n\n\t\t\t\tr = s.NewRecord()\n\t\t\t\tr.Set(\"tag\", \"B\")\n\t\t\t\tr.Save()\n\n\t\t\t\tr = s.NewRecord()\n\t\t\t\tr.Set(\"tag\", \"A\")\n\t\t\t\tr.Save()\n\n\t\t\t\treturn \u0026s\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"custom index with offset\",\n\t\t\toptions: []QueryOption{UseIndex(\"tag\", \"B\"), WithOffset(1)},\n\t\t\tresults: []uint64{3, 4},\n\t\t\tsetup: func() *Storage {\n\t\t\t\ts := NewStorage(\"foo\", WithIndex(index))\n\n\t\t\t\tr := s.NewRecord()\n\t\t\t\tr.Set(\"tag\", \"B\")\n\t\t\t\tr.Save()\n\n\t\t\t\tr = s.NewRecord()\n\t\t\t\tr.Set(\"tag\", \"A\")\n\t\t\t\tr.Save()\n\n\t\t\t\tr = s.NewRecord()\n\t\t\t\tr.Set(\"tag\", \"B\")\n\t\t\t\tr.Save()\n\n\t\t\t\tr = s.NewRecord()\n\t\t\t\tr.Set(\"tag\", \"B\")\n\t\t\t\tr.Save()\n\n\t\t\t\treturn \u0026s\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"custom index with offset and size\",\n\t\t\toptions: []QueryOption{UseIndex(\"tag\", \"B\"), WithOffset(1), WithSize(1)},\n\t\t\tresults: []uint64{3},\n\t\t\tsetup: func() *Storage {\n\t\t\t\ts := NewStorage(\"foo\", WithIndex(index))\n\n\t\t\t\tr := s.NewRecord()\n\t\t\t\tr.Set(\"tag\", \"B\")\n\t\t\t\tr.Save()\n\n\t\t\t\tr = s.NewRecord()\n\t\t\t\tr.Set(\"tag\", \"A\")\n\t\t\t\tr.Save()\n\n\t\t\t\tr = s.NewRecord()\n\t\t\t\tr.Set(\"tag\", \"B\")\n\t\t\t\tr.Save()\n\n\t\t\t\tr = s.NewRecord()\n\t\t\t\tr.Set(\"tag\", \"B\")\n\t\t\t\tr.Save()\n\n\t\t\t\treturn \u0026s\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"custom index not found\",\n\t\t\toptions: []QueryOption{UseIndex(\"foo\", \"B\")},\n\t\t\tsetup: func() *Storage {\n\t\t\t\ts := NewStorage(\"foo\")\n\t\t\t\treturn \u0026s\n\t\t\t},\n\t\t\terrMsg: \"storage index for query not found: foo\",\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Arrange\n\t\t\tstorage := tc.setup()\n\n\t\t\t// Act\n\t\t\trs, err := storage.Query(tc.options...)\n\n\t\t\t// Assert\n\t\t\tif tc.errMsg != \"\" {\n\t\t\t\tuassert.ErrorContains(t, err, tc.errMsg, \"expect error\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.NoError(t, err, \"expect no error\")\n\t\t\turequire.NotEqual(t, nil, rs, \"new record is not nil\")\n\t\t\turequire.Equal(t, len(tc.results), rs.Size(), \"expect query results count to match\")\n\n\t\t\tvar i int\n\t\t\trs.Iterate(func(r Record) bool {\n\t\t\t\turequire.Equal(t, tc.results[i], r.ID(), \"expect result IDs to match\")\n\t\t\t\ti++\n\t\t\t\treturn false\n\t\t\t})\n\t\t})\n\t}\n}\n\nfunc TestStorageGet(t *testing.T) {\n\tindex := NewIndex(\"name\", func(r Record) string {\n\t\tif v, found := r.Get(\"name\"); found {\n\t\t\treturn v.(string)\n\t\t}\n\t\treturn \"\"\n\t})\n\n\tcases := []struct {\n\t\tname     string\n\t\tkey      string\n\t\trecordID uint64\n\t\tsetup    func(*Storage)\n\t}{\n\t\t{\n\t\t\tname:     \"single record\",\n\t\t\tkey:      \"foobar\",\n\t\t\trecordID: 1,\n\t\t\tsetup: func(s *Storage) {\n\t\t\t\tr := s.NewRecord()\n\t\t\t\tr.Set(\"name\", \"foobar\")\n\t\t\t\tr.Save()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"two records\",\n\t\t\tkey:      \"foobar\",\n\t\t\trecordID: 1,\n\t\t\tsetup: func(s *Storage) {\n\t\t\t\tr := s.NewRecord()\n\t\t\t\tr.Set(\"name\", \"foobar\")\n\t\t\t\tr.Save()\n\n\t\t\t\tr = s.NewRecord()\n\t\t\t\tr.Set(\"name\", \"foobar\")\n\t\t\t\tr.Save()\n\n\t\t\t\tr = s.NewRecord()\n\t\t\t\tr.Set(\"name\", \"extra\")\n\t\t\t\tr.Save()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"record not found\",\n\t\t\tkey:  \"unknown\",\n\t\t\tsetup: func(s *Storage) {\n\t\t\t\tr := s.NewRecord()\n\t\t\t\tr.Set(\"name\", \"foobar\")\n\t\t\t\tr.Save()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"empty storage\",\n\t\t\tkey:  \"foobar\",\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tstorage := NewStorage(\"foo\", WithIndex(index))\n\t\t\tif tc.setup != nil {\n\t\t\t\ttc.setup(\u0026storage)\n\t\t\t}\n\n\t\t\tr, found := storage.Get(index.Name(), tc.key)\n\n\t\t\tif tc.recordID == 0 {\n\t\t\t\tuassert.Equal(t, nil, r, \"expect no record\")\n\t\t\t\tuassert.False(t, found, \"expect record not found\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tuassert.True(t, found, \"expect record found\")\n\t\t\turequire.NotEqual(t, nil, r, \"expect record to be found\")\n\t\t\tuassert.Equal(t, tc.recordID, r.ID(), \"expect ID to match\")\n\t\t})\n\t}\n}\n\nfunc TestStorageGetByID(t *testing.T) {\n\tcases := []struct {\n\t\tname     string\n\t\trecordID uint64\n\t\tfound    bool\n\t\tsetup    func(*Storage)\n\t}{\n\t\t{\n\t\t\tname:     \"single record\",\n\t\t\trecordID: 1,\n\t\t\tfound:    true,\n\t\t\tsetup: func(s *Storage) {\n\t\t\t\ts.NewRecord().Save()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple records\",\n\t\t\trecordID: 2,\n\t\t\tfound:    true,\n\t\t\tsetup: func(s *Storage) {\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\ts.NewRecord().Save()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"record not found\",\n\t\t\trecordID: 3,\n\t\t\tsetup: func(s *Storage) {\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\ts.NewRecord().Save()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"empty storage\",\n\t\t\trecordID: 1,\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tstorage := NewStorage(\"foo\")\n\t\t\tif tc.setup != nil {\n\t\t\t\ttc.setup(\u0026storage)\n\t\t\t}\n\n\t\t\tr, found := storage.GetByID(tc.recordID)\n\n\t\t\tif !tc.found {\n\t\t\t\tuassert.Equal(t, nil, r, \"expect no record\")\n\t\t\t\tuassert.False(t, found, \"expect record not found\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tuassert.True(t, found, \"expect record found\")\n\t\t\turequire.NotEqual(t, nil, r, \"expect record to be found\")\n\t\t\tuassert.Equal(t, tc.recordID, r.ID(), \"expect ID to match\")\n\t\t})\n\t}\n}\n\nfunc TestStorageDelete(t *testing.T) {\n\tstorage := NewStorage(\"foo\")\n\tr := storage.NewRecord()\n\tr.Save()\n\n\tdeleted := storage.Delete(r.ID())\n\tuassert.True(t, deleted)\n\n\tdeleted = storage.Delete(r.ID())\n\tuassert.False(t, deleted)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"JttdEiVbK3lXZG6J6zWm7GP0EcrNfboadr4n51PKVEsfUfT/y2PEX/yWi+Pg6eqAWNI323QhvaoFbzbgtCzTeg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"murmur3","path":"gno.land/p/jeronimoalbi/murmur3","files":[{"name":"README.md","body":"# MurmurHash3 Package\n\nPackage implements Austin Appleby's MurmurHash3 algorithm.\n\nMurmurHash3 is a fast, non-cryptographic hash function well suited for hash tables,\nbloom filters, data partitioning, and deduplication where speed and good distribution\nmatter more than cryptographic security.\n\n## API\n\n- `New32()` — Returns a `hash.Hash32` with seed zero\n- `NewWithSeed32(seed)` — Returns a `hash.Hash32` with the given seed\n- `Sum32(data)` — One-shot hash with seed zero\n- `Sum32WithSeed(data, seed)` — One-shot hash with the given seed\n- `New64()` — Returns a `hash.Hash64` using seeds zero and one\n- `NewWithSeed64(seed1, seed2)` — Returns a `hash.Hash64` with the given seeds\n- `Sum64(data)` — One-shot 64-bit hash using seeds zero and one\n- `Sum64WithSeed(data, seed1, seed2)` — One-shot 64-bit hash with the given seeds\n- `EncodeToString(sum64)` — Returns the hexadecimal encoding of a hash value\n\n## Usage\n\n[embedmd]:# (filetests/readme_filetest.gno go)\n```go\npackage main\n\nimport \"gno.land/p/jeronimoalbi/murmur3\"\n\nfunc main() {\n\tdata := []byte(\"Hello, world!\")\n\tseed := uint32(42)\n\n\t// Hash32 without seed\n\th32 := murmur3.New32()\n\th32.Write(data)\n\tsum32 := uint64(h32.Sum32())\n\tprintln(murmur3.EncodeToString(sum32))\n\n\t// Hash32 with seed\n\th32 = murmur3.NewWithSeed32(seed)\n\th32.Write(data)\n\tsum32 = uint64(h32.Sum32())\n\tprintln(murmur3.EncodeToString(sum32))\n\n\t// Hash32 without seed using a helper function\n\tsum32 = uint64(murmur3.Sum32(data))\n\tprintln(murmur3.EncodeToString(sum32))\n\n\t// Hash32 with seed using a helper function\n\tsum32 = uint64(murmur3.Sum32WithSeed(data, seed))\n\tprintln(murmur3.EncodeToString(sum32))\n\n\t// Hash64 without seed\n\th64 := murmur3.New64()\n\th64.Write(data)\n\tsum64 := h64.Sum64()\n\tprintln(murmur3.EncodeToString(sum64))\n\n\t// Hash64 with seed\n\th64 = murmur3.NewWithSeed64(0, seed)\n\th64.Write(data)\n\tsum64 = h64.Sum64()\n\tprintln(murmur3.EncodeToString(sum64))\n\n\t// Hash64 without seed using a helper function\n\tsum64 = murmur3.Sum64(data)\n\tprintln(murmur3.EncodeToString(sum64))\n\n\t// Hash64 with seed using a helper function\n\tsum64 = murmur3.Sum64WithSeed(data, 0, seed)\n\tprintln(murmur3.EncodeToString(sum64))\n}\n\n// Output:\n// c0363e43\n// 2c8c8533\n// c0363e43\n// 2c8c8533\n// c0363e43aa5dc85b\n// c0363e432c8c8533\n// c0363e43aa5dc85b\n// c0363e432c8c8533\n```\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/jeronimoalbi/murmur3\"\ngno = \"0.9\"\n"},{"name":"murmur32.gno","body":"// Package murmur3 implements the MurmurHash3 hash algorithm.\n//\n// Package implements Austin Appleby's MurmurHash3 algorithm.\n//\n// MurmurHash3 is a fast, non-cryptographic hash function well suited for hash\n// tables, bloom filters, data partitioning, and deduplication where speed and\n// good distribution matter more than cryptographic security.\n//\n// References:\n//\n//\thttps://en.wikipedia.org/wiki/MurmurHash#Algorithm\n//\thttps://github.com/aappleby/smhasher/blob/master/src/MurmurHash3.cpp\npackage murmur3\n\nimport \"strconv\"\n\n// EncodeToString returns the hexadecimal encoding of a hash value.\nfunc EncodeToString(sum uint64) string {\n\treturn strconv.FormatUint(sum, 16)\n}\n"},{"name":"murmur3_32.gno","body":"package murmur3\n\nimport (\n\t\"hash\"\n\t\"math/bits\"\n)\n\nconst (\n\tc1          = 0xcc9e2d51\n\tc2          = 0x1b873593\n\tr1          = 15\n\tr2          = 13\n\tblockSize32 = 4\n\thashSize32  = 4\n)\n\ntype digest32 struct {\n\tseed           uint32\n\th1             uint32\n\ttail           [4]byte\n\ttailBytesCount int\n\twrittenLen     int\n}\n\n// New32 returns a new hash.Hash32 computing the MurmurHash3 hash.\n// It can generate 2 to the power of 32 unique 32-bit values ranging\n// from 0 to 4_294_967_296.\n// Hash is not collision safe, so it's recommended to handle potential\n// collisions when dealing with more than 65_536 values which is when\n// probability raises.\nfunc New32() hash.Hash32 {\n\treturn \u0026digest32{}\n}\n\n// NewWithSeed32 returns a new hash.Hash32 computing the MurmurHash3\n// hash with the given seed.\n// It can generate 2 to the power of 32 unique 32-bit values ranging\n// from 0 to 4_294_967_296.\n// Hash is not collision safe, so it's recommended to handle potential\n// collisions when dealing with more than 65_536 values which is when\n// provability raises.\nfunc NewWithSeed32(seed uint32) hash.Hash32 {\n\treturn \u0026digest32{seed: seed, h1: seed}\n}\n\n// Sum32 returns the MurmurHash3 hash of the specified data.\nfunc Sum32(data []byte) uint32 {\n\treturn Sum32WithSeed(data, 0)\n}\n\n// Sum32WithSeed returns the MurmurHash3 hash of the specified data\n// using the given seed.\nfunc Sum32WithSeed(data []byte, seed uint32) uint32 {\n\th := NewWithSeed32(seed)\n\th.Write(data)\n\treturn h.Sum32()\n}\n\nfunc (digest32) BlockSize() int {\n\treturn blockSize32\n}\n\nfunc (digest32) Size() int {\n\treturn hashSize32\n}\n\nfunc (d *digest32) Reset() {\n\td.h1 = d.seed\n\td.tailBytesCount = 0\n\td.writtenLen = 0\n}\n\nfunc (d *digest32) Write(bz []byte) (int, error) {\n\t// Keep track of all written bytes\n\ttotal := len(bz)\n\td.writtenLen += total\n\n\t// If there are leftover bytes in the tail,\n\t// try to fill a block with the new data.\n\tif d.tailBytesCount \u003e 0 {\n\t\tneed := blockSize32 - d.tailBytesCount\n\t\tif len(bz) \u003c need {\n\t\t\t// Append more data to the block\n\t\t\tcopy(d.tail[d.tailBytesCount:], bz)\n\t\t\td.tailBytesCount += len(bz)\n\n\t\t\t// Tail block is still not filled\n\t\t\treturn total, nil\n\t\t}\n\n\t\t// Append remaining block data and process current block\n\t\tcopy(d.tail[d.tailBytesCount:], bz[:need])\n\t\td.processBlock(d.tail[:])\n\n\t\t// Update data to exclude the appended processed data\n\t\tbz = bz[need:]\n\t\td.tailBytesCount = 0\n\t}\n\n\t// Process full 4-byte blocks\n\tfor len(bz) \u003e= blockSize32 {\n\t\td.processBlock(bz[:blockSize32])\n\t\tbz = bz[blockSize32:]\n\t}\n\n\t// Save remaining bytes in the tail. Remaining data can be left\n\t// out of the last block when data size is less than 4 bytes.\n\tif len(bz) \u003e 0 {\n\t\td.tailBytesCount = copy(d.tail[:], bz)\n\t}\n\n\treturn total, nil\n}\n\nfunc (d digest32) Sum32() uint32 {\n\th1 := d.h1\n\n\t// Process tail bytes when they exist. Blocks have 4 bytes, so depending\n\t// on the size of the data some bytes might not be processed after the last\n\t// write if the last block has less than 4 bytes.\n\tvar k1 uint32\n\tswitch d.tailBytesCount {\n\tcase 3:\n\t\tk1 ^= uint32(d.tail[2]) \u003c\u003c 16\n\t\tfallthrough\n\tcase 2:\n\t\tk1 ^= uint32(d.tail[1]) \u003c\u003c 8\n\t\tfallthrough\n\tcase 1:\n\t\tk1 ^= uint32(d.tail[0])\n\t\tk1 *= c1\n\t\tk1 = bits.RotateLeft32(k1, r1)\n\t\tk1 *= c2\n\t\th1 ^= k1\n\t}\n\n\t// Finalization mix\n\th1 ^= uint32(d.writtenLen)\n\n\th1 ^= h1 \u003e\u003e 16\n\th1 *= 0x85ebca6b\n\th1 ^= h1 \u003e\u003e r2\n\th1 *= 0xc2b2ae35\n\th1 ^= h1 \u003e\u003e 16\n\n\treturn h1\n}\n\nfunc (d digest32) Sum(bz []byte) []byte {\n\t// Append hash in little-endian order\n\ts := d.Sum32()\n\treturn append(bz, byte(s), byte(s\u003e\u003e8), byte(s\u003e\u003e16), byte(s\u003e\u003e24))\n}\n\n// processBlock reads a 4-byte little-endian block\n// and mixes it into the hash state.\nfunc (d *digest32) processBlock(bz []byte) {\n\tk1 := uint32(bz[0]) | uint32(bz[1])\u003c\u003c8 | uint32(bz[2])\u003c\u003c16 | uint32(bz[3])\u003c\u003c24\n\tk1 *= c1\n\tk1 = bits.RotateLeft32(k1, r1)\n\tk1 *= c2\n\n\td.h1 ^= k1\n\td.h1 = bits.RotateLeft32(d.h1, r2)\n\td.h1 = d.h1*5 + 0xe6546b64 // MurmurHash3 \"m\" = 5, \"n\" = 0xe6546b64\n}\n"},{"name":"murmur3_32_test.gno","body":"package murmur3_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/jeronimoalbi/murmur3\"\n)\n\nfunc TestHash32Sum32(t *testing.T) {\n\t// Test cases -\u003e https://en.wikipedia.org/wiki/MurmurHash#Algorithm\n\ttests := []struct {\n\t\tname string\n\t\tdata string\n\t\tseed uint32\n\t\twant uint32\n\t}{\n\t\t{\"empty with seed 0\", \"\", 0, 0},\n\t\t{\"empty with seed 1\", \"\", 1, 0x514e28b7},\n\t\t{\"empty with seed max\", \"\", 0xffffffff, 0x81f16f39},\n\t\t{\"test\", \"test\", 0, 0xba6bd213},\n\t\t{\"test with seed\", \"test\", 0x9747b28c, 0x704b81dc},\n\t\t{\"hello\", \"Hello, world!\", 0, 0xc0363e43},\n\t\t{\"hello with seed\", \"Hello, world!\", 42, 0x2c8c8533},\n\t\t{\"hello with seed 0x9747b28c\", \"Hello, world!\", 0x9747b28c, 0x24884cba},\n\t\t{\"pangram\", \"The quick brown fox jumps over the lazy dog\", 0, 0x2e4ff723},\n\t\t{\"pangram with seed\", \"The quick brown fox jumps over the lazy dog\", 0x9747b28c, 0x2fa826cd},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := murmur3.Sum32WithSeed([]byte(tt.data), tt.seed)\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"Sum32WithSeed(%q, %d) = 0x%08x, want 0x%08x\", tt.data, tt.seed, got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHash32InterfaceSizes(t *testing.T) {\n\th := murmur3.New32()\n\tif h.Size() != 4 {\n\t\tt.Errorf(\"Size() = %d, want 4\", h.Size())\n\t}\n\tif h.BlockSize() != 4 {\n\t\tt.Errorf(\"BlockSize() = %d, want 4\", h.BlockSize())\n\t}\n}\n\nfunc TestHash32IncrementalWrite(t *testing.T) {\n\twant := uint32(3224780355)\n\tdata := []byte(\"Hello, world!\")\n\n\t// Hash one byte at a time\n\th := murmur3.New32()\n\tfor _, b := range data {\n\t\th.Write([]byte{b})\n\t}\n\n\tif got := h.Sum32(); got != want {\n\t\tt.Errorf(\"incremental Write got 0x%08x, want 0x%08x\", got, want)\n\t}\n\n\t// Hash in two parts\n\th.Reset()\n\th.Write(data[:5])\n\th.Write(data[5:])\n\tif got := h.Sum32(); got != want {\n\t\tt.Errorf(\"two-part Write got 0x%08x, want 0x%08x\", got, want)\n\t}\n}\n\nfunc TestHash32Sum(t *testing.T) {\n\th := murmur3.New32()\n\th.Write([]byte(\"Hello, world!\"))\n\tb := h.Sum(nil)\n\tif len(b) != 4 {\n\t\tt.Fatalf(\"Sum(nil) returned %d bytes, want 4\", len(b))\n\t}\n\n\tgot := uint32(b[0]) | uint32(b[1])\u003c\u003c8 | uint32(b[2])\u003c\u003c16 | uint32(b[3])\u003c\u003c24\n\twant := uint32(3224780355)\n\tif got != want {\n\t\tt.Errorf(\"Sum got 0x%08x, want 0x%08x\", got, want)\n\t}\n}\n\nfunc TestHash32SumDoesNotChangeState(t *testing.T) {\n\th := murmur3.New32()\n\th.Write([]byte(\"Hello\"))\n\ts1 := h.Sum32()\n\ts2 := h.Sum32()\n\tif s1 != s2 {\n\t\tt.Errorf(\"Sum32 changed state: first 0x%08x, second 0x%08x\", s1, s2)\n\t}\n}\n\nfunc TestHash32Reset(t *testing.T) {\n\th := murmur3.New32()\n\th.Write([]byte(\"some data\"))\n\th.Reset()\n\th.Write([]byte(\"Hello, world!\"))\n\tgot := h.Sum32()\n\twant := uint32(3224780355)\n\tif got != want {\n\t\tt.Errorf(\"after Reset got 0x%08x, want 0x%08x\", got, want)\n\t}\n}\n"},{"name":"murmur3_64.gno","body":"package murmur3\n\nimport \"hash\"\n\nconst hashSize64 = 8\n\ntype digest64 struct {\n\td1 digest32\n\td2 digest32\n}\n\n// New64 returns a new hash.Hash64 computing a 64-bit MurmurHash3\n// hash using two 32-bit hashes with different seeds.\n// It can generate 2 to the power of 64 unique 64-bit values.\n// Hash is not collision safe, so it's recommended to handle potential\n// collisions when dealing with more than 4_294_967_296 values which is\n// when probability raises.\nfunc New64() hash.Hash64 {\n\treturn \u0026digest64{\n\t\td1: digest32{},\n\t\td2: digest32{seed: 1, h1: 1},\n\t}\n}\n\n// NewWithSeed64 returns a new hash.Hash64 computing a 64-bit\n// MurmurHash3 hash. The first 32-bit hash uses seed1 and the\n// second uses seed2. The two seeds should ideally be different.\n// It can generate 2 to the power of 64 unique 64-bit values.\n// Hash is not collision safe, so it's recommended to handle potential\n// collisions when dealing with more than 4_294_967_296 values which is\n// when provability raises.\nfunc NewWithSeed64(seed1, seed2 uint32) hash.Hash64 {\n\treturn \u0026digest64{\n\t\td1: digest32{seed: seed1, h1: seed1},\n\t\td2: digest32{seed: seed2, h1: seed2},\n\t}\n}\n\n// Sum64 returns the 64-bit MurmurHash3 hash of the specified data.\nfunc Sum64(data []byte) uint64 {\n\treturn Sum64WithSeed(data, 0, 1)\n}\n\n// Sum64WithSeed returns the 64-bit MurmurHash3 hash of the specified\n// data using the given seeds.\nfunc Sum64WithSeed(data []byte, seed1, seed2 uint32) uint64 {\n\th := NewWithSeed64(seed1, seed2)\n\th.Write(data)\n\treturn h.Sum64()\n}\n\nfunc (digest64) BlockSize() int {\n\t// Use a 32-bit hash block size because writes are\n\t// done to two hash32 hashes using the same data.\n\treturn blockSize32\n}\n\nfunc (digest64) Size() int {\n\treturn hashSize64\n}\n\nfunc (d *digest64) Reset() {\n\td.d1.Reset()\n\td.d2.Reset()\n}\n\nfunc (d *digest64) Write(bz []byte) (int, error) {\n\td.d1.Write(bz)\n\treturn d.d2.Write(bz)\n}\n\nfunc (d digest64) Sum64() uint64 {\n\treturn uint64(d.d1.Sum32())\u003c\u003c32 | uint64(d.d2.Sum32())\n}\n\nfunc (d digest64) Sum(bz []byte) []byte {\n\ts := d.Sum64()\n\treturn append(bz,\n\t\tbyte(s), byte(s\u003e\u003e8), byte(s\u003e\u003e16), byte(s\u003e\u003e24),\n\t\tbyte(s\u003e\u003e32), byte(s\u003e\u003e40), byte(s\u003e\u003e48), byte(s\u003e\u003e56),\n\t)\n}\n"},{"name":"murmur3_64_test.gno","body":"package murmur3_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/jeronimoalbi/murmur3\"\n)\n\nfunc TestHash64Sum64(t *testing.T) {\n\t// Test cases -\u003e https://en.wikipedia.org/wiki/MurmurHash#Algorithm\n\ttests := []struct {\n\t\tname string\n\t\tdata string\n\t\twant uint64\n\t}{\n\t\t{\"empty\", \"\", 0x00000000514e28b7},\n\t\t{\"test\", \"test\", 0xba6bd21399c02ae2},\n\t\t{\"hello\", \"Hello, world!\", 0xc0363e43aa5dc85b},\n\t\t{\"pangram\", \"The quick brown fox jumps over the lazy dog\", 0x2e4ff72378e69e27},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := murmur3.Sum64([]byte(tt.data))\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"Sum64(%q) = 0x%016x, want 0x%016x\", tt.data, got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHash64InterfaceSizes(t *testing.T) {\n\th := murmur3.New64()\n\tif h.Size() != 8 {\n\t\tt.Errorf(\"Size() = %d, want 8\", h.Size())\n\t}\n\tif h.BlockSize() != 4 {\n\t\tt.Errorf(\"BlockSize() = %d, want 4\", h.BlockSize())\n\t}\n}\n\nfunc TestHash64IncrementalWrite(t *testing.T) {\n\twant := uint64(13850326164366542939)\n\tdata := []byte(\"Hello, world!\")\n\n\t// Hash one byte at a time\n\th := murmur3.New64()\n\tfor _, b := range data {\n\t\th.Write([]byte{b})\n\t}\n\n\tif got := h.Sum64(); got != want {\n\t\tt.Errorf(\"incremental Write got 0x%016x, want 0x%016x\", got, want)\n\t}\n\n\t// Hash in two parts\n\th.Reset()\n\th.Write(data[:5])\n\th.Write(data[5:])\n\tif got := h.Sum64(); got != want {\n\t\tt.Errorf(\"two-part Write got 0x%016x, want 0x%016x\", got, want)\n\t}\n}\n\nfunc TestHash64Sum(t *testing.T) {\n\th := murmur3.New64()\n\th.Write([]byte(\"Hello, world!\"))\n\tb := h.Sum(nil)\n\tif len(b) != 8 {\n\t\tt.Fatalf(\"Sum(nil) returned %d bytes, want 8\", len(b))\n\t}\n\n\tgot := uint64(b[0]) | uint64(b[1])\u003c\u003c8 | uint64(b[2])\u003c\u003c16 | uint64(b[3])\u003c\u003c24 |\n\t\tuint64(b[4])\u003c\u003c32 | uint64(b[5])\u003c\u003c40 | uint64(b[6])\u003c\u003c48 | uint64(b[7])\u003c\u003c56\n\twant := uint64(13850326164366542939)\n\tif got != want {\n\t\tt.Errorf(\"Sum got 0x%016x, want 0x%016x\", got, want)\n\t}\n}\n\nfunc TestHash64SumDoesNotChangeState(t *testing.T) {\n\th := murmur3.New64()\n\th.Write([]byte(\"Hello\"))\n\ts1 := h.Sum64()\n\ts2 := h.Sum64()\n\tif s1 != s2 {\n\t\tt.Errorf(\"Sum64 changed state: first 0x%016x, second 0x%016x\", s1, s2)\n\t}\n}\n\nfunc TestHash64Reset(t *testing.T) {\n\th := murmur3.New64()\n\th.Write([]byte(\"some data\"))\n\th.Reset()\n\th.Write([]byte(\"Hello, world!\"))\n\tgot := h.Sum64()\n\twant := uint64(13850326164366542939)\n\tif got != want {\n\t\tt.Errorf(\"after Reset got 0x%016x, want 0x%016x\", got, want)\n\t}\n}\n"},{"name":"readme_filetest.gno","body":"package main\n\nimport \"gno.land/p/jeronimoalbi/murmur3\"\n\nfunc main() {\n\tdata := []byte(\"Hello, world!\")\n\tseed := uint32(42)\n\n\t// Hash32 without seed\n\th32 := murmur3.New32()\n\th32.Write(data)\n\tsum32 := uint64(h32.Sum32())\n\tprintln(murmur3.EncodeToString(sum32))\n\n\t// Hash32 with seed\n\th32 = murmur3.NewWithSeed32(seed)\n\th32.Write(data)\n\tsum32 = uint64(h32.Sum32())\n\tprintln(murmur3.EncodeToString(sum32))\n\n\t// Hash32 without seed using a helper function\n\tsum32 = uint64(murmur3.Sum32(data))\n\tprintln(murmur3.EncodeToString(sum32))\n\n\t// Hash32 with seed using a helper function\n\tsum32 = uint64(murmur3.Sum32WithSeed(data, seed))\n\tprintln(murmur3.EncodeToString(sum32))\n\n\t// Hash64 without seed\n\th64 := murmur3.New64()\n\th64.Write(data)\n\tsum64 := h64.Sum64()\n\tprintln(murmur3.EncodeToString(sum64))\n\n\t// Hash64 with seed\n\th64 = murmur3.NewWithSeed64(0, seed)\n\th64.Write(data)\n\tsum64 = h64.Sum64()\n\tprintln(murmur3.EncodeToString(sum64))\n\n\t// Hash64 without seed using a helper function\n\tsum64 = murmur3.Sum64(data)\n\tprintln(murmur3.EncodeToString(sum64))\n\n\t// Hash64 with seed using a helper function\n\tsum64 = murmur3.Sum64WithSeed(data, 0, seed)\n\tprintln(murmur3.EncodeToString(sum64))\n}\n\n// Output:\n// c0363e43\n// 2c8c8533\n// c0363e43\n// 2c8c8533\n// c0363e43aa5dc85b\n// c0363e432c8c8533\n// c0363e43aa5dc85b\n// c0363e432c8c8533\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"iUfHnUYbDm7c0uvEU7/kOdpc0/OyruKo88iPueuYJTg2P4Xyqtgh2bXcJ0IOhcPpjbQlYL8r2vg1oFf+cRp2mw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5","package":{"name":"pkgerr","path":"gno.land/p/leon/pkgerr","files":[{"name":"err.gno","body":"// Package pkgerr provides a custom error wrapper that prepends the realm link to your error\n// This is useful for identifying the source package of the error.\n//\n// Usage:\n//\n// To wrap an error with realm and chain domain information, use the `New` function:\n//\n//\terr := pkgerr.New(\"my error message\") // in gno.land/r/leon/example\n//\tfmt.Println(err.Error()) // Output: \"r/leon/example: my error message\"\npackage pkgerr\n\nimport (\n\t\"chain/runtime/unsafe\"\n\t\"errors\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// pkgErr is a custom error type that prepends the realm link\n// (captured at construction time) to the original error message.\ntype pkgErr struct {\n\trealmPath   string\n\toriginalErr error\n}\n\n// New creates a new pkgErr with the given message. The realm link\n// captured by unsafe.CurrentRealm() at THIS call's evaluation is\n// stored on the returned value, so the prefix in Error() reflects\n// the realm that CREATED the error — not whichever realm later\n// happens to call .Error() on it.\n//\n// SECURITY (Class-2-shaped, accepted as display-only): the capture\n// uses unsafe.CurrentRealm() inside a non-crossing /p/ function and\n// is therefore .Title()-vulnerable (the walk resolves to the\n// most-recent crossing ancestor, not the immediate caller). For the\n// typical package-level-var usage (e.g. `var ErrX = pkgerr.New(...)`\n// in r/foo/bar) the construction site IS that package's init, so\n// the resolution is correct. For dynamic construction inside a\n// function call chain, a contrived chain could produce a misleading\n// prefix. This is acceptable here because no consumer in /examples\n// uses the prefix for authorization — it is rendered only as part of\n// error.Error() text. See docs/resources/gno-security.md for the\n// threat-class taxonomy.\nfunc New(msg string) error {\n\treturn \u0026pkgErr{\n\t\trealmPath:   unsafe.CurrentRealm().PkgPath(),\n\t\toriginalErr: errors.New(msg),\n\t}\n}\n\n// Wrap behaves like New but reuses an existing error as the cause.\n// Same construction-time capture semantics and same SECURITY caveat\n// as New.\nfunc Wrap(err error) error {\n\tif err == nil {\n\t\treturn nil\n\t}\n\n\treturn \u0026pkgErr{\n\t\trealmPath:   unsafe.CurrentRealm().PkgPath(),\n\t\toriginalErr: err,\n\t}\n}\n\nfunc (e pkgErr) Unwrap() error {\n\treturn e.originalErr\n}\n\n// Error implements the `error` interface for pkgErr. It uses the\n// realmPath captured at construction time so the prefix is stable\n// across the error's lifetime, regardless of which realm renders it.\nfunc (e *pkgErr) Error() string {\n\treturn ufmt.Sprintf(\"%s: %s\",\n\t\tstrings.TrimPrefix(e.realmPath, \"gno.land/\"),\n\t\te.originalErr)\n}\n"},{"name":"err_test.gno","body":"package pkgerr\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\t\"testing\"\n)\n\nconst pkgPath = \"gno.land/r/leon/test\"\n\nvar prefix = strings.TrimPrefix(pkgPath, \"gno.land/\")\n\nfunc TestNew(t *testing.T) {\n\t// SetRealm BEFORE New: capture happens at construction time now,\n\t// so the realm context must be set first. (The previous test\n\t// ordering — SetRealm AFTER New — asserted the now-fixed bug\n\t// where the prefix was read at .Error()-render time.)\n\ttesting.SetRealm(testing.NewCodeRealm(pkgPath))\n\n\terr := New(\"my error message\")\n\tif err == nil {\n\t\tt.Fatal(\"Expected an error, got nil\")\n\t}\n\n\texpected := prefix + \": my error message\"\n\n\tif err.Error() != expected {\n\t\tt.Errorf(\"Expected error message %q, got %q\", expected, err.Error())\n\t}\n}\n\nfunc TestWrap(t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(pkgPath))\n\n\toriginalErr := errors.New(\"original error\")\n\twrappedErr := Wrap(originalErr)\n\tif wrappedErr == nil {\n\t\tt.Fatal(\"Expected an error, got nil\")\n\t}\n\n\texpected := prefix + \": original error\"\n\tif wrappedErr.Error() != expected {\n\t\tt.Errorf(\"Expected error message %q, got %q\", expected, wrappedErr.Error())\n\t}\n}\n\nfunc TestUnwrap(t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(pkgPath))\n\toriginalErr := errors.New(\"original error\")\n\twrappedErr := Wrap(originalErr)\n\n\tunwrappedErr := wrappedErr.(*pkgErr).Unwrap()\n\tif unwrappedErr != originalErr {\n\t\tt.Errorf(\"Expected unwrapped error %v, got %v\", originalErr, unwrappedErr)\n\t}\n}\n\nfunc TestErrorMethod(t *testing.T) {\n\t// Build the struct directly with realmPath set so this test\n\t// exercises Error()'s formatting independently of the\n\t// New/Wrap construction-time capture.\n\toriginalErr := errors.New(\"original error\")\n\tpkgErr := \u0026pkgErr{realmPath: pkgPath, originalErr: originalErr}\n\n\texpected := prefix + \": original error\"\n\tif pkgErr.Error() != expected {\n\t\tt.Errorf(\"Expected error message %q, got %q\", expected, pkgErr.Error())\n\t}\n}\n\nfunc TestWrapNilError(t *testing.T) {\n\terr := Wrap(nil)\n\tif err != nil {\n\t\tt.Errorf(\"Expected nil error, got %v\", err)\n\t}\n}\n\nfunc TestNewWithEmptyMessage(t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(pkgPath))\n\terr := New(\"\")\n\tif err == nil {\n\t\tt.Fatal(\"Expected an error, got nil\")\n\t}\n\n\texpected := prefix + \": \"\n\tif err.Error() != expected {\n\t\tt.Errorf(\"Expected error message %q, got %q\", expected, err.Error())\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/leon/pkgerr\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"ya53ir2AgE0X6FQYv9f6xhGZHMbvlj7fnsUZ8q3rf7kRBuloc4Q/CP2EusvzvIQ6QwxTN2qMEnH5UrC69JPbrg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"ascii","path":"gno.land/p/lou/ascii","files":[{"name":"ascii.gno","body":"package ascii\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/moul/md/v0\"\n)\n\ntype WAlignment string\ntype HAlignment string\n\nconst (\n\t// Width Alignment\n\tAlignWCenter WAlignment = \"center\"\n\tAlignLeft    WAlignment = \"left\"\n\tAlignRight   WAlignment = \"right\"\n\n\t// Height Alignment\n\tAlignHCenter HAlignment = \"center\"\n\tAlignTop     HAlignment = \"top\"\n\tAlignBottom  HAlignment = \"bottom\"\n)\n\n// padLine aligns text within a given width.\n//\n// Supports AlignLeft, AlignRight, and AlignWCenter alignment.\nfunc padLine(line string, width int, align WAlignment, space string) string {\n\tpadding := width - len(line)\n\tif width \u003c len(line) {\n\t\tpadding = 0\n\t}\n\n\tswitch align {\n\tcase AlignRight:\n\t\treturn Repeat(space, padding) + line\n\tcase AlignWCenter:\n\t\tleft := padding / 2\n\t\tright := padding - left\n\t\treturn Repeat(space, left) + line + Repeat(space, right)\n\tdefault: // AlignLeft\n\t\treturn line + Repeat(space, padding)\n\t}\n}\n\n// padHeight pads lines vertically according to alignment.\n//\n// Supports AlignTop, AlignBottom, and AlignHCenter alignment.\nfunc padHeight(lines []string, height int, align HAlignment) []string {\n\tpadded := []string{}\n\tif height \u003c= 0 {\n\t\treturn lines\n\t}\n\textra := height - len(lines)\n\n\ttopPad := 0\n\tswitch align {\n\tcase AlignBottom:\n\t\ttopPad = extra\n\tcase AlignHCenter:\n\t\ttopPad = extra / 2\n\t}\n\n\tfor i := 0; i \u003c topPad; i++ {\n\t\tpadded = append(padded, \"\")\n\t}\n\tpadded = append(padded, lines...)\n\tfor len(padded) \u003c height {\n\t\tpadded = append(padded, \"\")\n\t}\n\treturn padded\n}\n\n// Repeat returns repetition of a string n times.\nfunc Repeat(char string, n int) string {\n\tif n \u003c= 0 {\n\t\treturn \"\"\n\t}\n\treturn strings.Repeat(char, n)\n}\n\n// Box draws a single-line text in a simple box.\n// If the string contains newlines, it falls back to FlexFrame function.\n//\n// Example:\n//\n//\tBox(\"Hello World\\n!\")\n//\n// Gives:\n//\n//\t+-------------+\n//\t| Hello World |\n//\t| !           |\n//\t+-------------+\nfunc Box(text string) string {\n\treturn FlexFrame(strings.Split(text, \"\\n\"), AlignLeft)\n}\n\n// FlexFrame draws a frame with automatic width and alignment.\n//\n// Example:\n//\n//\tFlexFrame([]string{\"hello\", \"worldd\", \"!!\"}, \"right\")\n//\n// Gives:\n//\n//\t+-------+\n//\t|  hello |\n//\t| worldd |\n//\t|     !! |\n//\t+-------+\nfunc FlexFrame(lines []string, align WAlignment) string {\n\tmaxWidth := 0\n\n\tfor _, line := range lines {\n\t\tif len(line) \u003e maxWidth {\n\t\t\tmaxWidth = len(line)\n\t\t}\n\t}\n\n\ttop := \"+\" + Repeat(\"-\", maxWidth+2) + \"+\\n\"\n\tbottom := \"+\" + Repeat(\"-\", maxWidth+2) + \"+\"\n\n\tbody := \"\"\n\tfor i := 0; i \u003c len(lines); i++ {\n\t\tbody += \"| \" + padLine(lines[i], maxWidth, align, \" \") + \" |\\n\"\n\t}\n\n\treturn md.CodeBlock(top + body + bottom)\n}\n\n// Frame draws a frame with specific width, height and alignment options.\n//\n// Example:\n//\n//\tFrame([]string{\"hello\", \"world\", \"!!\"}, \"center\", 10, 5, \"center\")\n//\n// Gives:\n//\n//\t+------------+\n//\t|            |\n//\t|   hello    |\n//\t|   world    |\n//\t|     !!     |\n//\t|            |\n//\t+------------+\nfunc Frame(\n\tlines []string,\n\twAlign WAlignment,\n\twidth, height int,\n\thAlign HAlignment,\n) string {\n\tmaxWidth := width\n\tfor i := 0; i \u003c len(lines); i++ {\n\t\tif len(lines[i]) \u003e maxWidth {\n\t\t\tmaxWidth = len(lines[i])\n\t\t}\n\t}\n\n\tif len(lines) \u003e height {\n\t\theight = len(lines)\n\t}\n\tlines = padHeight(lines, height, hAlign)\n\n\ttop := \"+\" + Repeat(\"-\", maxWidth+2) + \"+\\n\"\n\tbottom := \"+\" + Repeat(\"-\", maxWidth+2) + \"+\"\n\tbody := \"\"\n\n\tfor _, line := range lines {\n\t\tbody += \"| \" + padLine(line, maxWidth, wAlign, \" \") + \" |\\n\"\n\t}\n\n\treturn md.CodeBlock(top + body + bottom)\n}\n\n// FixedFrame draws a frame with a fixed width and height, truncating or wrapping content as needed.\n// Width and height include the content area, not the frame borders.\n//\n// Example:\n//\n//\tFrame([]string{\"hello world!!\"}, ascii.AlignWCenter, 10, 4, ascii.AlignHCenter)\n//\n// Gives:\n//\n//\t+------------+\n//\t|            |\n//\t|   hello    |\n//\t|  world!!   |\n//\t|            |\n//\t+------------+\nfunc FixedFrame(\n\tlines []string,\n\twAlign WAlignment,\n\twidth, height int,\n\thAlign HAlignment,\n) string {\n\tvar wrapped []string\n\tif width \u003c 0 {\n\t\twidth = 0\n\t}\n\tif height \u003c 0 {\n\t\theight = 0\n\t}\n\n\tfor _, line := range lines {\n\t\twords := strings.Fields(line)\n\t\tcurrent := \"\"\n\t\tfor _, word := range words {\n\t\t\tif len(current)+len(word)+1 \u003e width {\n\t\t\t\twrapped = append(wrapped, current)\n\t\t\t\tcurrent = word\n\t\t\t} else {\n\t\t\t\tif current == \"\" {\n\t\t\t\t\tcurrent = word\n\t\t\t\t} else {\n\t\t\t\t\tcurrent += \" \" + word\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif current != \"\" {\n\t\t\twrapped = append(wrapped, current)\n\t\t}\n\t}\n\twrapped = padHeight(wrapped, height, hAlign)\n\n\ttop := \"+\" + Repeat(\"-\", width+2) + \"+\\n\"\n\tbottom := \"+\" + Repeat(\"-\", width+2) + \"+\"\n\n\tbody := \"\"\n\tfor i, line := range wrapped {\n\t\tif i == height {\n\t\t\tbreak\n\t\t}\n\t\tbody += \"| \" + padLine(line, width, wAlign, \" \") + \" |\\n\"\n\t}\n\n\treturn md.CodeBlock(top + body + bottom)\n}\n\n// ProgressBar renders a visual progress bar, the size represents the number of chars in length for the bar.\n//\n// Example:\n//\n//\tProgressBar(2, 6, 10, true)\n//\n// Gives: [###-------] 33%\nfunc ProgressBar(current int, total int, charSize int, displayPercent bool) string {\n\tif total == 0 {\n\t\treturn PercentageBar(0, charSize, displayPercent)\n\t}\n\tpercent := (current * 100) / total\n\n\treturn PercentageBar(percent, charSize, displayPercent)\n}\n\n// PercentageBar renders a visual progress bar, the size represents the number of chars in length for the bar.\n// This differs from ProgressBar in that it does not require a total value, takes a percentage directly.\n//\n// Example:\n//\n//\tPercentageBar(50, 6, true)\n//\n// Gives: [###---] 50%\nfunc PercentageBar(percent int, charSize int, displayPercent bool) string {\n\tfillLength := (percent * charSize) / 100\n\temptyLength := charSize - fillLength\n\n\tfilled := Repeat(\"#\", fillLength)\n\tempty := Repeat(\"-\", emptyLength)\n\n\tout := \"[\" + filled + empty + \"]\"\n\tif !displayPercent {\n\t\treturn out\n\t}\n\treturn out + \" \" + strconv.Itoa(percent) + \"%\"\n}\n\n// Grid renders a 2D grid of characters.\n//\n// Example:\n//\n//\tGrid(3, 3, \"x\")\n//\n// Gives:\n//\n//\txxx\n//\txxx\n//\txxx\nfunc Grid(rows int, cols int, fill string) string {\n\tout := \"\"\n\tif rows \u003c= 0 || cols \u003c= 0 {\n\t\treturn out\n\t}\n\n\tfor r := 0; r \u003c rows; r++ {\n\t\trow := \"\"\n\t\tfor c := 0; c \u003c cols; c++ {\n\t\t\trow += fill\n\t\t}\n\t\tout += row + \"\\n\"\n\t}\n\treturn out\n}\n"},{"name":"ascii_test.gno","body":"package ascii\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\n// padLine\n\nfunc TestpadLine_Left(t *testing.T) {\n\tgot := padLine(\"hi\", 6, AlignLeft, \" \")\n\twant := \"hi    \"\n\tif got != want {\n\t\tt.Fatalf(\"padLine left failed: got '%s', want '%s'\", got, want)\n\t}\n}\n\nfunc TestpadLine_Right(t *testing.T) {\n\tgot := padLine(\"hi\", 6, AlignRight, \" \")\n\twant := \"    hi\"\n\tif got != want {\n\t\tt.Fatalf(\"padLine right failed: got '%s', want '%s'\", got, want)\n\t}\n}\n\nfunc TestpadLine_Center(t *testing.T) {\n\tgot := padLine(\"hi\", 6, AlignWCenter, \" \")\n\twant := \"  hi  \"\n\tif got != want {\n\t\tt.Fatalf(\"padLine center failed: got '%s', want '%s'\", got, want)\n\t}\n}\n\nfunc TestpadLineNegativeWidth(t *testing.T) {\n\tgot := padLine(\"hi\", -1, AlignLeft, \" \")\n\twant := \"hi\"\n\tif got != want {\n\t\tt.Fatalf(\"padLine negative width failed: got '%s', want '%s'\", got, want)\n\t}\n}\n\n// padHeight\n\nfunc TestpadHeight_Top(t *testing.T) {\n\tlines := []string{\"one\", \"two\"}\n\tout := padHeight(lines, 4, AlignTop)\n\tif len(out) != 4 || out[0] != \"one\" || out[3] != \"\" {\n\t\tt.Fatalf(\"padHeight top failed: got %#v\", out)\n\t}\n}\n\nfunc TestpadHeight_Center(t *testing.T) {\n\tlines := []string{\"one\"}\n\tout := padHeight(lines, 3, AlignHCenter)\n\tif len(out) != 3 || out[0] != \"\" || out[1] != \"one\" || out[2] != \"\" {\n\t\tt.Fatalf(\"padHeight center failed: got %#v\", out)\n\t}\n}\n\nfunc TestpadHeight_Bottom(t *testing.T) {\n\tlines := []string{\"one\"}\n\tout := padHeight(lines, 2, AlignBottom)\n\tif len(out) != 2 || out[0] != \"\" || out[1] != \"one\" {\n\t\tt.Fatalf(\"padHeight bottom failed: got %#v\", out)\n\t}\n}\n\nfunc TestpadHeightNegativeHeight(t *testing.T) {\n\tlines := []string{\"one\", \"two\"}\n\tout := padHeight(lines, -1, AlignTop)\n\tif len(out) != 2 || out[0] != \"one\" || out[1] != \"two\" {\n\t\tt.Fatalf(\"padHeight negative height failed: got %#v\", out)\n\t}\n}\n\n// Box\n\nfunc TestBox_SingleLine(t *testing.T) {\n\tgot := Box(\"Hi\")\n\tif !strings.Contains(got, \"| Hi |\") {\n\t\tt.Fatalf(\"Box did not render expected line: got\\n%s\", got)\n\t}\n}\n\nfunc TestBox_MultiLineFallback(t *testing.T) {\n\tgot := Box(\"One\\nTwo\")\n\tif !strings.Contains(got, \"| One |\") || !strings.Contains(got, \"| Two |\") {\n\t\tt.Fatalf(\"Box (multi-line fallback) failed: got\\n%s\", got)\n\t}\n}\n\n// FlexFrame\n\nfunc TestFlexFrame(t *testing.T) {\n\tlines := []string{\"Short\", \"A bit longer\"}\n\tgot := FlexFrame(lines, AlignRight)\n\tif !strings.Contains(got, \"|        Short |\") {\n\t\tt.Fatalf(\"FlexFrame right alignment failed:\\n%s\", got)\n\t}\n}\n\n// Frame\n\nfunc TestFrame_SizeAlign(t *testing.T) {\n\tlines := []string{\"line\"}\n\tgot := Frame(lines, AlignWCenter, 10, 3, AlignHCenter)\n\tif res := strings.Count(got, \"\\n\"); res != 7 {\n\t\tt.Fatalf(\"Frame height failed (got %d):\\n%s\", res, got)\n\t}\n\tif !strings.Contains(got, \"|    line    |\") {\n\t\tt.Fatalf(\"Frame width or alignment failed:\\n%s\", got)\n\t}\n}\n\nfunc TestFrameNegativeSize(t *testing.T) {\n\tlines := []string{\"line\"}\n\tgot := Frame(lines, AlignWCenter, -1, -1, AlignHCenter)\n\tif res := strings.Count(got, \"\\n\"); res != 1+4 {\n\t\tt.Fatalf(\"Frame negative size failed (got %d):\\n%s\", res, got)\n\t}\n\tif !strings.Contains(got, \"| line |\") {\n\t\tt.Fatalf(\"Frame negative size failed:\\n%s\", got)\n\t}\n}\n\n// FixedFrame\n\nfunc TestFixedFrame(t *testing.T) {\n\tlines := []string{\"line\"}\n\tgot := FixedFrame(lines, AlignWCenter, 10, 3, AlignTop)\n\tif res := strings.Count(got, \"\\n\"); res != 7 {\n\t\tt.Fatalf(\"FixedFrame height failed (got %d):\\n%s\", res, got)\n\t}\n\tif !strings.Contains(got, \"|    line    |\") {\n\t\tt.Fatalf(\"FixedFrame width failed:\\n%s\", got)\n\t}\n}\n\nfunc TestFixedFrameNegativeSize(t *testing.T) {\n\tlines := []string{\"line\"}\n\tgot := FixedFrame(lines, AlignLeft, -1, -1, AlignTop)\n\tif res := strings.Count(got, \"\\n\"); res != 4 {\n\t\tt.Fatalf(\"FixedFrame negative size failed (got %d):\\n%s\", res, got)\n\t}\n\tif strings.Contains(got, \"| line |\") {\n\t\tt.Fatalf(\"FixedFrame negative size failed:\\n%s\", got)\n\t}\n}\n\n// ProgressBar\n\nfunc TestProgressBar(t *testing.T) {\n\tgot := ProgressBar(30, 100, 10, true)\n\tif !strings.Contains(got, \"30%\") || !strings.Contains(got, \"[###-------]\") {\n\t\tt.Fatalf(\"ProgressBar output incorrect: %s\", got)\n\t}\n}\n\nfunc TestProgressBar_ZeroTotal(t *testing.T) {\n\tgot := ProgressBar(0, 0, 10, true)\n\tif !strings.Contains(got, \"0%\") {\n\t\tt.Fatalf(\"ProgressBar zero-total failed: %s\", got)\n\t}\n}\n\n// Grid\n\nfunc TestGrid(t *testing.T) {\n\tgot := Grid(2, 3, \"*\")\n\twant := \"***\\n***\\n\"\n\tif got != want {\n\t\tt.Fatalf(\"Grid failed: got\\n%s\\nwant\\n%s\", got, want)\n\t}\n}\n\nfunc TestGrid_NegativeSize(t *testing.T) {\n\tgot := Grid(-1, -1, \"*\")\n\twant := \"\"\n\tif got != want {\n\t\tt.Fatalf(\"Grid negative size failed: got\\n%s\\nwant\\n%s\", got, want)\n\t}\n}\n"},{"name":"doc.gno","body":"/*\nPackage ascii provides a simple way to create ASCII components in Gno.\n\nThe ascii package allows you to create basic ASCII components like boxes and lines, and output the generated ASCII to a string in codeblock format when necessary.\n\nExample:\n\n\timport \"gno.land/p/lou/ascii\"\n\n\tfunc Foo() string {\n\t    return ascii.Frame([]string{\"Hello, World!\"}, 20, 5, ascii.AlignWCenter, ascii.AlignHCenter)\n\t}\n\n*/\n\npackage ascii // import \"gno.land/p/lou/ascii\"\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/lou/ascii\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"24tHoVZrRNFinCy+T5X4rftBenRA+o7jHuWRldSYZ9ETvgWdhtGCIc7+VJkjBsTWN9tMWEN9PU6MSAUGavb+og=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"query","path":"gno.land/p/lou/query","files":[{"name":"doc.gno","body":"/*\nPackage query provides a simple way to handle queries in your realm.\n\nIt includes helper functions to extract individual query parameters, convert the full query string into a map,\nand update one or more parameters in a URL.\n\nExample:\n\n\timport (\n\t\t\"gno.land/p/lou/query\"\n\t\t\"gno.land/p/moul/md/v0\"\n\t)\n\n\tfunc Render(rawPath string) string {\n\t\tmode, err := query.GetQueryValueFromURL(\"mode\", rawPath)\n\t\tif err != nil || mode == \"\" {\n\t\t\tmode = \"grid\" // default value\n\t\t}\n\n\t\t// Handle queryMode value\n\t\tlistURL, _ := query.UpdateQueryValue(rawPath, \"mode\", \"list\")\n\t\tgridURL, _ := query.UpdateQueryValue(rawPath, \"mode\", \"grid\")\n\n\t\tout := md.Link(\"list mode\", listURL) + \"\\n\\n\"\n\t\tout += md.Link(\"grid mode\", gridURL) + \"\\n\\n\"\n\t\treturn out\n\t}\n\n*/\n\npackage query // import \"gno.land/p/lou/query\"\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/lou/query\"\ngno = \"0.9\"\n"},{"name":"query.gno","body":"package query\n\nimport (\n\t\"net/url\"\n)\n\n// GetQueryValues returns all values for a given key in query parameters.\nfunc GetQueryValues(params url.Values, key string) []string {\n\treturn params[key]\n}\n\n// GetQueryFirstValue returns the first value for a given key in query parameters.\nfunc GetQueryFirstValue(params url.Values, key string) string {\n\tif vals, ok := params[key]; ok \u0026\u0026 len(vals) \u003e 0 {\n\t\treturn vals[0]\n\t}\n\treturn \"\"\n}\n\n// HasQueryKey checks if the given key exists in query parameters.\nfunc HasQueryKey(params url.Values, key string) bool {\n\t_, exists := params[key]\n\treturn exists\n}\n\n// GetQueryValueFromURL retrieves a single query value from the raw URL.\n// If the URL is invalid or the parameter does not exist, it returns an empty string.\n//\n//\t value := GetQueryValueFromURL(\"user\", \"https://example.com/?user=john\")\n//\t\t// value == \"john\"\nfunc GetQueryValueFromURL(key, rawPath string) (string, error) {\n\tu, err := url.Parse(rawPath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn u.Query().Get(key), nil\n}\n\n// GetQuery retrieves a single query parameter value from the raw URL.\n// If the URL is invalid or the parameter does not exist, it returns an empty string.\nfunc GetQuery(key, rawPath string) string {\n\tu, err := url.Parse(rawPath)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn u.Query().Get(key)\n}\n\n// ParseQueryFirstValuesMap returns a map of the first values for all query parameters.\n//\n//\tm := ParseQueryFirstValuesMap(\"https://example.com/?foo=1\u0026bar=2\")\n//\t// m == map[string]string{\"foo\": \"1\", \"bar\": \"2\"}\nfunc ParseQueryFirstValuesMap(rawPath string) (map[string]string, error) {\n\tu, err := url.Parse(rawPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := make(map[string]string)\n\tfor key, vals := range u.Query() {\n\t\tif len(vals) \u003e 0 {\n\t\t\tresult[key] = vals[0]\n\t\t}\n\t}\n\treturn result, nil\n}\n\n// UpdateQueryValue sets a single query key to one value, replacing existing values.\n//\n//\turl := UpdateQueryValue(\"https://example.com/?foo=1\", \"foo\", \"2\")\n//\t// url == \"https://example.com/?foo=2\"\nfunc UpdateQueryValue(rawPath, key, value string) (string, error) {\n\treturn UpdateQueryValues(rawPath, key, []string{value})\n}\n\n// UpdateQueryValues sets a single query key to multiple values.\n// Existing values are replaced, and new ones are added. Returns the modified URL as a string.\n//\n//\turl := UpdateQueryValues(\"https://example.com/?foo=1\", \"foo\", []string{\"2\", \"3\"})\n//\t// url == \"https://example.com/?foo=2\u0026foo=3\"\nfunc UpdateQueryValues(rawPath, key string, values []string) (string, error) {\n\tu, err := url.Parse(rawPath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tq := u.Query()\n\tq[key] = values\n\tu.RawQuery = q.Encode()\n\treturn u.String(), nil\n}\n\n// UpdateQueryFirstValues sets multiple query parameters to single values.\n// Existing parameters are overwritten, and new ones are added. Returns the modified URL as a string.\n//\n//\turl := UpdateQueryFirstValues(\"https://example.com/?foo=1\", map[string]string{\"foo\": \"2\", \"bar\": \"3\"})\n//\t// url == \"https://example.com/?bar=3\u0026foo=2\"\nfunc UpdateQueryFirstValues(rawPath string, updates map[string]string) (string, error) {\n\tu, err := url.Parse(rawPath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tq := u.Query()\n\tfor k, v := range updates {\n\t\tq.Set(k, v)\n\t}\n\tu.RawQuery = q.Encode()\n\treturn u.String(), nil\n}\n\n// UpdateQueryAllValues sets multiple query parameters, each with multiple values.\n// Existing parameters are replaced, and new ones are added. Returns the modified URL as a string.\n//\n//\turl := UpdateQueryAllValues(\"https://example.com/?foo=1\", map[string][]string{\"foo\": {\"2\", \"3\"}, \"bar\": {\"4\"}})\n//\t// url == \"https://example.com/?bar=4\u0026foo=2\u0026foo=3\"\nfunc UpdateQueryAllValues(rawPath string, updates map[string][]string) (string, error) {\n\tu, err := url.Parse(rawPath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tq := u.Query()\n\tfor k, v := range updates {\n\t\tq[k] = v\n\t}\n\tu.RawQuery = q.Encode()\n\treturn u.String(), nil\n}\n\n// SetQueries sets multiple query parameters in the URL.\n// It replaces existing parameters and adds new ones, returning the modified URL as a string.\n//\n// url := SetQueries(\"https://example.com/?fa=1\", map[string]string{\"foo\": \"2\", \"bar\": \"3\"})\n// // url == \"https://example.com/?bar=3\u0026foo=2\"\nfunc SetQueries(rawPath string, queries map[string]string) (string, error) {\n\tu, err := url.Parse(rawPath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tq := u.Query()\n\tfor k, v := range queries {\n\t\tq.Set(k, v)\n\t}\n\tu.RawQuery = q.Encode()\n\treturn u.String(), nil\n}\n\n// SetQueriesMulti sets multiple query parameters with multiple values.\n// It replaces existing parameters and adds new ones, returning the modified URL as a string.\n//\n// url := SetQueriesMulti(\"https://example.com/?fa=1\", map[string][]string{\"foo\": {\"2\", \"3\"}, \"bar\": {\"4\"}})\n// // url == \"https://example.com/?bar=4\u0026foo=2\u0026foo=3\"\nfunc SetQueriesMulti(rawPath string, queries map[string][]string) (string, error) {\n\tu, err := url.Parse(rawPath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tq := u.Query()\n\tfor k, v := range queries {\n\t\tq[k] = v\n\t}\n\tu.RawQuery = q.Encode()\n\treturn u.String(), nil\n}\n\n// DeleteQuery removes a key from the query parameters.\n//\n//\turl := DeleteQuery(\"https://example.com/?foo=1\u0026bar=2\", \"foo\")\n//\t// url == \"https://example.com/?bar=2\"\nfunc DeleteQuery(rawPath, key string) (string, error) {\n\tu, err := url.Parse(rawPath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tq := u.Query()\n\tq.Del(key)\n\tu.RawQuery = q.Encode()\n\treturn u.String(), nil\n}\n\n// ResetQuery clears all query parameters from the URL path.\n//\n//\turl := ResetQuery(\"https://example.com/?foo=1\u0026bar=2\")\n//\t// url == \"https://example.com/\"\nfunc ResetQuery(rawPath string) (string, error) {\n\tu, err := url.Parse(rawPath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tu.RawQuery = \"\"\n\treturn u.String(), nil\n}\n"},{"name":"query_test.gno","body":"package query\n\nimport (\n\t\"net/url\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestUpdateQueryValue(t *testing.T) {\n\traw := \"/blog?foo=bar\"\n\tupdated, err := UpdateQueryValue(raw, \"foo\", \"baz\")\n\turequire.NoError(t, err, \"no error\")\n\tuassert.Equal(t, \"/blog?foo=baz\", updated)\n}\n\nfunc TestUpdateQueryFirstValues(t *testing.T) {\n\traw := \"/blog?sort=alpha\"\n\tupdated, err := UpdateQueryFirstValues(raw, map[string]string{\n\t\t\"sort\":  \"recent\",\n\t\t\"order\": \"asc\",\n\t})\n\turequire.NoError(t, err, \"no error\")\n\texpected1 := \"/blog?order=asc\u0026sort=recent\"\n\texpected2 := \"/blog?sort=recent\u0026order=asc\"\n\tif updated != expected1 \u0026\u0026 updated != expected2 {\n\t\tt.Errorf(\"got %s, expected %s or %s\", updated, expected1, expected2)\n\t}\n}\n\nfunc TestGetQueryValueFromURL(t *testing.T) {\n\traw := \"/blog?view=grid\u0026sort=alpha\"\n\tval, err := GetQueryValueFromURL(\"sort\", raw)\n\turequire.NoError(t, err, \"no error\")\n\tuassert.Equal(t, \"alpha\", val)\n}\n\nfunc TestParseQueryFirstValuesMap(t *testing.T) {\n\traw := \"/blog?sort=alpha\u0026order=desc\"\n\tm, err := ParseQueryFirstValuesMap(raw)\n\turequire.NoError(t, err, \"no error\")\n\tuassert.Equal(t, \"alpha\", m[\"sort\"])\n\tuassert.Equal(t, \"desc\", m[\"order\"])\n}\n\nfunc TestDeleteQuery(t *testing.T) {\n\traw := \"/blog?sort=alpha\u0026mode=grid\"\n\tdeleted, err := DeleteQuery(raw, \"sort\")\n\turequire.NoError(t, err, \"no error\")\n\tuassert.Equal(t, \"/blog?mode=grid\", deleted)\n}\n\nfunc TestMalformedURLHandling(t *testing.T) {\n\t_, err := ParseQueryFirstValuesMap(\":::\")\n\turequire.Error(t, err)\n\n\t_, err = UpdateQueryValue(\":::\", \"a\", \"b\")\n\turequire.Error(t, err)\n\n\tval, err := GetQueryValueFromURL(\"x\", \":::\")\n\turequire.Error(t, err)\n\turequire.Equal(t, \"\", val)\n}\n\nfunc TestGetQueryValues(t *testing.T) {\n\tvalues := url.Values{\n\t\t\"foo\": {\"1\", \"2\"},\n\t}\n\tgot := GetQueryValues(values, \"foo\")\n\tuassert.Equal(t, \"1\", got[0])\n\tuassert.Equal(t, \"2\", got[1])\n}\n\nfunc TestGetQueryFirstValue(t *testing.T) {\n\tvalues := url.Values{\n\t\t\"foo\": {\"1\", \"2\"},\n\t}\n\tuassert.Equal(t, \"1\", GetQueryFirstValue(values, \"foo\"))\n\tuassert.Equal(t, \"\", GetQueryFirstValue(values, \"bar\")) // key doesn't exist\n}\n\nfunc TestHasQueryKey(t *testing.T) {\n\tvalues := url.Values{\n\t\t\"foo\": {\"1\"},\n\t}\n\tuassert.Equal(t, true, HasQueryKey(values, \"foo\"))\n\tuassert.Equal(t, false, HasQueryKey(values, \"bar\"))\n}\n\nfunc TestUpdateQueryValues(t *testing.T) {\n\traw := \"/blog?foo=bar\"\n\tupdated, err := UpdateQueryValues(raw, \"foo\", []string{\"1\", \"2\"})\n\turequire.NoError(t, err, \"no error\")\n\tuassert.Equal(t, \"/blog?foo=1\u0026foo=2\", updated)\n}\n\nfunc TestUpdateQueryAllValues(t *testing.T) {\n\traw := \"/blog?foo=1\"\n\tupdated, err := UpdateQueryAllValues(raw, map[string][]string{\n\t\t\"foo\": {\"2\", \"3\"},\n\t\t\"bar\": {\"4\"},\n\t})\n\turequire.NoError(t, err, \"no error\")\n\t// order may vary\n\texpected1 := \"/blog?bar=4\u0026foo=2\u0026foo=3\"\n\texpected2 := \"/blog?foo=2\u0026foo=3\u0026bar=4\"\n\tif updated != expected1 \u0026\u0026 updated != expected2 {\n\t\tt.Errorf(\"got %s, expected %s or %s\", updated, expected1, expected2)\n\t}\n}\n\nfunc TestSetQueries(t *testing.T) {\n\traw := \"/blog?foo=1\"\n\tupdated, err := SetQueries(raw, map[string]string{\n\t\t\"foo\": \"2\",\n\t\t\"bar\": \"3\",\n\t})\n\turequire.NoError(t, err, \"no error\")\n\texpected1 := \"/blog?bar=3\u0026foo=2\"\n\texpected2 := \"/blog?foo=2\u0026bar=3\"\n\tif updated != expected1 \u0026\u0026 updated != expected2 {\n\t\tt.Errorf(\"got %s, expected %s or %s\", updated, expected1, expected2)\n\t}\n}\n\nfunc TestSetQueriesMulti(t *testing.T) {\n\traw := \"/blog?foo=1\"\n\tupdated, err := SetQueriesMulti(raw, map[string][]string{\n\t\t\"foo\": {\"2\", \"3\"},\n\t\t\"bar\": {\"4\"},\n\t})\n\turequire.NoError(t, err, \"no error\")\n\texpected1 := \"/blog?bar=4\u0026foo=2\u0026foo=3\"\n\texpected2 := \"/blog?foo=2\u0026foo=3\u0026bar=4\"\n\tif updated != expected1 \u0026\u0026 updated != expected2 {\n\t\tt.Errorf(\"got %s, expected %s or %s\", updated, expected1, expected2)\n\t}\n}\n\nfunc TestResetQuery(t *testing.T) {\n\traw := \"/blog?foo=1\u0026bar=2\"\n\tupdated, err := ResetQuery(raw)\n\turequire.NoError(t, err, \"no error\")\n\tuassert.Equal(t, \"/blog\", updated)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"1/AKH8XdmuCVBDZmOSTMis7n8YO7GIe4N2wD04xPHbsUoT2WDkbIDCCTeaodp+hPjENNEy1jvPqOZ+Db79YfEw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"blog","path":"gno.land/p/lou/blog","files":[{"name":"blog.gno","body":"package blog\n\nimport (\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/ownable/exts/authorizable/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)\n\ntype Blog struct {\n\tAuthorizable     *authorizable.Authorizable\n\ttitle            string\n\tprefix           string\n\tPostId           seqid.ID\n\tPosts            *avl.Tree // id --\u003e *Post\n\tPostsBySlug      *avl.Tree // slug --\u003e *Post\n\tPostsByUpdatedAt *avl.Tree // \"\u003cid\u003e::updatedAt\" --\u003e *Post (To ensure no overlay)\n\tTagsIndex        *avl.Tree // tagName --\u003e int\n\tTagsSorted       *avl.Tree // \"\u003ccount\u003e::tag\" --\u003e tag (To sort from most/least common tags)\n\tAuthorsIndex     *avl.Tree // authorAddress --\u003e int\n\tAuthorsSorted    *avl.Tree // \"\u003ccount\u003e::author\" --\u003e author (To sort from most/least common authors)\n\n\tCustomHeader    *[]HeaderPreset // header.gno\n\tDisableLikes    bool\n\tDisableComments bool\n\tUserResolver    UserResolver // moderation.gno\n}\n\nfunc (b Blog) Title() string {\n\treturn b.title\n}\n\nfunc (b Blog) Prefix() string {\n\treturn b.prefix\n}\n\n// NewBlog creates a Blog whose prefix is bound to the calling realm.\n// rlm must be the caller's own captured cur (asserted via\n// rlm.IsCurrent()); rlm.PkgPath() — the calling realm itself — provides\n// the prefix used for txlink/markdown URLs. Same shape as\n// grc20.NewToken's identity binding: unforgeable because rlm comes from\n// a runtime-validated crossing frame.\nfunc NewBlog(_ int, rlm realm, title string, owner address, opts ...Options) (*Blog, error) {\n\tif title == \"\" {\n\t\treturn nil, ErrEmptyTitle\n\t}\n\tif err := CheckAddr(owner); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif !rlm.IsCurrent() {\n\t\treturn nil, ErrEmptyPrefix\n\t}\n\tpkgPath := rlm.PkgPath()\n\tif pkgPath == \"\" {\n\t\treturn nil, ErrEmptyPrefix\n\t}\n\tprefix := strings.Split(pkgPath, \"gno.land\")[1]\n\tif prefix == \"\" {\n\t\treturn nil, ErrEmptyPrefix\n\t}\n\n\tauth := authorizable.New(ownable.NewWithAddress(owner))\n\tblog := \u0026Blog{\n\t\tAuthorizable:     auth,\n\t\ttitle:            title,\n\t\tprefix:           prefix,\n\t\tPostId:           seqid.ID(0),\n\t\tPosts:            avl.NewTree(),\n\t\tPostsBySlug:      avl.NewTree(),\n\t\tPostsByUpdatedAt: avl.NewTree(),\n\t\tTagsIndex:        avl.NewTree(),\n\t\tTagsSorted:       avl.NewTree(),\n\t\tAuthorsIndex:     avl.NewTree(),\n\t\tAuthorsSorted:    avl.NewTree(),\n\t\tDisableLikes:     false,\n\t\tDisableComments:  false,\n\t\tUserResolver:     nil,\n\t\tCustomHeader:     nil,\n\t}\n\n\tfor _, opt := range opts {\n\t\topt(blog)\n\t}\n\treturn blog, nil\n}\n\ntype Options func(*Blog)\n\nfunc WithDisableLikes() Options {\n\treturn func(b *Blog) {\n\t\tb.DisableLikes = true\n\t\tb.setDisablePostLikes(true)\n\t}\n}\n\nfunc WithDisableComments() Options {\n\treturn func(b *Blog) {\n\t\tb.DisableComments = true\n\t\tb.setDisableComments(true)\n\t}\n}\n\nfunc WithUserResolver(resolver UserResolver) Options {\n\treturn func(b *Blog) {\n\t\tb.UserResolver = resolver\n\t}\n}\n\nfunc (b *Blog) AddPost(_ int, rlm realm, post *Post) error {\n\tb.Authorizable.AssertPreviousOnAuthList(0, rlm)\n\n\tpost.id = b.PostId.Next()\n\tif _, err := b.GetPostBySlug(post.Slug()); err == nil {\n\t\treturn ErrPostAlreadyExists\n\t}\n\tif err := CheckAddr(address(post.Publisher())); err != nil {\n\t\treturn err\n\t}\n\n\tpost.DisableLikes = b.DisableLikes\n\tpost.DisableComments = b.DisableComments\n\tif b.UserResolver != nil {\n\t\tpost.UserResolver = b.UserResolver\n\t\tpost.authors = post.resolveAuthors(post.Authors())\n\t\tpost.publisher, _ = CheckUser(post.Publisher(), b.UserResolver)\n\t}\n\n\tb.addToIndex(post)\n\tb.Posts.Set(post.ID(), post)\n\tb.PostsBySlug.Set(post.Slug(), post)\n\tb.PostsByUpdatedAt.Set(post.ID()+\"::\"+post.UpdatedAt().String(), post)\n\treturn nil\n}\n\nfunc (b *Blog) UpdatePostById(_ int, rlm realm, id string, newPost *Post) error {\n\tb.Authorizable.AssertPreviousOnAuthList(0, rlm)\n\n\tpost, err := b.GetPostById(id)\n\tif err != nil {\n\t\treturn err\n\t}\n\tpostBySlug := b.PostsBySlug.Get(post.Slug())\n\tpostByUpdated := b.PostsByUpdatedAt.Get(post.ID() + \"::\" + post.UpdatedAt().String())\n\tpost.UpdatePost(newPost)\n\tpostBySlug.(*Post).UpdatePost(newPost)\n\tpostByUpdated.(*Post).UpdatePost(newPost)\n\treturn nil\n}\n\nfunc (b *Blog) UpdatePostBySlug(_ int, rlm realm, slug string, newPost *Post) error {\n\tb.Authorizable.AssertPreviousOnAuthList(0, rlm)\n\n\tpost, err := b.GetPostBySlug(slug)\n\tif err != nil {\n\t\treturn err\n\t}\n\tpostById := b.Posts.Get(post.ID())\n\tpostByUpdated := b.PostsByUpdatedAt.Get(post.ID() + \"::\" + post.UpdatedAt().String())\n\tpost.UpdatePost(newPost)\n\tpostById.(*Post).UpdatePost(newPost)\n\tpostByUpdated.(*Post).UpdatePost(newPost)\n\treturn nil\n}\n\nfunc (b *Blog) DeletePostById(_ int, rlm realm, id string) error {\n\tb.Authorizable.AssertPreviousOnAuthList(0, rlm)\n\n\tpost, err := b.GetPostById(id)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn b.DeletePost(post)\n}\n\nfunc (b *Blog) DeletePostBySlug(_ int, rlm realm, slug string) error {\n\tb.Authorizable.AssertPreviousOnAuthList(0, rlm)\n\n\tpost, err := b.GetPostBySlug(slug)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn b.DeletePost(post)\n}\n\nfunc (b *Blog) DeletePost(post *Post) error {\n\t_, removed := b.Posts.Remove(post.ID())\n\t_, removedSlug := b.PostsBySlug.Remove(post.Slug())\n\t_, removedUpdated := b.PostsByUpdatedAt.Remove(post.ID() + \"::\" + post.UpdatedAt().String())\n\tif !removed || !removedSlug || !removedUpdated {\n\t\treturn ErrDeleteFailed\n\t}\n\treturn nil\n}\n\nfunc (b *Blog) LikePostById(_ int, rlm realm, id string) error { // toggles between like and unlike\n\tb.Authorizable.AssertPreviousOnAuthList(0, rlm)\n\n\tpost, err := b.GetPostById(id)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcaller := rlm.Previous().Address().String()\n\tif err := post.LikePost(caller); err != nil {\n\t\tpost.UnlikePost(caller)\n\t}\n\treturn nil\n\n}\n\nfunc (b *Blog) LikePostBySlug(_ int, rlm realm, slug string) error { // toggles between like and unlike\n\tb.Authorizable.AssertPreviousOnAuthList(0, rlm)\n\n\tpost, err := b.GetPostBySlug(slug)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcaller := rlm.Previous().Address().String()\n\tif err := post.LikePost(caller); err != nil {\n\t\tpost.UnlikePost(caller)\n\t}\n\treturn nil\n}\n\nfunc (b Blog) GetPostById(id string) (*Post, error) {\n\tpost := b.Posts.Get(id)\n\tif post == nil {\n\t\treturn nil, ErrPostNotFound\n\t}\n\treturn post.(*Post), nil\n}\n\nfunc (b Blog) GetPostBySlug(slug string) (*Post, error) {\n\tpost := b.PostsBySlug.Get(slug)\n\tif post == nil {\n\t\treturn nil, ErrPostNotFound\n\t}\n\treturn post.(*Post), nil\n}\n\nfunc (b *Blog) SetDisablePostLikes(_ int, rlm realm, disable bool) {\n\tb.Authorizable.AssertPreviousOnAuthList(0, rlm)\n\tb.setDisablePostLikes(disable)\n}\n\nfunc (b *Blog) setDisablePostLikes(disable bool) {\n\tb.DisableLikes = disable\n\tb.Posts.Iterate(\"\", \"\", func(_ string, value any) bool {\n\t\tpost := value.(*Post)\n\t\tpost.DisableLikes = disable\n\t\treturn false\n\t})\n\tb.PostsBySlug.Iterate(\"\", \"\", func(_ string, value any) bool {\n\t\tpost := value.(*Post)\n\t\tpost.DisableLikes = disable\n\t\treturn false\n\t})\n\tb.PostsByUpdatedAt.Iterate(\"\", \"\", func(_ string, value any) bool {\n\t\tpost := value.(*Post)\n\t\tpost.DisableLikes = disable\n\t\treturn false\n\t})\n}\n\nfunc (b *Blog) SetDisableComments(_ int, rlm realm, disable bool) {\n\tb.Authorizable.AssertPreviousOnAuthList(0, rlm)\n\tb.setDisableComments(disable)\n}\n\nfunc (b *Blog) setDisableComments(disable bool) {\n\tb.DisableComments = disable\n\tb.Posts.Iterate(\"\", \"\", func(_ string, value any) bool {\n\t\tpost := value.(*Post)\n\t\tpost.DisableComments = disable\n\t\treturn false\n\t})\n\tb.PostsBySlug.Iterate(\"\", \"\", func(_ string, value any) bool {\n\t\tpost := value.(*Post)\n\t\tpost.DisableComments = disable\n\t\treturn false\n\t})\n\tb.PostsByUpdatedAt.Iterate(\"\", \"\", func(_ string, value any) bool {\n\t\tpost := value.(*Post)\n\t\tpost.DisableComments = disable\n\t\treturn false\n\t})\n}\n\nfunc (b *Blog) SetUserResolver(_ int, rlm realm, resolver UserResolver) {\n\tb.Authorizable.AssertPreviousOnAuthList(0, rlm)\n\tb.UserResolver = resolver\n}\n\nfunc (b *Blog) SetCustomHeader(_ int, rlm realm, presets []HeaderPreset) {\n\tb.Authorizable.AssertPreviousOnAuthList(0, rlm)\n\tb.CustomHeader = \u0026presets\n}\n\nfunc (b Blog) Mention(role, recipient string) string {\n\tif role == \"author\" {\n\t\treturn md.Bold(md.Link(\"@\"+recipient, b.Prefix()+\":authors/\"+recipient))\n\t}\n\tif role == \"commenter\" {\n\t\treturn md.Bold(md.Link(\"@\"+recipient, b.Prefix()+\":commenters/\"+recipient))\n\t}\n\tif role == \"tag\" {\n\t\treturn md.Bold(md.Link(\"#\"+recipient, b.Prefix()+\":tags/\"+recipient))\n\t}\n\treturn \"\"\n}\n\nfunc (b *Blog) addToIndex(post *Post) {\n\tfor _, tag := range post.Tags() {\n\t\toldCount := 0\n\t\tif val := b.TagsIndex.Get(tag); val != nil {\n\t\t\toldCount = val.(int)\n\t\t\tb.TagsSorted.Remove(ufmt.Sprintf(\"%05d::%s\", oldCount, tag))\n\t\t}\n\t\tnewCount := oldCount + 1\n\t\tb.TagsIndex.Set(tag, newCount)\n\t\tb.TagsSorted.Set(ufmt.Sprintf(\"%05d::%s\", newCount, tag), newCount)\n\t}\n\tfor _, author := range post.Authors() {\n\t\toldCount := 0\n\t\tif val := b.AuthorsIndex.Get(author); val != nil {\n\t\t\toldCount = val.(int)\n\t\t\tb.AuthorsSorted.Remove(ufmt.Sprintf(\"%05d::%s\", oldCount, author))\n\t\t}\n\t\tnewCount := oldCount + 1\n\t\tb.AuthorsIndex.Set(author, newCount)\n\t\tb.AuthorsSorted.Set(ufmt.Sprintf(\"%05d::%s\", newCount, author), newCount)\n\t}\n}\n\nfunc (b Blog) filterPostsStartEnd(tree *avl.Tree, start, end *time.Time) *avl.Tree {\n\tfiltered := avl.NewTree()\n\ttree.Iterate(\"\", \"\", func(k string, v interface{}) bool {\n\t\tpost := v.(*Post)\n\t\tif (start == nil || post.CreatedAt().After(*start)) \u0026\u0026\n\t\t\t(end == nil || post.CreatedAt().Before(*end)) {\n\t\t\tfiltered.Set(k, post)\n\t\t}\n\t\treturn false\n\t})\n\treturn filtered\n}\n\nfunc (b Blog) filterPostsByField(field, value, sort string) (*avl.Tree, bool) {\n\trecentPosts := avl.NewTree()\n\talphaPosts := avl.NewTree()\n\tupdatedPosts := avl.NewTree()\n\n\tswitch field {\n\tcase \"tag\", \"author\":\n\t\tb.Posts.ReverseIterate(\"\", \"\", func(k string, v interface{}) bool {\n\t\t\tpost := v.(*Post)\n\t\t\tvar match bool\n\t\t\tif field == \"tag\" {\n\t\t\t\tmatch = hasField(post.Tags(), value)\n\t\t\t} else {\n\t\t\t\tmatch = hasField(post.Authors(), value)\n\t\t\t}\n\t\t\tif match {\n\t\t\t\trecentPosts.Set(k, post)\n\t\t\t\talphaPosts.Set(post.Slug(), post)\n\t\t\t\tupdatedPosts.Set(post.UpdatedAt().String(), post)\n\t\t\t}\n\t\t\treturn false\n\t\t})\n\n\tcase \"commenter\":\n\t\tcommenterId := seqid.ID(0)\n\t\tb.Posts.ReverseIterate(\"\", \"\", func(k string, v interface{}) bool {\n\t\t\tpost := v.(*Post)\n\t\t\tcomments := post.GetCommentsByAuthor(value)\n\t\t\tfor _, comment := range comments {\n\t\t\t\tkeyPrefix := commenterId.Next().String() + \"::\"\n\t\t\t\trecentPosts.Set(keyPrefix+post.ID(), comment)\n\t\t\t\talphaPosts.Set(keyPrefix+post.Slug(), comment)\n\t\t\t\tupdatedPosts.Set(keyPrefix+post.UpdatedAt().String(), comment)\n\t\t\t}\n\t\t\treturn false\n\t\t})\n\t}\n\n\tswitch sort {\n\tcase \"alpha\":\n\t\treturn alphaPosts, alphaPosts.Size() \u003e 0\n\tcase \"update\":\n\t\treturn updatedPosts, updatedPosts.Size() \u003e 0\n\tdefault:\n\t\treturn recentPosts, recentPosts.Size() \u003e 0\n\t}\n}\n\nfunc (b Blog) findPostBySlug(value string) (string, bool) {\n\tvar foundKey string\n\tvar found bool\n\tb.Posts.Iterate(\"\", \"\", func(k string, v interface{}) bool {\n\t\tpost := v.(*Post)\n\t\tif post.Slug() == value {\n\t\t\tfoundKey = k\n\t\t\tfound = true\n\t\t}\n\t\treturn found\n\t})\n\treturn foundKey, found\n}\n"},{"name":"comment.gno","body":"package blog\n\nimport (\n\t\"strconv\"\n\t\"time\"\n\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n)\n\ntype Comment struct {\n\tid        seqid.ID\n\tauthor    string\n\tcontent   string\n\tcreatedAt time.Time\n\teditedAt  *time.Time // nil if unedited\n\tpinned    bool\n\tlikes     int\n\tRepliesId seqid.ID\n\tReplies   *avl.Tree // id --\u003e *Comment\n\n\tfooter       string // additional text, e.g. \"via @lou\" or txlink calls\n\tDisableLikes bool\n}\n\nfunc (c Comment) ID() string {\n\treturn c.id.String()\n}\n\nfunc (c Comment) Author() string {\n\treturn c.author\n}\n\nfunc (c Comment) Content() string {\n\treturn c.content\n}\n\nfunc (c Comment) CreatedAt() time.Time {\n\treturn c.createdAt\n}\n\nfunc (c Comment) EditedAt() *time.Time {\n\treturn c.editedAt\n}\n\nfunc (c Comment) Pinned() bool {\n\treturn c.pinned\n}\n\nfunc (c Comment) Likes() int {\n\treturn c.likes\n}\n\nfunc (c Comment) Footer() string {\n\treturn c.footer\n}\n\nfunc NewComment(author, content string) (*Comment, error) {\n\tif content == \"\" {\n\t\treturn nil, ErrEmptyComment\n\t}\n\treturn \u0026Comment{\n\t\tauthor:       author,\n\t\tcontent:      content,\n\t\tcreatedAt:    time.Now(),\n\t\teditedAt:     nil,\n\t\tpinned:       false,\n\t\tlikes:        0,\n\t\tRepliesId:    seqid.ID(0),\n\t\tReplies:      avl.NewTree(),\n\t\tfooter:       \"\",\n\t\tDisableLikes: false,\n\t}, nil\n}\n\nfunc (c *Comment) Edit(content string) error {\n\tif content == \"\" {\n\t\treturn ErrEmptyComment\n\t}\n\tc.content = content\n\tnow := time.Now()\n\tc.editedAt = \u0026now\n\treturn nil\n}\n\nfunc (c *Comment) Pin() {\n\tc.pinned = true\n}\n\nfunc (c *Comment) Unpin() {\n\tc.pinned = false\n}\n\nfunc (c *Comment) AddLike() {\n\tc.likes++\n}\n\nfunc (c *Comment) RemoveLike() {\n\tif c.likes \u003e 0 {\n\t\tc.likes--\n\t}\n}\n\nfunc (c *Comment) SetID(id seqid.ID) {\n\tc.id = id\n}\n\nfunc (c *Comment) SetFooter(footer string) {\n\tc.footer = footer\n}\n\nfunc (c *Comment) Render(blogPrefix string, depth int, resolver UserResolver) string {\n\tprefix := \"\"\n\tfor i := 0; i \u003c depth; i++ {\n\t\tprefix += \"\u003e \"\n\t}\n\tuser, _ := CheckUser(c.Author(), resolver)\n\tout := prefix + md.Bold(md.Link(\"@\"+user, blogPrefix+\":commenters/\"+user)) + \" \"\n\tif c.Pinned() {\n\t\tout += \"📌 \"\n\t}\n\tif c.EditedAt() != nil {\n\t\tout += md.Italic(formatTime(*c.EditedAt(), \"relative\")+\" (edited)\") + \"\\n\"\n\t} else {\n\t\tout += md.Italic(formatTime(c.CreatedAt(), \"relative\")) + \"\\n\"\n\t}\n\tout += \"\\n\" + prefix + c.Content() + \"\\n\"\n\tif !c.DisableLikes {\n\t\tout += prefix + \"❤️ \" + strconv.Itoa(c.Likes()) + \" \\n\\n\"\n\t}\n\tif c.Footer() != \"\" {\n\t\tout += prefix + c.Footer() + \"\\n\\n\"\n\t}\n\n\tif c.Replies != nil \u0026\u0026 c.Replies.Size() \u003e 0 {\n\t\tc.Replies.ReverseIterate(\"\", \"\", func(key string, value any) bool {\n\t\t\treply := value.(*Comment)\n\t\t\tout += reply.Render(blogPrefix, depth+1, resolver)\n\t\t\treturn false\n\t\t})\n\t}\n\treturn out\n}\n\nfunc searchInReplies(tree *avl.Tree, id string, result **Comment) bool {\n\tif tree == nil {\n\t\treturn false\n\t}\n\tfound := false\n\ttree.ReverseIterate(\"\", \"\", func(_ string, v interface{}) bool {\n\t\tc := v.(*Comment)\n\t\tif c.ID() == id {\n\t\t\t*result = c\n\t\t\tfound = true\n\t\t\treturn true\n\t\t}\n\t\tif searchInReplies(c.Replies, id, result) {\n\t\t\tfound = true\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t})\n\treturn found\n}\n"},{"name":"doc.gno","body":"/*\nPackage blog is a simple blogging system that allows users to create, update, delete, and render blog posts.\n\nIt supports features like pagination, filtering by tags and authors, and rendering posts in Markdown format. The package is designed to be extensible and can be integrated into larger applications.\n\nExample:\n\n\timport \"gno.land/p/lou/blog\"\n\n\tfunc init(cur realm) {\n\t\tmyBlog, _ = blog.NewBlog(\n\t\t\t0, cur,\n\t\t\t\"Personal Blog\",\n\t\t\t\"g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs\",\n\t\t\tblog.WithUserResolver(myResolver),\n\t\t)\n\t}\n\n\tfunc CreatePost(cur realm, slug, title, body, publicationDate, authors, tags string) {\n\t\tauthorsField := strings.Split(authors, \" \")\n\t\ttagsField := strings.Split(tags, \" \")\n\n\t\tpost, err := blog.NewPost(\n\t\t\tslug,\n\t\t\ttitle,\n\t\t\tbody,\n\t\t\tpublicationDate,\n\t\t\tunsafe.OriginCaller().String(),\n\t\t\tauthorsField,\n\t\t\ttagsField,\n\t\t)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tif err := myBlog.AddPost(0, cur, post); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\tfunc Render(path string) string {\n\t\treturn myBlog.Render(path)\n\t}\n\n\tfunc myResolver(input string) (string, bool) {\n\t\tdata, ok := users.ResolveAny(input)\n\t\tif !ok || data == nil {\n\t\t\treturn \"\", false\n\t\t}\n\t\treturn data.Name(), true\n\t}\n*/\npackage blog // import \"gno.land/p/lou/blog\"\n"},{"name":"errors.gno","body":"package blog\n\nimport \"errors\"\n\nvar (\n\tErrPostAlreadyExists = errors.New(\"post already exists\")\n\n\tErrPostNotFound    = errors.New(\"post not found\")\n\tErrCommentNotFound = errors.New(\"comment not found\")\n\tErrLikesDisabled   = errors.New(\"likes are disabled\")\n\tErrAlreadyLiked    = errors.New(\"this address has already liked\")\n\tErrNotLiked        = errors.New(\"this address hasn't liked yet\")\n\n\tErrInvalidPostID    = errors.New(\"invalid post ID\")\n\tErrInvalidCommentID = errors.New(\"invalid comment ID\")\n\tErrInvalidPost      = errors.New(\"post not found/ invalid\")\n\n\tErrEmptyPrefix  = errors.New(\"prefix cannot be empty\")\n\tErrEmptyTitle   = errors.New(\"title cannot be empty\")\n\tErrEmptyBody    = errors.New(\"body cannot be empty\")\n\tErrEmptySlug    = errors.New(\"slug cannot be empty\")\n\tErrEmptyComment = errors.New(\"content of comment cannot be empty\")\n\n\tErrDeleteFailed        = errors.New(\"delete operation failed\")\n\tErrUnlikeFailed        = errors.New(\"failed to unlike post\")\n\tErrCommentDeleteFailed = errors.New(\"failed to delete comment\")\n\n\tErrInvalidCaller = errors.New(\"invalid caller address\")\n\tErrNotSuperuser  = errors.New(\"address is not owner\")\n)\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/lou/blog\"\ngno = \"0.9\"\n"},{"name":"header.gno","body":"package blog\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/lou/query\"\n\t\"gno.land/p/moul/md/v0\"\n)\n\n// HeaderLink applies to a function that transform the rawPath to make the suggested query changes (when header is interacted with)\n// will typically returns a md.Link as string\ntype HeaderLink func(rawPath string) string\n\ntype HeaderPreset struct {\n\tLabel   string // title of header (can be your query value, etc...)\n\tDefault string // default value, if none just \"\"\n\tRender  HeaderLink\n}\n\nfunc RenderModeToggle(rawPath string) string {\n\treturn md.Link(\"⊞ grid\", GetGridFmtPath(rawPath)) + \" | \" +\n\t\tmd.Link(\"≔ list\", GetListFmtPath(rawPath))\n}\n\nfunc RenderTimeFormat(rawPath string) string {\n\ttime := query.GetQuery(\"time\", rawPath)\n\tif time == \"\" {\n\t\ttime = \"relative\"\n\t}\n\treturn md.Link(\"⧖ \"+time, GetTimeFmtPath(rawPath, toggleTimeFormat(time)))\n}\n\nfunc RenderSortAlpha(rawPath string) string {\n\tsort := query.GetQuery(\"sort\", rawPath)\n\torder := query.GetQuery(\"order\", rawPath)\n\tif order == \"\" {\n\t\torder = \"desc\"\n\t}\n\talphaOrder := \"(A-Z)\"\n\tif order == \"desc\" \u0026\u0026 sort == \"alpha\" {\n\t\talphaOrder = \"(Z-A)\"\n\t}\n\treturn md.Link(orderArrow(\"alpha\", sort, order)+\" alphabetical \"+alphaOrder,\n\t\tGetSortByAlphabeticalPath(rawPath, order))\n}\n\nfunc RenderSortRecent(rawPath string) string {\n\tsort := query.GetQuery(\"sort\", rawPath)\n\torder := query.GetQuery(\"order\", rawPath)\n\tif order == \"\" {\n\t\torder = \"desc\"\n\t}\n\tpublishedOrder := \" recent\"\n\tif order == \"desc\" \u0026\u0026 sort == \"recent\" {\n\t\tpublishedOrder = \" oldest\"\n\t}\n\treturn md.Link(orderArrow(\"recent\", sort, order)+publishedOrder,\n\t\tGetSortByPublishedPath(rawPath, order))\n}\n\nfunc RenderSortUpdate(rawPath string) string {\n\tsort := query.GetQuery(\"sort\", rawPath)\n\torder := query.GetQuery(\"order\", rawPath)\n\tif order == \"\" {\n\t\torder = \"desc\"\n\t}\n\treturn md.Link(orderArrow(\"update\", sort, order)+\" last updated\",\n\t\tGetSortByUpdatedPath(rawPath, order))\n}\n\nfunc RenderSortCommon(rawPath string) string {\n\tsort := query.GetQuery(\"sort\", rawPath)\n\torder := query.GetQuery(\"order\", rawPath)\n\tif order == \"\" {\n\t\torder = \"desc\"\n\t}\n\tcommonOrder := \" most common\"\n\tif order == \"desc\" \u0026\u0026 sort == \"common\" {\n\t\tcommonOrder = \" least common\"\n\t}\n\treturn md.Link(orderArrow(\"common\", sort, order)+commonOrder,\n\t\tGetSortByCommonPath(rawPath, order))\n}\n\nfunc RenderHeader(rawPath string, presets []HeaderPreset) string {\n\tout := \"\"\n\tfor i, preset := range presets {\n\t\tif preset.Render != nil {\n\t\t\tout += preset.Render(rawPath)\n\t\t} else {\n\t\t\tout += preset.Label + \": \" + preset.Default\n\t\t}\n\t\tif i \u003c len(presets)-1 {\n\t\t\tout += \" | \"\n\t\t}\n\t}\n\treturn out\n}\n\n// TimeRangePreset struct is used for any customisation of the time range feature\ntype TimeRangePreset struct {\n\tLabel string // title of time range\n\tStart string // start time in form of string (check compatibility through `tryParseFlexibleDate`)\n\tEnd   string // end time ^^\n}\n\nvar DefaultTimeRanges = []TimeRangePreset{\n\t{\n\t\t\"past year\",\n\t\ttime.Now().AddDate(-1, 0, 0).Format(\"2006-01-02\"),\n\t\ttime.Now().Format(\"2006-01-02\"),\n\t},\n\t{\n\t\t\"this year\",\n\t\ttime.Date(time.Now().Year(), 1, 1, 0, 0, 0, 0, time.Now().Location()).Format(\"2006-01-02\"),\n\t\ttime.Now().Format(\"2006-01-02\"),\n\t},\n\t{\n\t\t\"last 30 days\",\n\t\ttime.Now().AddDate(0, 0, -30).Format(\"2006-01-02\"),\n\t\ttime.Now().Format(\"2006-01-02\"),\n\t},\n}\n\nfunc RenderTimeRangeLinks(rawPath string, presets *[]TimeRangePreset) string {\n\tout := \"\"\n\ttimePresets := presets\n\tif presets == nil {\n\t\ttimePresets = \u0026DefaultTimeRanges\n\t}\n\tfor i, r := range *timePresets {\n\t\tlink, _ := query.UpdateQueryFirstValues(rawPath, map[string]string{\n\t\t\t\"start\": r.Start,\n\t\t\t\"end\":   r.End,\n\t\t})\n\t\tout += md.Link(r.Label, link)\n\t\tif i \u003c len(*timePresets)-1 {\n\t\t\tout += \", \"\n\t\t}\n\t}\n\treturn out\n}\n"},{"name":"moderation.gno","body":"package blog\n\nfunc (b *Blog) AddModerator(_ int, rlm realm, addr address) error {\n\treturn b.Authorizable.AddToAuthList(0, rlm, addr)\n}\n\nfunc (b *Blog) DeleteModerator(_ int, rlm realm, addr address) error {\n\treturn b.Authorizable.DeleteFromAuthList(0, rlm, addr)\n}\n\ntype UserResolver func(input string) (string, bool)\n\nfunc CheckAddr(addr address) error {\n\tif !addr.IsValid() {\n\t\treturn ErrInvalidCaller\n\t}\n\treturn nil\n}\n\nfunc CheckUser(addr string, resolver UserResolver) (string, error) {\n\tif CheckAddr(address(addr)) != nil {\n\t\treturn \"\", ErrInvalidCaller\n\t}\n\tif !isRegistered(addr, resolver) {\n\t\treturn addr, nil\n\t}\n\tuserData, _ := resolver(addr)\n\treturn userData, nil\n}\n\nfunc isRegistered(addr string, resolver UserResolver) bool {\n\tif resolver == nil {\n\t\treturn false\n\t}\n\tuserData, _ := resolver(addr)\n\tif userData == \"\" {\n\t\treturn false\n\t}\n\treturn true\n}\n"},{"name":"post.gno","body":"package blog\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\ntype Post struct {\n\tid           seqid.ID\n\tslug         string\n\ttitle        string\n\tbody         string\n\tcreatedAt    time.Time\n\tupdatedAt    time.Time\n\tpublishedAt  time.Time\n\ttags         []string\n\tauthors      []string\n\tpublisher    string\n\tlikes        *avl.Tree // addr --\u003e bool\n\tCommentsId   seqid.ID\n\tComments     *avl.Tree // id --\u003e *Comment\n\tCommentsSize int\n\n\tPreviewFooter   string // additional text, e.g. \"via @lou\" or txlink calls\n\tDisableLikes    bool\n\tDisableComments bool\n\tUserResolver    UserResolver\n}\n\nfunc (p Post) ID() string {\n\treturn p.id.String()\n}\n\nfunc (p Post) Slug() string {\n\treturn p.slug\n}\n\nfunc (p Post) Title() string {\n\treturn p.title\n}\n\nfunc (p Post) Body() string {\n\treturn p.body\n}\n\nfunc (p Post) CreatedAt() time.Time {\n\treturn p.createdAt\n}\n\nfunc (p Post) UpdatedAt() time.Time {\n\treturn p.updatedAt\n}\n\nfunc (p Post) PublishedAt() time.Time {\n\treturn p.publishedAt\n}\n\nfunc (p Post) Tags() []string {\n\treturn p.tags\n}\n\nfunc (p Post) Authors() []string {\n\treturn p.authors\n}\n\nfunc (p Post) Publisher() string {\n\treturn p.publisher\n}\n\nfunc (p Post) Likes() int {\n\treturn p.likes.Size()\n}\n\nfunc NewPost(slug, title, body, publicationDate, caller string, authors, tags []string) (*Post, error) {\n\tnewSlug := handleSlug(slug, title)\n\tif title == \"\" {\n\t\treturn nil, ErrEmptyTitle\n\t}\n\tif body == \"\" {\n\t\treturn nil, ErrEmptyBody\n\t}\n\n\tvar err error\n\tpublishedAt := time.Now()\n\tif publicationDate != \"\" {\n\t\tpublishedAt, err = time.Parse(time.RFC3339, publicationDate)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn \u0026Post{\n\t\tslug:            newSlug,\n\t\ttitle:           title,\n\t\tbody:            body,\n\t\tcreatedAt:       publishedAt,\n\t\tupdatedAt:       publishedAt,\n\t\tpublishedAt:     time.Now(),\n\t\ttags:            tags,\n\t\tauthors:         authors,\n\t\tpublisher:       caller,\n\t\tlikes:           avl.NewTree(),\n\t\tCommentsId:      seqid.ID(0),\n\t\tComments:        avl.NewTree(),\n\t\tCommentsSize:    0,\n\t\tPreviewFooter:   \"\",\n\t\tDisableLikes:    false,\n\t\tDisableComments: false,\n\t\tUserResolver:    nil,\n\t}, nil\n}\n\nfunc (p *Post) UpdatePost(newPost *Post) {\n\tif p.UserResolver != nil {\n\t\tp.authors = p.resolveAuthors(p.Authors())\n\t\tp.publisher, _ = CheckUser(p.Publisher(), p.UserResolver)\n\t} else {\n\t\tp.authors = newPost.authors\n\t}\n\n\tp.slug = newPost.slug\n\tp.title = newPost.title\n\tp.body = newPost.body\n\tp.tags = newPost.tags\n\tp.CommentsId = newPost.CommentsId\n\tp.Comments = newPost.Comments\n\tp.likes = newPost.likes\n\tp.DisableLikes = newPost.DisableLikes\n\tp.DisableComments = newPost.DisableComments\n\tp.PreviewFooter = newPost.PreviewFooter\n\tp.updatedAt = time.Now()\n}\n\nfunc (p *Post) AddComment(comment *Comment) error {\n\tif p == nil {\n\t\treturn ErrInvalidPost\n\t}\n\tuser, err := CheckUser(comment.Author(), p.UserResolver)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcomment.author = user\n\n\tp.CommentsId.Next()\n\tp.CommentsSize++\n\tcomment.SetID(p.CommentsId)\n\tp.Comments.Set(p.CommentsId.String(), comment)\n\treturn nil\n}\n\nfunc (p *Post) AddReply(parentID string, reply *Comment) error {\n\tif p == nil {\n\t\treturn ErrInvalidPost\n\t}\n\tuser, err := CheckUser(reply.Author(), p.UserResolver)\n\tif err != nil {\n\t\treturn err\n\t}\n\treply.author = user\n\n\tparent, err := p.GetCommentByID(parentID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tp.CommentsId.Next()\n\tp.CommentsSize++\n\treply.SetID(p.CommentsId)\n\tparent.Replies.Set(p.CommentsId.String(), reply)\n\treturn nil\n}\n\nfunc (p *Post) EditCommentByID(id string, content string) error {\n\tcomment, err := p.GetCommentByID(id)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := comment.Edit(content); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (p *Post) DeleteCommentById(id string) error {\n\tif _, err := p.GetCommentByID(id); err != nil {\n\t\treturn err\n\t}\n\t_, removed := p.Comments.Remove(id)\n\tif !removed {\n\t\treturn ErrCommentDeleteFailed\n\t}\n\tp.CommentsSize--\n\treturn nil\n}\n\nfunc (p *Post) PinCommentById(id string) error {\n\tcomment, err := p.GetCommentByID(id)\n\tif err != nil {\n\t\treturn ErrCommentNotFound\n\t}\n\tcomment.Pin()\n\treturn nil\n}\n\nfunc (p *Post) LikePost(addr string) error {\n\tif p.DisableLikes {\n\t\treturn ErrLikesDisabled\n\t}\n\tif p.likes.Has(addr) {\n\t\treturn ErrAlreadyLiked\n\t}\n\tp.likes.Set(addr, true)\n\treturn nil\n}\n\nfunc (p *Post) UnlikePost(addr string) error {\n\tif p.DisableLikes {\n\t\treturn ErrLikesDisabled\n\t}\n\tif !p.likes.Has(addr) {\n\t\treturn ErrNotLiked\n\t}\n\tif _, removed := p.likes.Remove(addr); !removed {\n\t\treturn ErrUnlikeFailed\n\t}\n\treturn nil\n}\n\nfunc (p *Post) SetPreviewFooter(footer string) {\n\tp.PreviewFooter = footer\n}\n\nfunc (p Post) GetCommentByID(id string) (*Comment, error) {\n\tcomment := p.Comments.Get(id)\n\tif comment != nil {\n\t\treturn comment.(*Comment), nil\n\t}\n\n\tvar result *Comment\n\tp.Comments.ReverseIterate(\"\", \"\", func(_ string, v interface{}) bool {\n\t\tc := v.(*Comment)\n\t\tif found := searchInReplies(c.Replies, id, \u0026result); found {\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t})\n\tif result == nil {\n\t\treturn nil, ErrCommentNotFound\n\t}\n\treturn result, nil\n}\n\nfunc (p Post) GetCommentsByAuthor(author string) []*Comment {\n\tvar comments []*Comment = nil\n\tp.Comments.ReverseIterate(\"\", \"\", func(_ string, comment interface{}) bool {\n\t\tc := comment.(*Comment)\n\t\tif c.Author() == author {\n\t\t\tcomments = append(comments, c)\n\t\t}\n\t\tcomments = append(comments, p.getReplies(c.Replies, author)...)\n\t\treturn false\n\t})\n\treturn comments\n}\n\nfunc (p Post) RenderPreview(gridMode bool, timeFmt, prefix string) string {\n\tout := md.H2(md.Link(p.Title(), prefix+\":posts/\"+p.Slug())) + \"\\n\\n\"\n\tout += md.H5(md.Italic(\"/\"+p.Slug())) + \"\\n\\n\"\n\tif p.UpdatedAt() != p.CreatedAt() {\n\t\tout += formatTime(p.UpdatedAt(), timeFmt) + \" (updated)\\n\\n\"\n\t} else {\n\t\tout += formatTime(p.CreatedAt(), timeFmt) + \"\\n\\n\"\n\t}\n\tif !gridMode {\n\t\tout += md.Bold(\"author(s):\") + \" \" + strings.Join(p.Authors(), \", \") + \"\\n\\n\"\n\t}\n\tif len(p.Tags()) \u003e 0 \u0026\u0026 p.Tags()[0] != \"\" {\n\t\tout += md.Bold(\"tags:\") + \" `\" + strings.Join(p.Tags(), \"`, `\") + \"`\\n\\n\"\n\t}\n\tif !p.DisableComments {\n\t\tout += md.Bold(md.Link(\"comments (\"+strconv.Itoa(p.CommentsSize), prefix+\":posts/\"+p.Slug()+\"#comments\")) + \") \"\n\t}\n\tif !p.DisableLikes {\n\t\tout += ufmt.Sprintf(\"| ❤️ (%d)\\n\\n\", p.Likes())\n\t}\n\tif p.PreviewFooter != \"\" {\n\t\tout += p.PreviewFooter + \"\\n\\n\"\n\t}\n\tout += md.HorizontalRule()\n\treturn out\n}\n\nfunc (p Post) RenderPost(prefix string) string {\n\tresolvedAuthors := p.resolveAuthors(p.Authors())\n\tout := md.H1(p.Title()) + \"\\n\\n\"\n\tif p.Comments.Size() \u003e 0 {\n\t\tout += md.Link(strconv.Itoa(p.CommentsSize)+\" Comment(s)\", \"#comments\") + \"\\n\\n\"\n\t}\n\tout += md.Italic(\"Author(s):\") + \" \" + renderAuthorLinks(prefix, resolvedAuthors) + \"\\n\\n\"\n\tout += p.Body() + \"\\n\\n\"\n\tout += md.HorizontalRule()\n\tout += md.Bold(\"Created on:\") + \" \" + formatTime(p.CreatedAt(), \"full\") + \"\\n\\n\"\n\tout += md.Bold(\"Published on:\") + \" \" + formatTime(p.PublishedAt(), \"full\") + \"\\n\\n\"\n\tif p.UpdatedAt() != p.CreatedAt() {\n\t\tout += md.Bold(\"Last updated:\") + \" \" + formatTime(p.UpdatedAt(), \"full\") + \"\\n\\n\"\n\t}\n\tout += md.Bold(\"Publisher:\") + \" \" + md.Link(p.Publisher(), \"/u/\"+p.Publisher()) + \"\\n\\n\"\n\tout += md.Bold(\"Tags:\") + \" \" + renderTagLinks(prefix, p.Tags()) + \"\\n\\n\"\n\tif !p.DisableLikes {\n\t\tout += ufmt.Sprintf(\"❤️ %d\\n\\n\", p.Likes())\n\t}\n\tif !p.DisableComments {\n\t\tout += md.HorizontalRule()\n\t\tout += p.RenderComments(prefix)\n\t}\n\treturn out\n}\n\nfunc (p Post) RenderComments(prefix string) string {\n\tout := md.H3(\"Comments (\"+strconv.Itoa(p.CommentsSize)+\")\") + \"\\n\\n\"\n\tif p.Comments.Size() == 0 {\n\t\tout += \"No comments yet.\\n\\n\"\n\t\treturn out\n\t}\n\tp.Comments.ReverseIterate(\"\", \"\", func(_ string, v interface{}) bool {\n\t\tcomment := v.(*Comment)\n\t\tif comment.Pinned() {\n\t\t\tout += comment.Render(prefix, 0, p.UserResolver) + \"\\n\\n\"\n\t\t}\n\t\treturn false\n\t})\n\tp.Comments.ReverseIterate(\"\", \"\", func(_ string, v interface{}) bool {\n\t\tcomment := v.(*Comment)\n\t\tif !comment.Pinned() {\n\t\t\tout += comment.Render(prefix, 0, p.UserResolver) + \"\\n\\n\"\n\t\t}\n\t\treturn false\n\t})\n\treturn out\n}\n\nfunc (p Post) getReplies(replies *avl.Tree, author string) []*Comment {\n\tvar comments []*Comment\n\tif replies == nil {\n\t\treturn comments\n\t}\n\treplies.ReverseIterate(\"\", \"\", func(_ string, value any) bool {\n\t\tcomment := value.(*Comment)\n\t\tif comment.Author() == author {\n\t\t\tcomments = append(comments, comment)\n\t\t}\n\t\tif comment.Replies.Size() \u003e 0 {\n\t\t\tcomments = append(comments, p.getReplies(comment.Replies, author)...)\n\t\t}\n\t\treturn false\n\t})\n\treturn comments\n}\n\nfunc (p Post) resolveAuthors(authors []string) []string {\n\tvar resolvedAuthors []string\n\tfor _, author := range authors {\n\t\tuser, err := CheckUser(author, p.UserResolver)\n\t\tif err == nil {\n\t\t\tresolvedAuthors = append(resolvedAuthors, user)\n\t\t} else {\n\t\t\tresolvedAuthors = append(resolvedAuthors, author)\n\t\t}\n\t}\n\treturn resolvedAuthors\n}\n\nfunc renderTagLinks(prefix string, tags []string) string {\n\treturn renderLinks(prefix, \"tags\", tags, \"#\")\n}\n\nfunc renderAuthorLinks(prefix string, authors []string) string {\n\treturn renderLinks(prefix, \"authors\", authors, \"\")\n}\n"},{"name":"query.gno","body":"package blog\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/lou/query\"\n)\n\ntype RenderOptions struct {\n\tSort           string\n\tTimeFormat     string\n\tStartTime      *time.Time\n\tEndTime        *time.Time\n\tAscending      bool\n\tIsGrid         bool\n\tIsAlphabetical bool\n\tIsLastUpdated  bool\n\tIsCommon       bool\n\tHasOrder       bool\n}\n\nfunc ParseRenderOptions(rawPath string) RenderOptions {\n\tsort := query.GetQuery(\"sort\", rawPath)\n\torder := query.GetQuery(\"order\", rawPath)\n\tmode := query.GetQuery(\"mode\", rawPath)\n\ttimeFmt := query.GetQuery(\"time\", rawPath)\n\tstartTime, endTime := parseTimeRange(rawPath)\n\n\treturn RenderOptions{\n\t\tSort:           sort,\n\t\tTimeFormat:     timeFmt,\n\t\tStartTime:      startTime,\n\t\tEndTime:        endTime,\n\t\tAscending:      order == \"asc\" || order == \"\",\n\t\tIsGrid:         mode == \"grid\" || mode == \"\",\n\t\tIsAlphabetical: sort == \"alpha\",\n\t\tIsLastUpdated:  sort == \"update\",\n\t\tIsCommon:       sort == \"common\",\n\t\tHasOrder:       order == \"asc\" || order == \"desc\",\n\t}\n}\n\nfunc GetGridFmtPath(rawPath string) string {\n\tlink, _ := query.UpdateQueryValue(rawPath, \"mode\", \"grid\")\n\treturn link\n}\n\nfunc GetListFmtPath(rawPath string) string {\n\tlink, _ := query.UpdateQueryValue(rawPath, \"mode\", \"list\")\n\treturn link\n}\n\nfunc GetSortByPublishedPath(rawPath, order string) string {\n\tlink, _ := query.UpdateQueryFirstValues(rawPath, map[string]string{\n\t\t\"sort\":  \"recent\",\n\t\t\"order\": toggle(order),\n\t})\n\treturn link\n}\n\nfunc GetTimeFmtPath(rawPath, timeFormat string) string {\n\tlink, _ := query.UpdateQueryValue(rawPath, \"time\", timeFormat)\n\treturn link\n}\n\nfunc GetTimeStartPath(rawPath, startTime string) string {\n\tlink, _ := query.UpdateQueryValue(rawPath, \"start\", startTime)\n\treturn link\n}\n\nfunc GetTimeEndPath(rawPath, endTime string) string {\n\tlink, _ := query.UpdateQueryValue(rawPath, \"end\", endTime)\n\treturn link\n}\n\nfunc GetSortByUpdatedPath(rawPath, order string) string {\n\tlink, _ := query.UpdateQueryFirstValues(rawPath, map[string]string{\n\t\t\"sort\":  \"update\",\n\t\t\"order\": toggle(order),\n\t})\n\treturn link\n}\n\nfunc GetSortByAlphabeticalPath(rawPath, order string) string {\n\tlink, _ := query.UpdateQueryFirstValues(rawPath, map[string]string{\n\t\t\"sort\":  \"alpha\",\n\t\t\"order\": toggle(order),\n\t})\n\treturn link\n}\n\nfunc GetSortByCommonPath(rawPath, order string) string {\n\tlink, _ := query.UpdateQueryFirstValues(rawPath, map[string]string{\n\t\t\"sort\":  \"common\",\n\t\t\"order\": toggle(order),\n\t})\n\treturn link\n}\n\nfunc toggle(current string) string {\n\tif current == \"desc\" {\n\t\treturn \"asc\"\n\t}\n\treturn \"desc\"\n}\n\nfunc toggleTimeFormat(current string) string {\n\tif current == \"relative\" {\n\t\treturn \"short\"\n\t}\n\tif current == \"short\" {\n\t\treturn \"full\"\n\t}\n\tif current == \"full\" {\n\t\treturn \"relative\"\n\t}\n\treturn \"short\"\n}\n\nfunc parseTimeRange(rawPath string) (*time.Time, *time.Time) {\n\tstartStr := query.GetQuery(\"start\", rawPath)\n\tendStr := query.GetQuery(\"end\", rawPath)\n\tvar start, end *time.Time\n\n\tif startStr != \"\" {\n\t\tif t := tryParseFlexibleDate(startStr); t != nil {\n\t\t\tstart = t\n\t\t}\n\t}\n\tif endStr != \"\" {\n\t\tif t := tryParseFlexibleDate(endStr); t != nil {\n\t\t\tend = t\n\t\t}\n\t}\n\treturn start, end\n}\n"},{"name":"query_test.gno","body":"package blog\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nfunc TestParseRenderOptions(t *testing.T) {\n\tt.Run(\"default options with empty path\", func(t *testing.T) {\n\t\topts := ParseRenderOptions(\"/blog\")\n\t\tuassert.Equal(t, \"\", opts.Sort)\n\t\tuassert.Equal(t, \"\", opts.TimeFormat)\n\t\tuassert.True(t, opts.Ascending)\n\t\tuassert.True(t, opts.IsGrid)\n\t\tuassert.False(t, opts.IsAlphabetical)\n\t\tuassert.False(t, opts.IsLastUpdated)\n\t\tuassert.False(t, opts.IsCommon)\n\t\tuassert.False(t, opts.HasOrder)\n\t})\n\n\tt.Run(\"full options parsing\", func(t *testing.T) {\n\t\turl := \"/blog?sort=alpha\u0026order=desc\u0026mode=list\u0026time=full\"\n\t\topts := ParseRenderOptions(url)\n\t\tuassert.Equal(t, \"alpha\", opts.Sort)\n\t\tuassert.Equal(t, \"full\", opts.TimeFormat)\n\t\tuassert.False(t, opts.Ascending)\n\t\tuassert.False(t, opts.IsGrid)\n\t\tuassert.True(t, opts.IsAlphabetical)\n\t\tuassert.False(t, opts.IsLastUpdated)\n\t\tuassert.False(t, opts.IsCommon)\n\t\tuassert.True(t, opts.HasOrder)\n\t})\n}\n\nfunc TestToggle(t *testing.T) {\n\tuassert.Equal(t, \"asc\", toggle(\"desc\"))\n\tuassert.Equal(t, \"desc\", toggle(\"asc\"))\n\tuassert.Equal(t, \"desc\", toggle(\"\")) // fallback\n}\n\nfunc TestToggleTimeFormat(t *testing.T) {\n\tuassert.Equal(t, \"short\", toggleTimeFormat(\"relative\"))\n\tuassert.Equal(t, \"full\", toggleTimeFormat(\"short\"))\n\tuassert.Equal(t, \"relative\", toggleTimeFormat(\"full\"))\n\tuassert.Equal(t, \"short\", toggleTimeFormat(\"\")) // fallback\n}\n\nfunc TestGetSortHelpers(t *testing.T) {\n\tbase := \"/blog?sort=recent\u0026order=desc\"\n\n\tuassert.Equal(t, \"/blog?mode=grid\", GetGridFmtPath(\"/blog\"))\n\tuassert.Equal(t, \"/blog?mode=list\", GetListFmtPath(\"/blog\"))\n\n\tuassert.Equal(t, \"/blog?order=asc\u0026sort=recent\", GetSortByPublishedPath(base, \"desc\"))\n\tuassert.Equal(t, \"/blog?order=asc\u0026sort=update\", GetSortByUpdatedPath(base, \"desc\"))\n\tuassert.Equal(t, \"/blog?order=asc\u0026sort=alpha\", GetSortByAlphabeticalPath(base, \"desc\"))\n\tuassert.Equal(t, \"/blog?order=asc\u0026sort=common\", GetSortByCommonPath(base, \"desc\"))\n}\n\nfunc TestTimeFormatAndRangeHelpers(t *testing.T) {\n\tbase := \"/blog\"\n\n\tuassert.Equal(t, \"/blog?time=full\", GetTimeFmtPath(base, \"full\"))\n\tuassert.Equal(t, \"/blog?start=2024-01-01\", GetTimeStartPath(base, \"2024-01-01\"))\n\tuassert.Equal(t, \"/blog?end=2025-01-01\", GetTimeEndPath(base, \"2025-01-01\"))\n}\n\nfunc TestParseTimeRange(t *testing.T) {\n\tpath := \"/blog?start=2024-01-01\u0026end=2024-12-31\"\n\tstart, end := parseTimeRange(path)\n\tuassert.NotEqual(t, nil, start)\n\tuassert.NotEqual(t, nil, end)\n\n\tuassert.Equal(t, true, start.Before(*end))\n}\n"},{"name":"render.gno","body":"package blog\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n\n\t\"gno.land/p/lou/query\"\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/nt/avl/pager/v0\"\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\nfunc (b Blog) Render(path string) string {\n\trouter := mux.NewRouter()\n\trouter.HandleFunc(\"\", b.RenderPosts)\n\trouter.HandleFunc(\"posts\", b.RenderPosts)\n\trouter.HandleFunc(\"posts/{slug}\", b.RenderPost)\n\trouter.HandleFunc(\"tags\", b.RenderTags)\n\trouter.HandleFunc(\"tags/{slug}\", b.RenderTags)\n\trouter.HandleFunc(\"authors\", b.RenderAuthors)\n\trouter.HandleFunc(\"authors/{slug}\", b.RenderAuthors)\n\trouter.HandleFunc(\"commenters\", b.RenderCommenters)\n\trouter.HandleFunc(\"commenters/{slug}\", b.RenderCommenters)\n\treturn router.Render(path)\n}\n\nfunc RenderBlogHeader(prefix, rawPath string) string {\n\trawPath = prefix + \":\" + rawPath\n\theaderPresets := []HeaderPreset{\n\t\t{\"mode\", \"grid\", RenderModeToggle},\n\t\t{\"time\", \"relative\", RenderTimeFormat},\n\t\t{\"alpha\", \"\", RenderSortAlpha},\n\t\t{\"recent\", \"\", RenderSortRecent},\n\t\t{\"update\", \"\", RenderSortUpdate},\n\t\t{\"time-range\", \"\", func(p string) string { return RenderTimeRangeLinks(p, nil) }},\n\t\t{\"reset\", \"\", func(p string) string { return md.Link(\"⟳ reset\", prefix) }},\n\t}\n\treturn RenderHeader(rawPath, headerPresets)\n}\n\n// :posts\nfunc (b Blog) RenderPosts(res *mux.ResponseWriter, req *mux.Request) {\n\tout := md.H1(b.Title()) + \"\\n\"\n\tout += RenderBlogHeader(b.Prefix(), req.RawPath) + \"\\n\\n\"\n\tif b.Posts.Size() == 0 {\n\t\tres.Write(out + \"No posts found.\")\n\t\treturn\n\t}\n\n\toptions := ParseRenderOptions(req.RawPath)\n\tpageSize := 9\n\tif !options.IsGrid {\n\t\tpageSize = 3\n\t}\n\torder := options.Ascending\n\tif options.IsAlphabetical {\n\t\torder = !order\n\t}\n\n\tvar tree *avl.Tree\n\tswitch {\n\tcase options.IsLastUpdated:\n\t\ttree = b.PostsByUpdatedAt\n\tcase options.IsAlphabetical:\n\t\ttree = b.PostsBySlug\n\tdefault:\n\t\ttree = b.Posts\n\t}\n\tif options.StartTime != nil || options.EndTime != nil {\n\t\ttree = b.filterPostsStartEnd(tree, options.StartTime, options.EndTime)\n\t}\n\n\tp := pager.NewPager(tree, pageSize, order)\n\tpage := p.MustGetPageByPath(req.RawPath)\n\tout += b.RenderListGrid(page, options.IsGrid, options.TimeFormat) + page.Picker(req.Path)\n\tres.Write(out)\n}\n\nfunc (b Blog) RenderListGrid(page *pager.Page, gridMode bool, timeFmt string) string {\n\tcolCount := 0\n\tout := \"\u003cgno-columns\u003e\\n\"\n\tfor _, item := range page.Items {\n\t\tif colCount%3 == 0 {\n\t\t\tout += \"\u003cgno-columns\u003e\\n\"\n\t\t}\n\t\tpost := item.Value.(*Post)\n\t\tout += post.RenderPreview(gridMode, timeFmt, b.Prefix())\n\t\tcolCount++\n\t\tif colCount%3 == 0 {\n\t\t\tout += \"\u003c/gno-columns\u003e\\n\"\n\t\t} else if gridMode {\n\t\t\tout += \"\u003cgno-columns-sep\u003e\\n\"\n\t\t}\n\t}\n\tif colCount%3 != 0 {\n\t\tout += \"\u003c/gno-columns\u003e\\n\"\n\t}\n\treturn out\n}\n\n// :posts/{slug}\nfunc (b Blog) RenderPost(res *mux.ResponseWriter, req *mux.Request) {\n\tpostSlug := req.GetVar(\"slug\")\n\tfoundKey, found := b.findPostBySlug(url.PathEscape(postSlug))\n\tif !found {\n\t\tres.Write(\"Post not found.\")\n\t\treturn\n\t}\n\tpost := b.Posts.Get(foundKey)\n\tout := post.(*Post).RenderPost(b.Prefix())\n\n\tres.Write(out)\n}\n\nfunc (b Blog) RenderFilterHeader(req *mux.Request, user string, isListing bool) string {\n\tout := \"\"\n\tfield := strings.Split(req.RawPath, \"/\")[0]\n\tswitch field {\n\tcase \"commenters\":\n\t\tout += md.H2(md.Link(b.Prefix(), b.Prefix()) + \"/commenters/\" + user)\n\t\tout += md.H4(md.Link(\"@\"+user, \"/u/\"+user) + \"'s profile\")\n\tcase \"authors\":\n\t\tout += md.H2(renderBreadcrumb(b.Prefix(), req.Path))\n\t\tif user != \"\" {\n\t\t\tout += md.H4(md.Link(\"@\"+user, \"/u/\"+user) + \"'s profile\")\n\t\t}\n\tdefault:\n\t\tout += md.H2(renderBreadcrumb(b.Prefix(), req.Path))\n\t}\n\n\tvar headerPresets []HeaderPreset\n\tif !isListing {\n\t\theaderPresets = append(headerPresets,\n\t\t\tHeaderPreset{\"mode\", \"grid\", RenderModeToggle},\n\t\t\tHeaderPreset{\"time\", \"relative\", RenderTimeFormat},\n\t\t)\n\t}\n\theaderPresets = append(headerPresets,\n\t\tHeaderPreset{\"alpha\", \"\", RenderSortAlpha},\n\t\tHeaderPreset{\"recent\", \"\", RenderSortRecent},\n\t)\n\n\tif isListing {\n\t\theaderPresets = append(headerPresets, HeaderPreset{\"common\", \"\", RenderSortCommon})\n\t} else {\n\t\theaderPresets = append(headerPresets, HeaderPreset{\"update\", \"\", RenderSortUpdate})\n\t}\n\theaderPresets = append(headerPresets,\n\t\tHeaderPreset{\"time-range\", \"\", func(p string) string { return RenderTimeRangeLinks(p, nil) }},\n\t\tHeaderPreset{\"reset\", \"\", func(p string) string { return md.Link(\"⟳ reset\", b.Prefix()) }},\n\t)\n\tout += RenderHeader(b.Prefix()+\":\"+req.RawPath, headerPresets)\n\treturn out + \"\\n\\n\"\n}\n\n// :tags\nfunc (b Blog) RenderTags(res *mux.ResponseWriter, req *mux.Request) {\n\tout := b.RenderListings(\"tag\", req.GetVar(\"slug\"), req)\n\tres.Write(out)\n}\n\n// :authors\nfunc (b Blog) RenderAuthors(res *mux.ResponseWriter, req *mux.Request) {\n\tout := b.RenderListings(\"author\", req.GetVar(\"slug\"), req)\n\tres.Write(out)\n}\n\n// :commenters\nfunc (b Blog) RenderCommenters(res *mux.ResponseWriter, req *mux.Request) {\n\tif req.GetVar(\"slug\") == \"\" {\n\t\tres.Write(\"Commenter slug is required.\")\n\t\treturn\n\t}\n\tout := b.RenderListings(\"commenter\", req.GetVar(\"slug\"), req)\n\tres.Write(out)\n}\n\nfunc (b Blog) RenderListings(field, value string, req *mux.Request) string {\n\toptions := ParseRenderOptions(req.RawPath)\n\tif query.GetQuery(\"mode\", req.RawPath) == \"\" {\n\t\toptions.IsGrid = false\n\t}\n\tif value == \"\" {\n\t\treturn b.renderListingIndex(field, options, req)\n\t}\n\tuser, err := CheckUser(value, b.UserResolver)\n\tif err == nil \u0026\u0026 user != \"\" {\n\t\treturn b.renderFilteredListing(field, user, options, req)\n\t}\n\treturn b.renderFilteredListing(field, value, options, req)\n}\n\nfunc (b Blog) renderListingIndex(field string, options RenderOptions, req *mux.Request) string {\n\tout := b.RenderFilterHeader(req, \"\", true)\n\tp, exists := b.determinePager(options, field, \"\")\n\tif !exists {\n\t\treturn out + \"No \" + field + \"s found.\"\n\t}\n\tpage := p.MustGetPageByPath(req.RawPath)\n\n\tfor _, item := range page.Items {\n\t\tlabel, count := extractListingDisplay(item.Key, item.Value)\n\t\tout += md.H3(md.Link(label, b.Prefix()+\":\"+field+\"s/\"+label)+\" (\"+ufmt.Sprintf(\"%d\", count)+\")\") + \"\\n\\n\"\n\t}\n\tout += page.Picker(req.Path) + \"\\n\\n\"\n\treturn out\n}\n\nfunc (b Blog) renderFilteredListing(field, value string, options RenderOptions, req *mux.Request) string {\n\tout := b.RenderFilterHeader(req, value, false)\n\tp, exists := b.determinePager(options, field, value)\n\tif !exists {\n\t\tout += \"No posts found for this \" + field + \".\"\n\t}\n\tpage := p.MustGetPageByPath(req.RawPath)\n\tout += \"\u003cgno-columns\u003e\\n\"\n\tcolCount := 0\n\tfor _, item := range page.Items {\n\t\tif colCount%3 == 0 {\n\t\t\tout += \"\u003cgno-columns\u003e\\n\"\n\t\t}\n\t\tcolCount++\n\t\tif field == \"commenter\" {\n\t\t\tout += b.renderCommenterItem(item, options)\n\t\t} else {\n\t\t\tpost := item.Value.(*Post)\n\t\t\tout += post.RenderPreview(options.IsGrid, options.TimeFormat, b.Prefix()) + \"\\n\"\n\t\t}\n\t\tif colCount%3 == 0 {\n\t\t\tout += \"\u003c/gno-columns\u003e\\n\"\n\t\t} else if options.IsGrid {\n\t\t\tout += \"\u003cgno-columns-sep\u003e\\n\"\n\t\t}\n\t}\n\tif colCount%3 != 0 {\n\t\tout += \"\u003c/gno-columns\u003e\\n\"\n\t}\n\tout += page.Picker(req.Path) + \"\\n\\n\"\n\treturn out\n}\n\nfunc (b Blog) renderCommenterItem(item pager.Item, options RenderOptions) string {\n\tcomment := item.Value.(*Comment)\n\tparts := strings.Split(item.Key, \"::\")\n\tif len(parts) \u003c 2 {\n\t\treturn \"\"\n\t}\n\tpostKey := parts[1]\n\n\tpost := b.Posts.Get(postKey)\n\tif post == nil {\n\t\tpost = b.PostsBySlug.Get(postKey)\n\t}\n\tif post == nil {\n\t\tpost = b.PostsByUpdatedAt.Get(postKey)\n\t}\n\tif post == nil {\n\t\treturn \"\"\n\t}\n\tp := post.(*Post)\n\n\tuser, _ := CheckUser(comment.Author(), b.UserResolver)\n\tout := md.H4(b.Mention(\"commenter\", user)) + \"\\n\\n\"\n\tout += comment.Content() + \"\\n\\n\"\n\tout += md.Italic(formatTime(comment.CreatedAt(), options.TimeFormat)) + \"\\n\\n\"\n\tout += \"in \" + md.Link(p.Title(), b.Prefix()+\":posts/\"+p.Slug()) + \"\\n\\n\"\n\tout += md.HorizontalRule()\n\treturn out\n}\n\nfunc (b Blog) determinePager(options RenderOptions, field, value string) (*pager.Pager, bool) {\n\tif value == \"\" {\n\t\tif options.IsAlphabetical \u0026\u0026 field == \"author\" {\n\t\t\treturn pager.NewPager(b.AuthorsIndex, 12, !options.Ascending), true\n\t\t} else if options.IsCommon \u0026\u0026 field == \"author\" {\n\t\t\treturn pager.NewPager(b.AuthorsSorted, 12, options.Ascending), true\n\t\t} else if options.IsAlphabetical \u0026\u0026 field == \"tag\" {\n\t\t\treturn pager.NewPager(b.TagsIndex, 12, !options.Ascending), true\n\t\t} else if options.IsCommon \u0026\u0026 field == \"tag\" {\n\t\t\treturn pager.NewPager(b.TagsSorted, 12, options.Ascending), true\n\t\t} else if field == \"author\" {\n\t\t\treturn pager.NewPager(b.AuthorsIndex, 12, options.Ascending), true\n\t\t} else if field == \"tag\" {\n\t\t\treturn pager.NewPager(b.TagsIndex, 12, options.Ascending), true\n\t\t}\n\t}\n\tpageSize := 9\n\tif !options.IsGrid {\n\t\tpageSize = 3\n\t}\n\n\tfilteredPosts, exists := b.filterPostsByField(field, value, options.Sort)\n\tif options.StartTime != nil || options.EndTime != nil {\n\t\tfilteredPosts = b.filterPostsStartEnd(filteredPosts, options.StartTime, options.EndTime)\n\t}\n\tif options.Sort == \"alpha\" {\n\t\treturn pager.NewPager(filteredPosts, pageSize, !options.Ascending), exists\n\t}\n\treturn pager.NewPager(filteredPosts, pageSize, options.Ascending), exists\n}\n\nfunc orderArrow(targetSort, currentSort, order string) string {\n\tif targetSort != currentSort {\n\t\treturn \"↕\"\n\t}\n\tif order == \"asc\" {\n\t\treturn \"▲\"\n\t}\n\treturn \"▼\"\n}\n"},{"name":"utils.gno","body":"package blog\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nfunc renderLinks(prefix, kind string, values []string, labelPrefix string) string {\n\tlinks := make([]string, len(values))\n\tfor i, val := range values {\n\t\tlabel := labelPrefix + val\n\t\thref := prefix + \":\" + kind + \"/\" + val\n\t\tlinks[i] = md.Link(label, href)\n\t}\n\treturn strings.Join(links, \", \") + \"\\n\"\n}\n\nfunc hasField(fields []string, match string) bool {\n\tfor _, field := range fields {\n\t\tif field == match {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc extractListingDisplay(key string, value interface{}) (label string, count int) {\n\tif strings.Contains(key, \"::\") {\n\t\tparts := strings.Split(key, \"::\")\n\t\tif len(parts) \u003e 1 {\n\t\t\treturn parts[1], value.(int)\n\t\t}\n\t}\n\treturn key, value.(int)\n}\n\nfunc renderBreadcrumb(prefix, path string) string {\n\tsegments := strings.Split(path, \"/\")\n\tbase := prefix\n\tvar parts []string\n\n\tif prefix != \"\" {\n\t\tparts = append(parts, md.Link(prefix, prefix))\n\t}\n\n\tfor i, seg := range segments {\n\t\tif seg == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tbase += \":\" + seg\n\t\tlabel := seg\n\t\tif i == len(segments)-1 {\n\t\t\tlabel = seg\n\t\t\tparts = append(parts, label)\n\t\t\tbreak\n\t\t}\n\t\tparts = append(parts, md.Link(label, base))\n\t}\n\treturn strings.Join(parts, \"/\")\n}\n\nfunc formatTime(t time.Time, format string) string {\n\tswitch format {\n\tcase \"relative\":\n\t\treturn calculateTimeSince(t)\n\tcase \"short\":\n\t\treturn t.Format(\"02 Jan 2006\")\n\tcase \"full\":\n\t\treturn t.Format(\"January 02, 2006 at 3:04 PM\")\n\tdefault:\n\t\treturn calculateTimeSince(t)\n\t}\n}\n\nfunc calculateTimeSince(t time.Time) string {\n\tduration := time.Since(t)\n\tswitch {\n\tcase duration \u003c time.Minute:\n\t\treturn \"just now\"\n\tcase duration \u003c time.Hour:\n\t\tif duration \u003c 2*time.Minute {\n\t\t\treturn \"a minute ago\"\n\t\t}\n\t\treturn ufmt.Sprintf(\"%d minutes ago\", int(duration.Minutes()))\n\tcase duration \u003c 24*time.Hour:\n\t\tif duration \u003c 2*time.Hour {\n\t\t\treturn \"an hour ago\"\n\t\t}\n\t\treturn ufmt.Sprintf(\"%d hours ago\", int(duration.Hours()))\n\tcase duration \u003c 7*24*time.Hour:\n\t\tif duration \u003c 2*24*time.Hour {\n\t\t\treturn \"a day ago\"\n\t\t}\n\t\treturn ufmt.Sprintf(\"%d days ago\", int(duration.Hours()/24))\n\tcase duration \u003c 30*24*time.Hour:\n\t\tif duration \u003c 2*7*24*time.Hour {\n\t\t\treturn \"a week ago\"\n\t\t}\n\t\treturn ufmt.Sprintf(\"%d weeks ago\", int(duration.Hours()/(7*24)))\n\tdefault:\n\t\tif duration \u003c 2*30*24*time.Hour {\n\t\t\treturn \"a month ago\"\n\t\t}\n\t\treturn ufmt.Sprintf(\"%d months ago\", int(duration.Hours()/(30*24)))\n\t}\n}\n\nfunc tryParseFlexibleDate(s string) *time.Time {\n\tformats := []string{\n\t\ttime.RFC3339, // rfc3339 format\n\t\t\"2006-01-02\", // date only\n\t\t\"2006-01\",    // year and month\n\t\t\"2006\",       // year only\n\t}\n\tfor _, format := range formats {\n\t\tif t, err := time.Parse(format, s); err == nil {\n\t\t\treturn \u0026t\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc titleToSlug(title string) string {\n\tslug := strings.ToLower(title)\n\tslug = strings.ReplaceAll(slug, \" \", \"-\")\n\tslug = strings.Map(func(r rune) rune {\n\t\tif r \u003e= 'a' \u0026\u0026 r \u003c= 'z' || r \u003e= '0' \u0026\u0026 r \u003c= '9' || r == '-' {\n\t\t\treturn r\n\t\t}\n\t\treturn -1\n\t}, slug)\n\treturn slug\n}\n\nfunc handleSlug(slug, title string) string {\n\tif slug == \"\" {\n\t\tslug = titleToSlug(title)\n\t}\n\tonce := url.PathEscape(slug)\n\ttwice := url.PathEscape(once)\n\treturn twice\n}\n"},{"name":"z_blog_0_filetest.gno","body":"// PKGPATH: gno.land/r/test\npackage test\n\nimport (\n\t\"gno.land/p/lou/blog\"\n)\n\nfunc main(cur realm) {\n\tuser := address(\"g1testuser\")\n\t_, err := blog.NewBlog(0, cur, \"\", user)\n\tif err != nil {\n\t\tprintln(\"error:\", err.Error())\n\t}\n}\n\n// Output:\n// error: title cannot be empty\n"},{"name":"z_blog_1_filetest.gno","body":"// PKGPATH: gno.land/r/test\npackage test\n\nimport (\n\t\"gno.land/p/lou/blog\"\n)\n\nfunc main(cur realm) {\n\t_, err := blog.NewBlog(0, cur, \"My Blog\", address(\"he\"))\n\tif err != nil {\n\t\tprintln(\"error:\", err.Error())\n\t}\n}\n\n// Output:\n// error: invalid caller address\n"},{"name":"z_blog_2_filetest.gno","body":"// PKGPATH: gno.land/r/test\npackage test\n\nimport (\n\t\"gno.land/p/lou/blog\"\n)\n\nfunc main(cur realm) {\n\tuser := address(\"g14x2crt492f84shxa9s7e5ulk7lnf2p3euz7r9n\")\n\ttestBlog, err := blog.NewBlog(0, cur, \"test blog\", user)\n\tif err != nil {\n\t\tprintln(err.Error())\n\t}\n\tprintln(testBlog.Render(\"\"))\n}\n\n// Output:\n// # test blog\n//\n// [⊞ grid](/r/test:?mode=grid) | [≔ list](/r/test:?mode=list) | [⧖ relative](/r/test:?time=short) | [↕ alphabetical \\(A\\-Z\\)](/r/test:?order=asc\u0026sort=alpha) | [↕ recent](/r/test:?order=asc\u0026sort=recent) | [↕ last updated](/r/test:?order=asc\u0026sort=update) | [past year](/r/test:?end=2009-02-13\u0026start=2008-02-13), [this year](/r/test:?end=2009-02-13\u0026start=2009-01-01), [last 30 days](/r/test:?end=2009-02-13\u0026start=2009-01-14) | [⟳ reset](/r/test)\n//\n// No posts found.\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"mDM60dhyr/HdVf7kn8sDCAKsjej1yGAVABwE1B6Eqn55om9qsX/UbIOPEt4Exy4fLSOPB87HlVhvpYG2d1z4fg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"charts","path":"gno.land/p/matijamarjanovic/charts","files":[{"name":"bar_chart.gno","body":"package charts\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// GenerateBarChart creates an ASCII bar chart in markdown format\n// values: slice of float values to chart\n// labels: slice of labels for each bar\n// maxWidth: maximum width of bars in characters\n// title: chart title\n// Returns a markdown string representing the chart\nfunc GenerateBarChart(values []float64, labels []string, maxWidth int, title string) string {\n\tif len(values) == 0 || len(labels) == 0 || len(values) != len(labels) {\n\t\treturn \"invalid data for display\"\n\t}\n\n\tif maxWidth \u003c= 0 {\n\t\treturn \"maxWidth must be greater than 0\"\n\t}\n\n\tmaxVal := findMaxValue(values)\n\n\tmaxLabelLength := 0\n\tfor _, label := range labels {\n\t\tif len(label) \u003e maxLabelLength {\n\t\t\tmaxLabelLength = len(label)\n\t\t}\n\t}\n\n\tscale := float64(maxWidth) / maxVal\n\n\toutput := formatChartHeader(title)\n\toutput += \"\\n```\\n\"\n\n\tfor i, value := range values {\n\t\tpadding := strings.Repeat(\" \", maxLabelLength-len(labels[i]))\n\t\toutput += labels[i] + padding + \"    \"\n\n\t\tbarLength := int(value * scale)\n\t\toutput += strings.Repeat(\"█\", barLength)\n\n\t\toutput += \" \" + ufmt.Sprintf(\"%.2f\", value)\n\t\toutput += \"\\n\"\n\t}\n\n\toutput += \"```\\n\"\n\treturn output\n}\n"},{"name":"charts.gno","body":"// Charts is a package that provides functions to generate ASCII charts in markdown format.\n// It includes functions for line charts, bar charts, and column charts.\n//\n// Both line and column charts are implemented in a way that offers normalization of data;\n// In short terms precision of the chart can be adjusted by changing the number of points or columns.\n// Line chart offers a way to adjust the spacing between points as well, which allows for more control over the chart.\n//\n// e.g. Let's say there is a data set of a value of dollar per day in the year of 2025 in a slice of floats.\n// If those values are passed to the `GenerateLineChart` in this manner:\n// `GenerateLineChart(values, 12, 5, \"Dollar per day\", \"Days\", \"Dollars\")`\n// The chart will be normalized to 12 points, with 5 characters of spacing between each point, but more importantly,\n// these points will represent a rough monthly average of the dollar value (months are not of equal length, so this is not an exact science).\n\npackage charts\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nconst height = 15\n\n// normalizeData reduces the number of data points to maxColumns by averaging\n// Returns nil if values is empty or maxColumns is zero or negative\nfunc normalizeData(values []float64, maxColumns int) []float64 {\n\tif len(values) == 0 || maxColumns \u003c= 0 {\n\t\treturn nil\n\t}\n\n\tif maxColumns \u003e= len(values) {\n\t\tresult := make([]float64, len(values))\n\t\tcopy(result, values)\n\t\treturn result\n\t}\n\n\tresult := make([]float64, maxColumns)\n\tgroupSize := float64(len(values)) / float64(maxColumns)\n\n\tfor i := 0; i \u003c maxColumns; i++ {\n\t\tstart := int(float64(i) * groupSize)\n\t\tend := int(float64(i+1) * groupSize)\n\t\tif end \u003e len(values) {\n\t\t\tend = len(values)\n\t\t}\n\n\t\tsum := 0.0\n\t\tcount := end - start\n\t\tfor j := start; j \u003c end; j++ {\n\t\t\tsum += values[j]\n\t\t}\n\t\tif count \u003e 0 {\n\t\t\tresult[i] = sum / float64(count)\n\t\t}\n\t}\n\n\treturn result\n}\n\n// findMaxValue returns the maximum value in a slice of float64\nfunc findMaxValue(values []float64) float64 {\n\tif len(values) == 0 {\n\t\treturn 0\n\t}\n\n\tmaxVal := values[0]\n\tfor _, v := range values[1:] {\n\t\tif v \u003e maxVal {\n\t\t\tmaxVal = v\n\t\t}\n\t}\n\treturn maxVal\n}\n\n// calculateMaxValueStringLength returns the maximum string length when formatting values\nfunc calculateMaxValueStringLength(values []float64) int {\n\tmaxValStrLen := 0\n\tfor _, v := range values {\n\t\tvalStr := ufmt.Sprintf(\"%.2f\", v)\n\t\tif len(valStr) \u003e maxValStrLen {\n\t\t\tmaxValStrLen = len(valStr)\n\t\t}\n\t}\n\treturn maxValStrLen\n}\n\n// formatChartHeader formats the chart header with title and markdown code block\nfunc formatChartHeader(title string) string {\n\toutput := \"\"\n\tif title != \"\" {\n\t\toutput += \"## \" + title + \"\\n\"\n\t}\n\treturn output\n}\n\n// formatYAxisTitle adds the y-axis title to the chart\nfunc formatYAxisTitle(yAxisTitle string) string {\n\tif yAxisTitle != \"\" {\n\t\treturn yAxisTitle + \"\\n\\n\"\n\t}\n\treturn \"\"\n}\n\n// formatXAxisTitle adds the x-axis title to the chart\nfunc formatXAxisTitle(xAxisTitle string, width int) string {\n\tif xAxisTitle != \"\" {\n\t\tpadding := strings.Repeat(\" \", width/2-len(xAxisTitle)/2)\n\t\treturn \"       \" + padding + xAxisTitle + \"\\n\"\n\t}\n\treturn \"\"\n}\n\n// formatYAxisLabel formats a y-axis label with proper padding\nfunc formatYAxisLabel(value float64, scale float64, yAxisWidth int) string {\n\tyValue := ufmt.Sprintf(\"%.2f\", value/scale)\n\tpadding := strings.Repeat(\" \", yAxisWidth-len(yValue))\n\treturn ufmt.Sprintf(\"%s%s | \", padding, yValue)\n}\n\n// formatYAxisSpace returns spacing for y-axis when no label is needed\nfunc formatYAxisSpace(yAxisWidth int) string {\n\treturn strings.Repeat(\" \", yAxisWidth) + \" | \"\n}\n"},{"name":"charts_test.gno","body":"package charts\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nfunc TestGenerateLineChart_Basic(t *testing.T) {\n\tvalues := []float64{1, 2, 3, 2, 1}\n\tresult := GenerateLineChart(values, 5, 6, \"Test Line\", \"X\", \"Y\")\n\tuassert.True(t, strings.Contains(result, \"Test Line\"))\n\tuassert.True(t, strings.Contains(result, \"X\"))\n\tuassert.True(t, strings.Contains(result, \"Y\"))\n\tuassert.True(t, strings.Contains(result, \"*\"))\n}\n\nfunc TestGenerateLineChart_EmptyData(t *testing.T) {\n\tresult := GenerateLineChart([]float64{}, 5, 6, \"Title\", \"X\", \"Y\")\n\tuassert.Equal(t, \"no data to display\", result)\n}\n\nfunc TestGenerateLineChart_BadSpacing(t *testing.T) {\n\tvalues := []float64{1, 2, 3}\n\tresult := GenerateLineChart(values, 5, 2, \"Title\", \"X\", \"Y\")\n\tuassert.Equal(t, \"pointSpacing must be at least 6 to fit value labels\", result)\n}\n\nfunc TestGenerateLineChart_Normalization(t *testing.T) {\n\tvalues := []float64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}\n\tresult := GenerateLineChart(values, 5, 25, \"Norm\", \"X\", \"Y\")\n\tprintln(result)\n\tuassert.True(t, strings.Contains(result, \"Norm\"))\n\tuassert.True(t, len(result) \u003e 0)\n}\n\nfunc TestGenerateBarChart_Basic(t *testing.T) {\n\tvalues := []float64{3, 5, 2}\n\tlabels := []string{\"A\", \"B\", \"C\"}\n\tresult := GenerateBarChart(values, labels, 10, \"Bar Test\")\n\tuassert.True(t, strings.Contains(result, \"Bar Test\"))\n\tuassert.True(t, strings.Contains(result, \"A\"))\n\tuassert.True(t, strings.Contains(result, \"B\"))\n\tuassert.True(t, strings.Contains(result, \"C\"))\n\tuassert.True(t, strings.Contains(result, \"█\"))\n}\n\nfunc TestGenerateBarChart_InvalidData(t *testing.T) {\n\tresult := GenerateBarChart([]float64{}, []string{}, 10, \"Title\")\n\tuassert.Equal(t, \"invalid data for display\", result)\n\tresult2 := GenerateBarChart([]float64{1, 2}, []string{\"A\"}, 10, \"Title\")\n\tuassert.Equal(t, \"invalid data for display\", result2)\n}\n\nfunc TestGenerateBarChart_Widths(t *testing.T) {\n\tvalues := []float64{1, 2}\n\tlabels := []string{\"A\", \"B\"}\n\tresult := GenerateBarChart(values, labels, 5, \"Widths\")\n\tuassert.True(t, strings.Contains(result, \"Widths\"))\n}\n\nfunc TestGenerateColumnChart_Basic(t *testing.T) {\n\tvalues := []float64{2, 4, 3}\n\tresult := GenerateColumnChart(values, 3, \"Col Test\", \"X\", \"Y\")\n\tuassert.True(t, strings.Contains(result, \"Col Test\"))\n\tuassert.True(t, strings.Contains(result, \"X\"))\n\tuassert.True(t, strings.Contains(result, \"Y\"))\n\tuassert.True(t, strings.Contains(result, \"|||||\"))\n}\n\nfunc TestGenerateColumnChart_EmptyData(t *testing.T) {\n\tresult := GenerateColumnChart([]float64{}, 3, \"Title\", \"X\", \"Y\")\n\tuassert.Equal(t, \"no data to display\", result)\n}\n\nfunc TestGenerateColumnChart_Normalization(t *testing.T) {\n\tvalues := []float64{1, 2, 3, 4, 5, 6}\n\tresult := GenerateColumnChart(values, 3, \"Norm\", \"X\", \"Y\")\n\tuassert.True(t, strings.Contains(result, \"Norm\"))\n}\n"},{"name":"column_chart.gno","body":"package charts\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// GenerateColumnChart creates an ASCII column chart in markdown format\n// values: slice of float values to chart\n// maxColumns: maximum number of columns to display (will normalize if exceeded)\n// title: chart title\n// xAxisTitle: title for the x-axis\n// yAxisTitle: title for the y-axis\n// Returns a markdown string representing the chart\nfunc GenerateColumnChart(values []float64, maxColumns int, title, xAxisTitle, yAxisTitle string) string {\n\tif len(values) == 0 {\n\t\treturn \"no data to display\"\n\t}\n\n\tif maxColumns \u003c= 0 {\n\t\treturn \"maxColumns must be greater than 0\"\n\t}\n\n\tmaxVal := findMaxValue(values)\n\n\tdisplayValues := values\n\tif len(values) \u003e maxColumns {\n\t\tdisplayValues = normalizeData(values, maxColumns)\n\t}\n\n\tscale := height / maxVal\n\n\toutput := formatChartHeader(title)\n\toutput += \"\\n```\\n\"\n\toutput += formatYAxisTitle(yAxisTitle)\n\n\tmaxYValue := ufmt.Sprintf(\"%.2f\", maxVal)\n\tyAxisWidth := len(maxYValue)\n\n\tmaxValStrLen := calculateMaxValueStringLength(displayValues)\n\tif maxValStrLen \u003c 2 {\n\t\tmaxValStrLen = 2\n\t}\n\tcolWidth := maxValStrLen + 2\n\n\tfor row := height; row \u003e 0; row-- {\n\t\tif row%3 == 0 {\n\t\t\toutput += formatYAxisLabel(float64(row), scale, yAxisWidth)\n\t\t} else {\n\t\t\toutput += formatYAxisSpace(yAxisWidth)\n\t\t}\n\n\t\tfor _, val := range displayValues {\n\t\t\tscaledHeight := int(val * scale)\n\t\t\tif scaledHeight \u003e= row {\n\t\t\t\tbarWidth := colWidth - 1\n\t\t\t\toutput += strings.Repeat(\"|\", barWidth) + \" \"\n\t\t\t} else {\n\t\t\t\toutput += strings.Repeat(\" \", colWidth)\n\t\t\t}\n\t\t}\n\t\toutput += \"\\n\"\n\t}\n\n\toutput += strings.Repeat(\" \", yAxisWidth) + \" +\"\n\toutput += strings.Repeat(strings.Repeat(\"-\", colWidth), len(displayValues))\n\toutput += \"\\n\"\n\n\toutput += formatValueLabelsColumnChart(displayValues, colWidth, yAxisWidth)\n\n\tif xAxisTitle != \"\" {\n\t\ttotalWidth := len(displayValues) * colWidth\n\t\toutput += formatXAxisTitle(xAxisTitle, totalWidth)\n\t}\n\n\toutput += \"```\\n\"\n\treturn output\n}\n\n// formatValueLabelsColumnChart formats the value labels for the x-axis for column charts\nfunc formatValueLabelsColumnChart(values []float64, colWidth int, yAxisWidth int) string {\n\toutput := strings.Repeat(\" \", yAxisWidth+2)\n\n\tfor i := 0; i \u003c len(values); i++ {\n\t\tx := i * colWidth\n\t\tvalStr := ufmt.Sprintf(\"%.2f\", values[i])\n\t\tlabelPos := x + colWidth/2 - len(valStr)/2\n\t\tif labelPos \u003c 0 {\n\t\t\tlabelPos = 0\n\t\t}\n\n\t\tif i == 0 {\n\t\t\toutput += strings.Repeat(\" \", labelPos)\n\t\t} else {\n\t\t\tprevX := (i - 1) * colWidth\n\t\t\tprevValStr := ufmt.Sprintf(\"%.2f\", values[i-1])\n\t\t\tprevLabelEnd := prevX + colWidth/2 - len(prevValStr)/2 + len(prevValStr)\n\t\t\toutput += strings.Repeat(\" \", labelPos-prevLabelEnd)\n\t\t}\n\t\toutput += valStr\n\t}\n\n\treturn output + \"\\n\"\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/matijamarjanovic/charts\"\ngno = \"0.9\"\n"},{"name":"line_chart.gno","body":"package charts\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// GenerateLineChart creates an ASCII line chart in markdown format\n// values: slice of float values to chart\n// maxPoints: maximum number of points to display (will normalize if exceeded)\n// pointSpacing: horizontal spacing between points (in characters)\n// title: chart title\n// xAxisTitle: title for the x-axis\n// yAxisTitle: title for the y-axis\n// Returns a markdown string representing the chart\nfunc GenerateLineChart(values []float64, maxPoints int, pointSpacing int, title, xAxisTitle, yAxisTitle string) string {\n\tif len(values) == 0 {\n\t\treturn \"no data to display\"\n\t}\n\n\tif maxPoints \u003c= 0 {\n\t\treturn \"maxPoints must be greater than 0\"\n\t}\n\n\tmaxVal := findMaxValue(values)\n\tmaxValStrLen := calculateMaxValueStringLength(values)\n\n\tminSpacing := maxValStrLen + 2\n\tif pointSpacing \u003c minSpacing {\n\t\treturn ufmt.Sprintf(\"pointSpacing must be at least %d to fit value labels\", minSpacing)\n\t}\n\tif pointSpacing \u003e 25 {\n\t\treturn ufmt.Sprintf(\"pointSpacing must be between %d and 25\", minSpacing)\n\t}\n\n\tdisplayValues := values\n\tif len(values) \u003e maxPoints {\n\t\tdisplayValues = normalizeData(values, maxPoints)\n\t}\n\n\tscale := float64(height) / maxVal\n\n\tgridWidth := (len(displayValues)-1)*pointSpacing + len(displayValues)\n\tgrid := make([][]rune, height+1)\n\tfor i := range grid {\n\t\tgrid[i] = make([]rune, gridWidth)\n\t\tfor j := range grid[i] {\n\t\t\tgrid[i][j] = ' '\n\t\t}\n\t}\n\n\tspacing := pointSpacing + 1\n\tfor i := 0; i \u003c len(displayValues); i++ {\n\t\tx := i * spacing\n\t\ty := int(displayValues[i] * scale)\n\t\tif y \u003e height {\n\t\t\ty = height\n\t\t}\n\t\tgrid[height-y][x] = '*'\n\n\t\tif i \u003c len(displayValues)-1 {\n\t\t\tnextX := (i + 1) * spacing\n\t\t\tnextY := int(displayValues[i+1] * scale)\n\t\t\tif nextY \u003e height {\n\t\t\t\tnextY = height\n\t\t\t}\n\t\t\tdx := nextX - x\n\t\t\tdy := nextY - y\n\t\t\tprevInterpY := y\n\t\t\tfor step := 1; step \u003c dx; step++ {\n\t\t\t\tinterpY := y + (dy*step)/dx\n\t\t\t\tstartY := prevInterpY\n\t\t\t\tendY := interpY\n\t\t\t\tif startY \u003e endY {\n\t\t\t\t\tstartY, endY = endY, startY\n\t\t\t\t}\n\t\t\t\tfor fillY := startY; fillY \u003c= endY; fillY++ {\n\t\t\t\t\tgrid[height-fillY][x+step] = '*'\n\t\t\t\t}\n\t\t\t\tprevInterpY = interpY\n\t\t\t}\n\t\t}\n\t}\n\n\toutput := \"\\n\"\n\toutput += formatChartHeader(title)\n\toutput += \"\\n```\\n\"\n\toutput += formatYAxisTitle(yAxisTitle)\n\n\tmaxYValue := ufmt.Sprintf(\"%.2f\", maxVal)\n\tyAxisWidth := len(maxYValue)\n\n\tfor row := 0; row \u003c= height; row++ {\n\t\tgridRow := grid[row]\n\t\tgridY := height - row\n\t\tif gridY%3 == 0 {\n\t\t\toutput += formatYAxisLabel(float64(gridY), scale, yAxisWidth)\n\t\t} else {\n\t\t\toutput += formatYAxisSpace(yAxisWidth)\n\t\t}\n\t\toutput += string(gridRow)\n\t\toutput += \"\\n\"\n\t}\n\n\toutput += strings.Repeat(\" \", yAxisWidth) + \" +\"\n\toutput += strings.Repeat(\"-\", gridWidth+3)\n\toutput += \"\\n\"\n\n\toutput += formatValueLabelsLineChart(displayValues, pointSpacing, yAxisWidth)\n\n\tif xAxisTitle != \"\" {\n\t\toutput += formatXAxisTitle(xAxisTitle, gridWidth)\n\t}\n\n\toutput += \"```\"\n\treturn output\n}\n\n// formatValueLabelsLineChart formats the value labels for the x-axis for line charts\nfunc formatValueLabelsLineChart(displayValues []float64, pointSpacing int, yAxisWidth int) string {\n\toutput := strings.Repeat(\" \", yAxisWidth+2)\n\tspacing := pointSpacing + 1\n\tfor i := 0; i \u003c len(displayValues); i++ {\n\t\tx := i * spacing\n\t\tvalStr := ufmt.Sprintf(\"%.2f\", displayValues[i])\n\t\tlabelPos := x - len(valStr)/2\n\t\tif labelPos \u003c 0 {\n\t\t\tlabelPos = 0\n\t\t}\n\t\tif i == 0 {\n\t\t\toutput += strings.Repeat(\" \", labelPos)\n\t\t} else {\n\t\t\tprevX := (i - 1) * spacing\n\t\t\tprevValStr := ufmt.Sprintf(\"%.2f\", displayValues[i-1])\n\t\t\tprevLabelEnd := prevX - len(prevValStr)/2 + len(prevValStr)\n\t\t\toutput += strings.Repeat(\" \", labelPos-prevLabelEnd)\n\t\t}\n\t\toutput += valStr\n\t}\n\treturn output + \"\\n\"\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"j10HnN7dW5xzExlBU/7KNjGHcycrJ5rucWem/Vjp68Fjh1VjoWwB0fWYWbP0bqdkz9xlG8mOXIacG1UVz3BQIw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"zobrist","path":"gno.land/p/morgan/chess/zobrist","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/morgan/chess/zobrist\"\ngno = \"0.9\"\n"},{"name":"zobrist.gno","body":"// Package zobrist contains an implementation of the Zobrist Hashing algorithm.\n// https://www.chessprogramming.org/Zobrist_Hashing\n//\n// The hash is performed using a series of random numbers generated through\n// rands.go, in this source directory.\npackage zobrist\n\n// Board is a representation of a chess board.\n// Details on how to transform a chess algebraic position into an index\n// can be found at [Square].\ntype Board [64]Piece\n\n// Piece represents a piece on the board.\ntype Piece byte\n\n// Possible values of Piece. Within the context of Board, Piece is assumed to\n// be white, unless p\u0026PieceBlack != 0. Note PieceBlack is not a valid piece; it\n// must be bitwise OR'd to a non-empty piece.\nconst (\n\tPieceEmpty Piece = iota\n\n\tPiecePawn\n\tPieceRook\n\tPieceKnight\n\tPieceBishop\n\tPieceQueen\n\tPieceKing\n\n\tPieceBlack Piece = 8 // bit-flag\n)\n\n// InitialPosition is the zobrist hash (as implemented by this package)\n// for the initial chess position.\nconst InitialPosition uint64 = 1957848132405881468\n\n// Hash generates a Zobrist hash with the given parameter.\n//\n// castling can assume a value between [0,16),\n// epFile should be set to a value between [0,8) -- if there is no en passant\n// file, pass 255 (as a convention).\nfunc Hash(b Board, isBlack bool, castling, epFile byte) uint64 {\n\tvar hash uint64\n\tif isBlack {\n\t\thash ^= hashBlack\n\t}\n\tfor sq, p := range b {\n\t\tif p == PieceEmpty {\n\t\t\tcontinue\n\t\t}\n\t\tif p\u0026PieceBlack == 0 {\n\t\t\t// values 0-5\n\t\t\tp--\n\t\t} else {\n\t\t\t// values 6-11\n\t\t\tp -= PieceBlack | PiecePawn\n\t\t}\n\t\thash ^= hashPieces[sq*12+int(p)]\n\t}\n\thash ^= hashCastling[castling]\n\tif epFile \u003c 8 {\n\t\thash ^= hashEpFile[epFile]\n\t}\n\treturn hash\n}\n\nvar (\n\thashBlack    uint64 = 0x331cf7440c179431\n\thashCastling        = [...]uint64{\n\t\t0xda401e23397e78ca, 0x41dd6246f3cdd0fc, 0xe7a57999953a1784, 0x1e1fdc4db40f6c93,\n\t\t0xff76ddf8b88d7c47, 0xb25c00230f48e677, 0xfdffd279de1e888a, 0x61e89216839cbe34,\n\t\t0xc082fd59e3b8e542, 0x041d42f60a9a6719, 0xa162c6fbfe15b8c0, 0x5105c749ea4f9def,\n\t\t0x68e60d8b032b2f1c, 0xc4c95eb6f2a5600d, 0xd005b4bf6d3d18e1, 0xd18bb1e4fd30b333,\n\t}\n\thashEpFile = [...]uint64{\n\t\t0x986e8dc916fd650b, 0x97dfccc4b5827a70, 0x5888298d167f8b9b, 0x937b5022b30a532b,\n\t\t0x40228426ba8bb614, 0xb7c1c137ace2674e, 0x0c3ef7d3fac8d0be, 0x9cdf9d7b763f2e6c,\n\t}\n\t// 6 pieces * 2 colours * 64 squares = 768 random uint64's.\n\thashPieces = [...]uint64{\n\t\t0x6de9d290a487babc, 0x81095e35de2bc008, 0xfdf0e0d93a5f4c5c, 0xc63827a4cea42d4b,\n\t\t0x103c199f0290bdae, 0x61bf576e61c2d862, 0x178b9ec5427fb45c, 0xd34361ffe3a6abb7,\n\t\t0x4301de67181ccc30, 0xb70e72de5895cf5e, 0xedf154e7e16965fd, 0x543efdb0aeef09d2,\n\t\t0xb2091f463dce5477, 0x0db5bef892262850, 0x69c6bdc6ddb7c1cf, 0x3955a54fd5a60f96,\n\t\t0x74f5e115e8293d45, 0x9e9fb05af1c96e09, 0xab6685da47c28488, 0x80b82ee1a494665d,\n\t\t0x412701df2f89d3a1, 0x8ac06869db920dd2, 0xea7b99e10ba51307, 0xc13365ed0d123d8a,\n\t\t0x42cebc0bcaba8cd7, 0x314a7e49e9b51bcf, 0x5abb9198d4ad0f9d, 0x0a6f4456d77eb317,\n\t\t0x04d2ba6598ffb494, 0x89cdd7e425256b29, 0xfb5853f8b71a1496, 0x2111a3ba29f78227,\n\t\t0x1ac8ec9f94e60981, 0x9c1ea2e3e24da61f, 0x62cf491e74065750, 0x19578de8083259ac,\n\t\t0xb4a576703ccf3d02, 0xb38e458dd8196e9e, 0x42f5d714cd5947e5, 0x7b84ea130bcb7f7f,\n\t\t0xb05f2fcc47356344, 0x87c9c45a24657fe4, 0x815aff9977943588, 0x484c65c342290c44,\n\t\t0x2d326480e45a4f1a, 0x6c06c7db312dc1d2, 0xb6f01ac7c66f5a45, 0xf4c88ad4ff1176db,\n\t\t0x5f921eb462e69949, 0x990de640b9b274e1, 0x9c961b70243bf6ab, 0x51f7bfa5e4bc9575,\n\t\t0x2e89aed81f40939a, 0x78ce647c4760c4b4, 0xf12f7ebcc4f86666, 0x44bce0ebee1fc047,\n\t\t0x19613b81961e8c69, 0x1481b5c8affcba3a, 0x94d6321ba8417f26, 0xa43bf26acbb02a17,\n\t\t0xe0a66cca2572428a, 0x25fd4f9950444f18, 0x33728d2c1bdf953f, 0x8c35d0d7a31a7723,\n\t\t0x93a6641e67e92047, 0x0d27f43da10061d2, 0x560384de9e6b4b4f, 0xec1f847836357e1b,\n\t\t0xe9cd3eaf397443d3, 0x3cad122344e04c1b, 0x80626be75cccc58f, 0xaf6fcf59f731ff23,\n\t\t0x60f92853b13f2480, 0x865fb5e62163b781, 0x6019d5d1170770e9, 0x5e9528c7589f10c4,\n\t\t0x792d01b3672169c2, 0x8f812368414a03f7, 0xf8d7bd34601ad664, 0x13b52975cf7b3251,\n\t\t0xcb4fdf3b50b64338, 0x0536184766d7b464, 0x72ded1ba157bb20f, 0xf8ae92fde84a5c19,\n\t\t0xf2379c8815c2e71c, 0x86a6fda12a95af71, 0x3e18f5f431fd566d, 0x9e0b75a4eacc0b96,\n\t\t0x83ff97e0917f9db3, 0x8bd3889e98157154, 0xbc56ee3a9bcf3b4b, 0x8386739420fd2727,\n\t\t0x160dbcab1f83a9dc, 0x6e4e36335993cf57, 0xb53933016a624b7c, 0x11e7b44599a6a980,\n\t\t0x6f6c927432d695ae, 0x672ead6a7a67c3ce, 0xa313c9ce239a1a1e, 0x1d9b559b13be222a,\n\t\t0x73dfff734fbbee8b, 0x87abe0f6a14be15c, 0x42388fe5e4f904c2, 0x7705ef7492f0dc2d,\n\t\t0xc0e281c867e902b1, 0xbf29803779c71b67, 0xf468583ebde5789a, 0xe28ba371a6cac398,\n\t\t0x917f56b7b1c27398, 0xbf29669535aaa4e0, 0xe744219af67a0621, 0xd5af8e55ff58d5d5,\n\t\t0x72e2cc0e495c7094, 0x72c857412018b79a, 0x1db6efd2b47cb031, 0x9739fb93667f2bbe,\n\t\t0x7151b89b41dcb1bb, 0x24cf2e514e7f33e0, 0xfbc38081840febe1, 0x6eddef1967d0d653,\n\t\t0xf1f91203400ae92c, 0x26b1777c7babae3d, 0x3baf2f2abd8910c8, 0x5b47759ffbd37842,\n\t\t0xffe868968ec9270d, 0x89a7ac0d255b357f, 0xd9b56079f915c43d, 0x422ba52ec00bb2f9,\n\t\t0x18f66fad5c3b238c, 0x2a33af6d60146d64, 0xefdc9649adf7e956, 0x005f5181aa2d41db,\n\t\t0x1dfff9e7449938d3, 0xeabeac20a34e6545, 0x1e786123884674fd, 0x6a719ae518a98556,\n\t\t0x6020b1ea2fe10223, 0x600a04f0521dcb55, 0xa6c947a541137295, 0x80df3344d3cdfe9f,\n\t\t0xc2fdcc9cf8da02bd, 0xa3d52d7633c4b5b8, 0x7596ea450a6c4d79, 0x298299d89d59d5d1,\n\t\t0x0020b9b7c790a672, 0x7867f067a107dbbc, 0x04ae6e4821bbd355, 0x075efcf522abba1d,\n\t\t0x831e0c547977c42e, 0x252a2ccbb9cae9b9, 0xb64ba9e683a44b52, 0x1c271189f6f2fcea,\n\t\t0xde6252635b8a6c1e, 0x4294ae1f329ddd8a, 0xda93eb7bf7d9706f, 0x4382925fe1404821,\n\t\t0xf791164a1b549969, 0xd60e8934c81cb154, 0x81019c98d5254d75, 0x024f3dac35b8e853,\n\t\t0x2d5c20f2e708a079, 0x8efb9c33bf895e8a, 0x378d97d4b623cacc, 0xcb62d165606b574c,\n\t\t0x8ad6ec30e8367851, 0x9fb21d6482f9a6b8, 0xa7725c027baed0e7, 0xb3e15af756bcee91,\n\t\t0x407b67ef1c9c71bc, 0x6f981f475f94d19c, 0x86c26906e61dec18, 0xa263045310ee8935,\n\t\t0x272ac35622e61a11, 0xf6fc4c7c91691cbf, 0x910393ad76ced568, 0x10d3bcbcf6454bf8,\n\t\t0x3c05a3b1703469a0, 0x78029abf41b1bd40, 0x34b4e74ce8b474e9, 0xe3b47e81c39fefa6,\n\t\t0x91d83224fe32db32, 0xe6c96cb196fcc83e, 0xf7b94f6f7231d874, 0x9a834ff1a03f5b38,\n\t\t0x0e98878ae294edc1, 0xff2694b96b00b0dd, 0xae3bdce9265aac7c, 0xc97887009d14f08d,\n\t\t0xb7f44cdd578ec417, 0xe602ed604ef3c824, 0xe352a0ac128bcc3a, 0x79a0b291d4cd9c14,\n\t\t0x5ee4a9e2c195ff16, 0x26fa8a6d4d66e24d, 0xaeafa129bd9c62c7, 0x3da01af74b818094,\n\t\t0x38c499fb2823505b, 0x757a32af70db20da, 0xcd2b0fec6311d81c, 0xd10ed19e0da6460a,\n\t\t0x0f7b060a7707dc32, 0x7042d0c262a6a06b, 0xd0d437e753d560ed, 0xaefcc4e07a948586,\n\t\t0x9516944f8863368c, 0x60cfa52a9cfaf4d5, 0x2e22817497bd2772, 0xb7dfc139fbd79268,\n\t\t0x15ea3777b785a4df, 0xf44cb9592c31d48b, 0x5893e556af63aa5e, 0xb63609fb150e1e1f,\n\t\t0x44920c624c2c144e, 0xd9e13f6f8d504303, 0x09681482e9af2e2c, 0x072c18b4d02a88d0,\n\t\t0xd3f8886bd2e3c58c, 0x081abec6305b42c8, 0xe68c68756cba2638, 0x3f1843fc7969e258,\n\t\t0xb25d2db11a7b6757, 0x0a066242ead527d6, 0xf2a47d93d403bd29, 0xbbf5a65e74ca4d34,\n\t\t0x7ea55dabf5244210, 0x79d3ed1639e00808, 0xb2dbb1f606335295, 0xdf0b2c1256b1b3af,\n\t\t0x2bc9bf68fbf2c035, 0x7aabc308077a3400, 0x945a92d9b807784d, 0xae3a32ff2dd3ad6a,\n\t\t0x63a59bf8137d3c2b, 0xa919df7ac39f4ce3, 0x00c3688b75e3754b, 0xc589f782da55edcb,\n\t\t0xe97758166f728546, 0x3d9ebff5bea838f0, 0xe3f141938b9281dd, 0xc3c3a7a75aafbbac,\n\t\t0x1d6f368464eb8c54, 0x596ce122feada666, 0xf768ffa3509627c0, 0x5ea90c12875fe357,\n\t\t0x11307b7514087de5, 0x1eb75d679ca6ebe1, 0xfa2a85179ecccdc0, 0x73cafcc11d1a19b6,\n\t\t0x7942c51eb73934db, 0xd31925b7bebd3766, 0x4f1975c179b4b082, 0x406ec764a5b0059b,\n\t\t0x7df4dcbd21411be8, 0xaaa5c8df72d0e4f3, 0xfefa53616f4effb2, 0x82dd4177438fcc5b,\n\t\t0x8cc62697b107de0e, 0x140066b1a9e48d6a, 0xa1bc5118a5048eb3, 0x8924bdfe570facc7,\n\t\t0x55cf653d02d670b0, 0xa9130a65e2f0fa04, 0x0f6a1eecdb30d8a3, 0x255cb52477aba452,\n\t\t0xade06ddc6f835cd4, 0xd329541bdbb6b279, 0xdd921ee95ea595ea, 0xaedbba578197ffab,\n\t\t0x85b9b59808076c56, 0x2439ddd88b842c8a, 0xb11190014736ff58, 0x3223b6457cd90b6f,\n\t\t0x5b1c51474d5b443d, 0xca8b496fbed47b11, 0xe371cd43623b4e5f, 0x1e5a62f4040e9242,\n\t\t0xe5baa812538471c2, 0x3914ded0b9c68d7b, 0x211ceb64d9099bbe, 0x7fc33f702220dc78,\n\t\t0x3b5e9c5840a2b8b3, 0xa0e21e2c37d4c30a, 0x169fa2324b2addc3, 0x9b457d6ae103fa48,\n\t\t0x91f1f72f5e258912, 0xeb701bd0b95314c7, 0x7965c7b11ffbb5d7, 0x898b14b385433c77,\n\t\t0xe1ae26dbc9ec4653, 0xac37595684b326d3, 0x84bf7f441b85ef51, 0x0486ee8e1cf0efb2,\n\t\t0xa9b8acdf69f73960, 0xa34d1a08c8076a32, 0x7f9246295970518c, 0x6f656dc6fb980791,\n\t\t0xcfe89b893838d8ab, 0x51e9a6e88c6a59dd, 0x0eacf50f85a334bb, 0x82d23ccf2a49be8a,\n\t\t0xa7d385b0cf2a7d4e, 0xa3a849d79ebac654, 0xbc000cf465923371, 0xa1a3c61b7d9dde7d,\n\t\t0x08878bf2cf37eace, 0xf0ed44ef8046656b, 0xdb7c02c81afd1072, 0x3c7daea881c1c1af,\n\t\t0xba43d6f4df817382, 0x6d828358a59f5f2c, 0x75eebe28937070ad, 0x792df3f928010752,\n\t\t0x0c3be6141c90c5d9, 0x0efec51f83e272a8, 0x9a6f416b0a6df723, 0x04b2e247d3914036,\n\t\t0xcbbaf36eded44848, 0xda39769f8831bdcb, 0x47068c11232c8118, 0xf120b77c9b3e6210,\n\t\t0x52e11d727c031745, 0x2c78f899a17fb88f, 0x95ddc231c0cdd8c2, 0x1b32bca26532b25b,\n\t\t0x31af0dbbe225beec, 0x594e64fbe32f365f, 0xd4218aabd87b5569, 0xbb36aff9719c04cc,\n\t\t0xf44e23c7aa02657a, 0x0f980e87e514d690, 0x02db35ab5497bb72, 0x5adcdefaa2eae880,\n\t\t0x59b3ce68c22fac3d, 0x7dd52a14da3abc88, 0xcdf09a31d10c28cf, 0xc7ac17887fb668da,\n\t\t0xac55650afca95f7d, 0x81374af3db3e7b4b, 0x6c68c53c855bafc7, 0x1406727187441363,\n\t\t0x5ba83433a8bf732d, 0x4a4a968409ab0137, 0x304dd4ec27b37a5e, 0x5c00efb6a1a4b9ed,\n\t\t0xc06eecaa0c82859f, 0x9aa9465883ac52c3, 0x993f3d988706142a, 0x6d7395a0b1e83c09,\n\t\t0xaf8a2b5634ffe143, 0x9769ee0de8694d5e, 0xd439980424a5db0f, 0xb04f5a21742ae9e2,\n\t\t0xbf9059f0b1c4171c, 0x4ff1a289c070f22d, 0x6dd87cd9642d3ca6, 0x9febd09c132f3c88,\n\t\t0x332f6b2973b8d987, 0x14465e925981e26b, 0x5b4074fc700e8910, 0x6b5eb71612931147,\n\t\t0xa796b842757c937c, 0x4becbeca2c5a8d3b, 0xc48f1a998822fbd2, 0xd223e3864f6fa221,\n\t\t0x344cc8ffede3909e, 0x980724e16f14e4f4, 0xb836750d3f50f20b, 0x84c87a98513035a5,\n\t\t0x1dacdc18f768c936, 0xa99bb4ad168e2e38, 0x6e0708645f549d34, 0x9b8d705a0c81bae2,\n\t\t0x54a4ff6b225d98fb, 0x362cfdb47340082c, 0x2c07d8af734835bf, 0x5e04437c1e32b391,\n\t\t0x1be670cba5f4e187, 0xa71852f798fb9887, 0xa06f2d65637c765c, 0x8bbfe9dd54e33f7c,\n\t\t0xf3dd657e0650ecf7, 0x7e42c999e6430f76, 0x6073b783560802b0, 0xb69a6e688e4d8317,\n\t\t0xa0bfb135c7f32d01, 0x5e32d7581368346d, 0x68629a024f2c770c, 0x5018edb9a0dfee9c,\n\t\t0x3a3c1e0d89599a97, 0x95a78f07d2312971, 0x3b813a9323dea87c, 0xd63e9b2a16a2cfc2,\n\t\t0xebb8758e940ae51b, 0x363faaf7e372367f, 0x8f2a34f38a508850, 0xe05b4ef569b14cfc,\n\t\t0x5104b9217fe22504, 0xd349aaf78cb844bc, 0x6236abb35107b232, 0x1635a8e5231cfac6,\n\t\t0x53772874e0d274fb, 0x347c6f99ae78d163, 0x1ca9cc2fb069df18, 0x9aab3fa7c74018cf,\n\t\t0xe7a77550899599d6, 0x14d4ed6292d3f471, 0x21ead5efdaa996a0, 0xbf4fbba5d0ac53ef,\n\t\t0x3bc684e5c1b1722e, 0x2ac4ada0514a8c51, 0x22d6399046a83428, 0x3b08d444ef83104e,\n\t\t0x022c75a7c9f64723, 0xa5ed7da2590be77f, 0x8e721ad2defe24aa, 0x3e69e053e2b187a2,\n\t\t0xce60fe699dcacb24, 0x7e72705bc9af79da, 0x594e8f4f4ae42029, 0x6021f96539b44c14,\n\t\t0x6b8c89769f397818, 0x8866edf93b123c2e, 0xcb158860c0919543, 0x01ff9fe71fb36a63,\n\t\t0x90db5d765d3f5011, 0x46541703158ba9ac, 0x09225f5b655d3ba6, 0x4213c64639e1736f,\n\t\t0x68c8b644dd94355a, 0x87169c4e0431171b, 0xf36646eec0067423, 0x99e7cd8c751aa646,\n\t\t0xcfe626d3031a2171, 0x3ed71337f54757f3, 0x5ac1fd1f4b353432, 0x25dfc52821ea2394,\n\t\t0x35ad1be1cb2ad7be, 0x26139157c61fc26b, 0x35e9e273d88f99e7, 0xa0ee86a30ea7343b,\n\t\t0x886ce1e26d6001d9, 0x014403c0715f7b11, 0x3a6bd359b9f433b7, 0x5513d22bae1f21a1,\n\t\t0xe38a239677ed4277, 0x17b39ca14896c138, 0x41b98a7a2b89109f, 0x6c5b2c21ad260da1,\n\t\t0x45aec24a92505b1e, 0xa0bfe245c932c47d, 0x6318997a226c3ac2, 0x820cac6be1f42886,\n\t\t0x5760c2b35415c521, 0x83e7a063f2e6cd56, 0x76332d7fc2dcb1ff, 0xeb9ac3304d4b72ea,\n\t\t0x00c393e61141c46f, 0x4e0e603506aaef80, 0xd78b9f0a8b9042b6, 0xdb6b1ce781374a54,\n\t\t0x463f921e426d10e0, 0xd3928841c7d7c481, 0x82b84916883bd6a0, 0x339a8eb1581931db,\n\t\t0x004e077b5dc400da, 0xfbe0bb55d0595e84, 0xdebac9094a7416c5, 0xbbe4aeee8f16ea37,\n\t\t0x903143492d3a0958, 0xdc10c884e32309b9, 0x5758329e210f4bfe, 0x9fa79b8124950e9c,\n\t\t0x1dea376717a93e38, 0xd5ecd2f169f0c9a9, 0x12bc43a1112d451f, 0x972c868fd1c0ab91,\n\t\t0x5385dcbe56133869, 0xfdbaa4cb4f597f9e, 0x6e7acd0f42030036, 0xd89476e12d8cbe1f,\n\t\t0x5d567b8c0902e5d7, 0xf5b6430d3246c66b, 0x1ff82d8cc5bf68f2, 0x43939e47df3c2997,\n\t\t0xc20a589e2eccf7ad, 0x55100d45c1468ce9, 0xca1a2bbbfa7f99a0, 0xd97425cce2b3ca74,\n\t\t0xba2c73d6b645b815, 0x5c5b487b827e5c4a, 0x707cd09ac43a88b9, 0x453346a6582fa103,\n\t\t0xf781a361fe52e6d9, 0xfa302baeb75815ca, 0xfae30f25cfc3f559, 0x94a6d9edc13d4d14,\n\t\t0x1ff9243cd4b19e60, 0xd979205825360367, 0x066e3de27c6cf6fb, 0xd4736aabe8eacefe,\n\t\t0xdb64a75b5794885f, 0xc8b376b48351c9dd, 0x2bf1f8e779c49d15, 0x6fc413b9230771e6,\n\t\t0x824bfbefc0392561, 0x0ed0b8ad97a79ce3, 0xa763ee97f261840e, 0xd756fa2efc2781a1,\n\t\t0x61460c93e9a965be, 0x4d9d0b6847a7306d, 0xe9695b4b911b10f6, 0xa27997d0823ddddd,\n\t\t0xf3c563dd03448651, 0xab837e3084a2d65c, 0x5083294e0a4d749b, 0x1218544274486331,\n\t\t0xdc0dc147e39c57b7, 0x67145247fa3b66b2, 0x409e9f1d134bf695, 0x174ec11ccb150efe,\n\t\t0x026cca48bc69b7ca, 0xf7fb076aef504b5a, 0xfa8fdf601acf9ac0, 0xad03549894853639,\n\t\t0xaf315b2f8d954b06, 0x0f28d793bae103a7, 0x46a140f98a954e03, 0x21c1997400961c3c,\n\t\t0x292bf7bd55409eb7, 0xbd0b7669a52edffe, 0x350c5a32dddc5fdf, 0xa4dcf45331142e57,\n\t\t0x9e87a71388559236, 0xd35f7f41c3c1ed37, 0x660b4cb35e186a63, 0x8fc3ca5285b75d19,\n\t\t0x0970820ef13ba1c6, 0xea8d7ee8c0f14148, 0x0ab7ddf3b3d3758b, 0x9d8a0befc4bdb9ba,\n\t\t0xdb917c2a2a832bb7, 0xb2dceb528077e210, 0x4730786f8c9acd54, 0x8903ad39dc1b9b0f,\n\t\t0x6ec2884ffbca1aa1, 0xcb9e6a0660c3e3a2, 0x3162d7fec3b970af, 0xb284839e161a4286,\n\t\t0x982a1a79a1d7766f, 0x2b400e2a1d9714ec, 0xe892501cb2706459, 0x6fd7fb1bff1d3fcb,\n\t\t0x46392fa39c367d6a, 0x9e018a08716c4dc5, 0x167c4c329a9bfa2e, 0x649d2b29dfedb145,\n\t\t0x137591ec13d426ff, 0x9e3363980fe7da47, 0x04c9f9da52a0fae2, 0xd815b33f27fd7410,\n\t\t0x8a96624c4f789f6d, 0x18343837448f64dc, 0xbbd3ab977386ee3b, 0xa2b4ffb4b8f58918,\n\t\t0x1c13fc6f1956a2e8, 0xac7988d677b2d63d, 0xd03ecaf6c2e15145, 0x552528467c783247,\n\t\t0xa136ea87edcfe2ff, 0xc2b017fad6722a8a, 0x496b1d4b56893bd9, 0x8d1b173b6b59ce8a,\n\t\t0x4beb409746a3b1c9, 0x7f42465934340659, 0xd1c5cc9eef153ff2, 0x2d874e915df47096,\n\t\t0xc6262045a8619834, 0x04729a19deab5aa6, 0xc981a87fc69259ee, 0x89a239d61aa270ee,\n\t\t0x83a6ff9710437b1e, 0x44abfd3de24fdbf2, 0x8f3adfda0e3650ef, 0x9d8f73b40a578c2d,\n\t\t0x9a1c451324a30c3c, 0x4b57fe4caf865edf, 0xf5424003e4a17dd6, 0xc5b1561aadb03ab3,\n\t\t0x5b15e2791b0dd4ff, 0xc3f4da34adffddb3, 0x71c22fa8624965b4, 0xf2dc6b254c28af72,\n\t\t0x985e8e4a6b96ac4b, 0x224a9984be53e804, 0xa5c1b977dbe38b7b, 0xf5904fac298383db,\n\t\t0xe046ed14aa4bb4a4, 0x065f2b6273c95e85, 0x25bee6fdd8823f01, 0xc401c8f47f8063ce,\n\t\t0xc457975bb198da7a, 0x5b1b68f651d9f275, 0x27f5ba16132250e2, 0xaf21fd989c053981,\n\t\t0x7f2227281262014d, 0x4acff17cd88624c0, 0x04f1d174b80e66ec, 0x5ab3ec7a8e8c5f45,\n\t\t0x36df34fa8d802366, 0x2dc45158fb0505b9, 0x09c81949f6981e7a, 0x1b40e30915de08e4,\n\t\t0x5de1eae5e97ce5b9, 0xcbd089d1d17fedf7, 0xc61337c3959e5c2d, 0xee10d945d75eba04,\n\t\t0xf842d40598067f10, 0x4aa524d182736f5f, 0xff8dfbef7cf7ed73, 0x1a97605236a60f0b,\n\t\t0x1d627812ff7f0e46, 0xf95f3005af31daeb, 0x16dd691afa094b27, 0xa341a1f55ebdaf16,\n\t\t0xae4427ce089c81ee, 0x2a1d03bbf12ae877, 0x6219e32ebec52155, 0x104b5fccc184df6a,\n\t\t0x38fe4cd53cb1b4f5, 0x4b0f4c6a630ae5f9, 0x7e3736c21fcf82e7, 0xa3c535da32890136,\n\t\t0x957584eefed5c5b6, 0x8c5285471dda564b, 0x4c16c99cf64c3737, 0xc91fabd40b0ba31f,\n\t\t0x68982161074fce8c, 0x3c4e204aba55c7cf, 0x562d7364029f465f, 0x603446fcd29438f5,\n\t\t0x21c6244966890e45, 0x798ff5753194e48e, 0x3047800e51ea3bac, 0xc88043df5d5304a5,\n\t\t0xf0131c1f3a1a6606, 0x68f3be12e32bdb4f, 0x8a32c291670af7a0, 0x6c7639e075126b95,\n\t\t0xc2dc5d872ffdb1d1, 0xd7ef8519a343443f, 0xae36b135b2c6c3ab, 0xc2ba69004ad234ca,\n\t\t0x29ead2a1a6aa4a38, 0x361262755e7e706f, 0x5a691aaf4616d7d4, 0xdc721a4d5412977e,\n\t\t0x3c3b63d739fa7357, 0xe305255d4e333ece, 0x1aa76bbb5ba55887, 0x876f314814e54688,\n\t\t0x145df714c9a7afb5, 0x2f2bc11a57760c1e, 0xd7d2ad3305b9d44c, 0x566c06f97a90736c,\n\t\t0x9a3aaf529e2fd129, 0x29652b7ffebb6213, 0x15c6c74f62425d83, 0x4dc913fc82a43e91,\n\t\t0x7a01f560f603b8dc, 0xafa2291ecf10106d, 0xdc7611daba496735, 0xf81302aab11f65bc,\n\t\t0x743b9b1937af1167, 0xb7174f8520995ae4, 0xca881e5ea17f8705, 0xf605904ebbeda8ea,\n\t\t0x438ad40d293ad385, 0x0bb7d5de136f3d1c, 0xaae0348d3ffc5ff5, 0xf91e7e51f404856a,\n\t\t0x31fbd722068a6fc1, 0x864891d15bb4a89d, 0x6d126f0a4dfa6f1c, 0xd712b3b470fb5aaa,\n\t\t0x210679c2e7f2c4e5, 0x95e4a2f927478150, 0x7dd767a69d4b6e5f, 0x872e096f6218826e,\n\t\t0x377a47cffc920bad, 0xd25c95981469b866, 0xa88505a39da468a5, 0x37be0e4e0c788ab3,\n\t\t0x00eab8b3319105d9, 0x69b5e0512d2ee447, 0x03cc0ab9a4afbd84, 0xd8c48e75c57d27ef,\n\t\t0x50b9fbb4b2a7e28b, 0xcb6adb8d1ff67876, 0x73a1001086e53111, 0xf5ab519c8013349e,\n\t\t0x3cad9c51cbd789aa, 0x6054d55d2dfde2c6, 0x080ed1165d00c782, 0xa5c61ab7d8ad913d,\n\t\t0xf793f44ab7b08399, 0xea808a254cb32744, 0x725543e54ab02d17, 0x1a9e9b7af53bfd57,\n\t\t0x17e4f93f31e1946f, 0xc9c5ddeae73ecd62, 0x97d23bd21fd433cf, 0xa3b275491370d1d6,\n\t\t0xb11f894611aa3032, 0x88812884130146c5, 0x5e1d165feed365f2, 0x74eb3d8a3aa050eb,\n\t\t0xbdb53fef87ce2b77, 0xb56b7c6b8e252272, 0x1578c4dafc9cdd1a, 0xbcc3ebcf5e0e426e,\n\t\t0x050dde0254c70bf1, 0x85570c34369e7dfc, 0x79e9f82cab6ca84c, 0xbc81b8b89e3ebff5,\n\t\t0xc30e1620853f58a0, 0xc93e6d17fd29de5c, 0x22a81e8b93023d85, 0xb050be8554dcae34,\n\t\t0xa21c4e0248fafe46, 0x691a89370cf92c8f, 0x328f846846aaa897, 0xf36e7fd2b6e00d55,\n\t\t0x74eca9441886f398, 0x9b77954cfbc5d37d, 0xa601cc3d8dbd9417, 0x0d227beab2f2188d,\n\t\t0x6332124a8037360e, 0x475b581c1ae4617f, 0xe59ef4433484c860, 0x4ea43c61313e18dc,\n\t\t0x52880f1db6a06c9f, 0x3eda91ae52b504a7, 0x0902fa9c936f035d, 0xdf5807133df966ef,\n\t\t0x8f072fd1b4c332d1, 0x17d50c8eda5f369c, 0xc147ebe8f1c74e79, 0x9c5a22cb3b907ead,\n\t\t0x1d45a8b4b979eef7, 0x50c0493f531caf0b, 0x9b0503132930ad42, 0x1d93c9c4729d622f,\n\t\t0x62035e3181c9deb5, 0x8dfa8a80e9771bee, 0xae35a09d89a02aa2, 0x8692654edf0e67de,\n\t\t0xac06586213c259f1, 0x2a206d5282ec17c0, 0xa460b439cfc938a8, 0x04a0e405ba56c8ae,\n\t\t0x662b08dac8a62507, 0x469b055fe85471a6, 0x155250849d3aa846, 0xa126917e719bd859,\n\t\t0x7424f5be7c4447b0, 0x992b63c06e98a715, 0x3a182aacddbfa805, 0xda0c271e2fc2c6d6,\n\t}\n)\n"},{"name":"zobrist_test.gno","body":"package zobrist\n\nimport (\n\t\"testing\"\n)\n\n// piece character to internal piece\nvar p = [256]Piece{\n\t'P': PiecePawn,\n\t'R': PieceRook,\n\t'N': PieceKnight,\n\t'B': PieceBishop,\n\t'Q': PieceQueen,\n\t'K': PieceKing,\n\n\t'p': PieceBlack | PiecePawn,\n\t'r': PieceBlack | PieceRook,\n\t'n': PieceBlack | PieceKnight,\n\t'b': PieceBlack | PieceBishop,\n\t'q': PieceBlack | PieceQueen,\n\t'k': PieceBlack | PieceKing,\n}\n\n// NewBoard returns a Board normally set up at the initial position for standard\n// chess.\nfunc NewBoard() Board {\n\treturn Board{\n\t\t// row 1\n\t\tp['R'], p['N'], p['B'], p['Q'],\n\t\tp['K'], p['B'], p['N'], p['R'],\n\t\t// row 2\n\t\tp['P'], p['P'], p['P'], p['P'],\n\t\tp['P'], p['P'], p['P'], p['P'],\n\n\t\t// rows 3, 4, 5, 6\n\t\t0, 0, 0, 0, 0, 0, 0, 0,\n\t\t0, 0, 0, 0, 0, 0, 0, 0,\n\t\t0, 0, 0, 0, 0, 0, 0, 0,\n\t\t0, 0, 0, 0, 0, 0, 0, 0,\n\n\t\t// row 7\n\t\tp['p'], p['p'], p['p'], p['p'],\n\t\tp['p'], p['p'], p['p'], p['p'],\n\t\t// row 8\n\t\tp['r'], p['n'], p['b'], p['q'],\n\t\tp['k'], p['b'], p['n'], p['r'],\n\t}\n}\n\nfunc TestInitialPosition(t *testing.T) {\n\th := Hash(NewBoard(), false, 0, 255)\n\tif h != InitialPosition {\n\t\tt.Fatalf(\"InitialPosition is invalid: set to %d, should be %d\", InitialPosition, h)\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"34ldK5VHnJypB4l5k0jfWfAvp/RtuXbjbdEP96sVbi1J/xsQCJOy+asVPK2roMIzz4G1LNFN3JaGzqRsmSeoWQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"chess","path":"gno.land/p/morgan/chess","files":[{"name":"engine.gno","body":"// Package chess implements a simple chess engine, designed to implement all\n// of the official FIDE rules with the intention of validating moves for a\n// \"chess server\" realm (gno.land/r/demo/chess).\n//\n// To implement the rules, the FIDE \"Laws of Chess\" are used as a reference:\n// https://www.fide.com/FIDE/handbook/LawsOfChess.pdf\n//\n// This package was designed with a focus on clarity and on using this code as\n// a didactic tool. Any contributions to the code should respect this.\npackage chess\n\nimport (\n\t\"errors\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/morgan/chess/zobrist\"\n)\n\n// PositionFlags. The lower 4 bits indicate an en passant column; the upper\n// 4 indicate castling rights.\ntype PositionFlags byte\n\nconst (\n\tEnPassant PositionFlags = 1 \u003c\u003c (iota + 3)\n\tNoCastleWQ\n\tNoCastleWK\n\tNoCastleBQ\n\tNoCastleBK\n\n\tMaskEnPassant = 7 // low 4 bits\n)\n\n// CastlingRights returns FEN castling rights.\n// https://www.chessprogramming.org/Forsyth-Edwards_Notation#Castling_ability\nfunc (p PositionFlags) CastlingRights() string {\n\ts := \"\"\n\tif p\u0026NoCastleWK == 0 {\n\t\ts += \"K\"\n\t}\n\tif p\u0026NoCastleWQ == 0 {\n\t\ts += \"Q\"\n\t}\n\tif p\u0026NoCastleBK == 0 {\n\t\ts += \"k\"\n\t}\n\tif p\u0026NoCastleBQ == 0 {\n\t\ts += \"q\"\n\t}\n\tif s == \"\" {\n\t\treturn \"-\"\n\t}\n\treturn s\n}\n\n// Position contains the information about a chessboard, and surrounding\n// context: the previous moves, the castling rights and \"en passant\" column.\n//\n// NOTE: the position of a piece is encoded in a [Square].\ntype Position struct {\n\tB     Board\n\tMoves []Move\n\tFlags PositionFlags\n\n\t// Halfmoves since the last pawn move or capture.\n\t// https://www.chessprogramming.org/Halfmove_Clock\n\tHalfMoveClock uint16\n\t// Used to calculate repeating positions (3- 5-fold repetition).\n\t// Zobrist hashing: https://www.chessprogramming.org/Zobrist_Hashing\n\t// Reset together with halfmove clock.\n\tHashes []uint64\n}\n\n// NewPosition returns a new Position, set up with the initial board position.\nfunc NewPosition() Position {\n\treturn Position{\n\t\tB:      NewBoard(),\n\t\tMoves:  make([]Move, 0, 80), // typical chess game is ~40 moves, 80 half-moves\n\t\tHashes: []uint64{zobrist.InitialPosition},\n\t}\n}\n\n// Color of the \"next\" move after p.Moves. (White for even len(p.Moves),\n// Black otherwise)\nfunc (p Position) Color() Color { return Color(len(p.Moves)\u00261 == 1) }\n\n// uintSlice attaches the methods of sort.Interface to []uint64, sorting in increasing order.\ntype uintSlice []uint64\n\nfunc (x uintSlice) Len() int           { return len(x) }\nfunc (x uintSlice) Less(i, j int) bool { return x[i] \u003c x[j] }\nfunc (x uintSlice) Swap(i, j int)      { x[i], x[j] = x[j], x[i] }\n\n// maxHashCount counts the maximum number of repeating hashes in p.Hashes.\nfunc (p Position) maxHashCount() int {\n\tif len(p.Hashes) == 0 {\n\t\treturn 0\n\t}\n\tsort.Sort(uintSlice(p.Hashes))\n\tvar last uint64\n\tvar cur, max int\n\tfor _, v := range p.Hashes {\n\t\tif last != v {\n\t\t\tlast = v\n\t\t\tif cur \u003e= max {\n\t\t\t\tmax = cur\n\t\t\t}\n\t\t\tcur = 0\n\t\t}\n\t\tcur++\n\t}\n\tif cur \u003e= max {\n\t\tmax = cur\n\t}\n\treturn max\n}\n\nfunc sign(n int8) int8 {\n\tswitch {\n\tcase n \u003e 0:\n\t\treturn 1\n\tcase n \u003c 0:\n\t\treturn -1\n\tdefault:\n\t\treturn 0\n\t}\n}\n\nfunc abs(n int8) int8 {\n\treturn n * sign(n)\n}\n\n// EncodeFEN encodes p into FEN.\n// https://www.chessprogramming.org/Forsyth-Edwards_Notation\nfunc (p Position) EncodeFEN() string {\n\tvar s string\n\temptyCount := 0\n\t// FEN has different ordering from us, as [0] is a black rook while for us\n\t// is a white rook. So we need to invert the order of rows.\n\tfor i := 56; i \u003e= 0; i++ {\n\t\tv := p.B[i]\n\t\tif v == PieceEmpty {\n\t\t\temptyCount++\n\t\t\tif i%8 == 7 {\n\t\t\t\ts += strconv.Itoa(emptyCount) + \"/\"\n\t\t\t\temptyCount = 0\n\t\t\t\ti -= 16\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif emptyCount \u003e 0 {\n\t\t\ts += strconv.Itoa(emptyCount)\n\t\t\temptyCount = 0\n\t\t}\n\t\ts += v.String()\n\t\tif i%8 == 7 {\n\t\t\ts += \"/\"\n\t\t\ti -= 16\n\t\t}\n\t}\n\t// remove trailing slash\n\ts = s[:len(s)-1]\n\n\tstrs := []string{\n\t\ts,                        // 0: piece placement\n\t\t\"w\",                      // 1: side to move\n\t\tp.Flags.CastlingRights(), // 2: castling ability\n\t\t\"-\",                      // 3: e.p. target square\n\n\t\tstrconv.Itoa(int(p.HalfMoveClock)), // 4: halfmove clock\n\t\tstrconv.Itoa(len(p.Moves)/2 + 1),   // 5: fullmove counter\n\t}\n\n\tvar epFile byte\n\tif p.Flags\u0026EnPassant \u003e 0 {\n\t\tepFile = 'a' + byte(p.Flags\u0026MaskEnPassant)\n\t}\n\n\tif p.Color() == Black {\n\t\tstrs[1] = \"b\"\n\t\tif epFile != 0 {\n\t\t\tstrs[3] = string(epFile) + \"3\"\n\t\t}\n\t} else if epFile != 0 {\n\t\tstrs[3] = string(epFile) + \"6\"\n\t}\n\n\treturn strings.Join(strs, \" \")\n}\n\n// ValidateMove checks whether the given move is legal in Chess.\n//\n// Caller must guarantee m.To and m.From to be valid (\u003c64).\nfunc (p Position) ValidateMove(m Move) (newP Position, valid bool) {\n\tif p.B[m.To].StripColor() == PieceKing {\n\t\treturn\n\t}\n\n\treturn p.validateMove(m)\n}\n\n// validateMove allows for m to be a \"king-capture\" move, which is illegal in\n// chess, but it is useful for InCheck.\n// This is commonly known in chess programming as a \"pseudo-legal\" move.\nfunc (oldp Position) validateMove(m Move) (newPos Position, ok bool) {\n\tp := oldp\n\n\tpiece := p.B[m.From]\n\n\t// piece moved must be of player's color\n\tcolor := p.Color()\n\tif piece == PieceEmpty || piece.Color() != color ||\n\t\t// additionally, check piece has actually moved\n\t\tm.From == m.To {\n\t\treturn\n\t}\n\t// destination must not be occupied by piece of same color\n\tif to := p.B[m.To]; to != PieceEmpty \u0026\u0026 to.Color() == color {\n\t\treturn\n\t}\n\n\t// one of the two necessarily != 0 (consequence of m.From != m.To).\n\tdelta := m.From.Sub(m.To)\n\tdr, dc := delta[0], delta[1]\n\n\t// Keep old castling rights; remove en passant info.\n\tnewFlags := p.Flags \u0026^ (MaskEnPassant | EnPassant)\n\t// Marked as true for succesful promotions.\n\tvar promoted bool\n\n\tisDiag := func() bool {\n\t\t// move diagonally (|dr| == |dc|)\n\t\tif abs(dr) != abs(dc) {\n\t\t\treturn false\n\t\t}\n\t\tsignr, signc := sign(dr), sign(dc)\n\t\t// squares crossed must be empty\n\t\tfor i := int8(1); i \u003c abs(dr); i++ {\n\t\t\tif p.B[m.From.Move(i*signr, i*signc)] != PieceEmpty {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\treturn true\n\t}\n\tisHorizVert := func() bool {\n\t\t// only one of dr, dc must be 0 (horiz/vert movement)\n\t\tif dr != 0 \u0026\u0026 dc != 0 {\n\t\t\treturn false\n\t\t}\n\t\t// squares crossed must be empty\n\t\tfor i := int8(1); i \u003c abs(dr); i++ {\n\t\t\tif p.B[m.From.Move(i*sign(dr), 0)] != PieceEmpty {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\tfor i := int8(1); i \u003c abs(dc); i++ {\n\t\t\tif p.B[m.From.Move(0, i*sign(dc))] != PieceEmpty {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\treturn true\n\t}\n\n\tswitch piece.StripColor() {\n\tcase PieceRook:\n\t\tif !isHorizVert() {\n\t\t\treturn\n\t\t}\n\t\t// if rook has moved from a starting position, this disables castling\n\t\t// on the side of the rook. flag accordingly in the move.\n\t\tvar fg Square\n\t\tif color == Black {\n\t\t\tfg = 7 \u003c\u003c 3\n\t\t}\n\t\tswitch m.From {\n\t\tcase fg: // a-col rook (either side)\n\t\t\tif color == White {\n\t\t\t\tnewFlags |= NoCastleWQ\n\t\t\t} else {\n\t\t\t\tnewFlags |= NoCastleBQ\n\t\t\t}\n\t\tcase fg | 7: // h-col rook (either side)\n\t\t\tif color == White {\n\t\t\t\tnewFlags |= NoCastleWK\n\t\t\t} else {\n\t\t\t\tnewFlags |= NoCastleBK\n\t\t\t}\n\t\t}\n\n\tcase PieceKnight:\n\t\t// move L-shaped\n\t\t// rationale: if you only have positive integers, the only way you can\n\t\t// obtain x * y == 2 is if x,y are either 1,2 or 2,1.\n\t\tif abs(dc*dr) != 2 {\n\t\t\treturn\n\t\t}\n\n\tcase PieceBishop:\n\t\tif !isDiag() {\n\t\t\treturn\n\t\t}\n\n\tcase PieceQueen:\n\t\tif !isHorizVert() \u0026\u0026 !isDiag() {\n\t\t\treturn\n\t\t}\n\n\tcase PieceKing:\n\t\t// castling\n\t\tif abs(dc) == 2 \u0026\u0026 dr == 0 {\n\t\t\t// determine if castle is a valid form of castling for the given color\n\t\t\tctype := m.isCastle(color)\n\t\t\tif ctype == 0 {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif false ||\n\t\t\t\t// check that there are no previous moves which disable castling\n\t\t\t\tp.castlingDisabled(color, ctype) ||\n\t\t\t\t// check that we have the exact board set ups we need\n\t\t\t\t// + make sure that the original and crossed squares are not in check\n\t\t\t\t!p.checkCastlingSetup(ctype) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// perform rook move here\n\t\t\tp.B = p.B.castleRookMove(color, ctype)\n\t\t\t// add NoCastle flags to prevent any further castling\n\t\t\tif color == White {\n\t\t\t\tnewFlags |= NoCastleWQ | NoCastleWK\n\t\t\t} else {\n\t\t\t\tnewFlags |= NoCastleBQ | NoCastleBK\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\t// move 1sq in all directions\n\t\tif dc \u003c -1 || dc \u003e 1 || dr \u003c -1 || dr \u003e 1 {\n\t\t\treturn\n\t\t}\n\t\t// king has moved: disable castling.\n\t\tif color == White {\n\t\t\tnewFlags |= NoCastleWQ | NoCastleWK\n\t\t} else {\n\t\t\tnewFlags |= NoCastleBQ | NoCastleBK\n\t\t}\n\n\tcase PiecePawn:\n\t\t// determine direction depending on color\n\t\tdir := int8(1)\n\t\tif color == Black {\n\t\t\tdir = -1\n\t\t}\n\n\t\tswitch {\n\t\tcase dc == 0 \u0026\u0026 dr == dir: // 1sq up\n\t\t\t// destination must be empty (no captures allowed)\n\t\t\tif p.B[m.To] != PieceEmpty {\n\t\t\t\treturn\n\t\t\t}\n\t\tcase dc == 0 \u0026\u0026 dr == dir*2: // 2sq up (only from starting row)\n\t\t\twantRow := Square(1)\n\t\t\tif color == Black {\n\t\t\t\twantRow = 6\n\t\t\t}\n\t\t\t// check starting row, and that two squares are empty\n\t\t\tif (m.From\u003e\u003e3) != wantRow ||\n\t\t\t\tp.B[m.From.Move(int8(dir), 0)] != PieceEmpty ||\n\t\t\t\tp.B[m.To] != PieceEmpty {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t_, col := m.To.Split()\n\t\t\tnewFlags |= EnPassant | PositionFlags(col)\n\t\tcase abs(dc) == 1 \u0026\u0026 dr == dir: // capture on diag\n\t\t\t// must be a capture\n\t\t\tif p.B[m.To] == PieceEmpty {\n\t\t\t\tif sq := p.checkEnPassant(color, m.To); sq != SquareInvalid {\n\t\t\t\t\t// remove other pawn\n\t\t\t\t\tp.B[sq] = PieceEmpty\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// p.B[m.To] is necessarily an opponent piece; we check \u0026 return\n\t\t\t// p.B[m.To].Color == color at the beginning of the fn.\n\t\tdefault: // not a recognized move\n\t\t\treturn\n\t\t}\n\n\t\trow := m.To \u003e\u003e 3\n\t\tif (color == White \u0026\u0026 row == 7) ||\n\t\t\t(color == Black \u0026\u0026 row == 0) {\n\t\t\tswitch m.Promotion {\n\t\t\tcase 0:\n\t\t\t\t// m.To is a king? then this is a pseudo-move check.\n\t\t\t\t// assume queen in that case.\n\t\t\t\tif p.B[m.To].StripColor() != PieceKing {\n\t\t\t\t\t// no promotion given, invalid\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tm.Promotion = PieceQueen\n\t\t\tcase PieceQueen, PieceBishop, PieceKnight, PieceRook:\n\t\t\tdefault:\n\t\t\t\treturn\n\t\t\t}\n\t\t\tpromoted = true\n\t\t\tp.B[m.From] = m.Promotion | color.Piece()\n\t\t}\n\t}\n\n\t// reject moves with promotion if there's nothing to promote\n\tif m.Promotion != 0 \u0026\u0026 !promoted {\n\t\treturn\n\t}\n\n\tif p.B[m.To].StripColor() == PieceKing {\n\t\t// King captures don't check for our own king in check;\n\t\t// these are only \"theoretical\" moves.\n\t\treturn Position{}, true\n\t}\n\n\t// perform board mutation\n\tcapture := p.B[m.To] != PieceEmpty\n\tp.B[m.From], p.B[m.To] = PieceEmpty, p.B[m.From]\n\tp.Flags = newFlags\n\tp.Moves = append([]Move{}, p.Moves...)\n\tp.Moves = append(p.Moves, m)\n\n\t// halfmove clock + hashes logic\n\tif piece.StripColor() == PiecePawn || capture {\n\t\t// reset both\n\t\tp.HalfMoveClock = 0\n\t\tp.Hashes = nil\n\t} else {\n\t\tp.HalfMoveClock++\n\t}\n\tep := byte(255)\n\tif p.Flags\u0026EnPassant != 0 {\n\t\tep = byte(p.Flags \u0026 MaskEnPassant)\n\t}\n\t// Not ideal, but avoids GenMoves potentially polluting \"real moves\" hashes.\n\tp.Hashes = append([]uint64{}, p.Hashes...)\n\tp.Hashes = append(\n\t\tp.Hashes,\n\t\t// color is inverted, because we consider the present move as already\n\t\t// done (hence, it is the other player to move).\n\t\tzobrist.Hash(toZobristBoard(p.B), bool(!color), byte(p.Flags)\u003e\u003e4, ep),\n\t)\n\n\t// is our king in check, as a result of the current move?\n\tif p.B.InCheck(color) {\n\t\treturn\n\t}\n\treturn p, true\n}\n\nfunc toZobristBoard(src Board) zobrist.Board {\n\tvar zb zobrist.Board\n\tfor pos, piece := range src {\n\t\tzb[pos] = zobrist.Piece(piece)\n\t}\n\treturn zb\n}\n\n// used by InCheck to simulate a move by black player.\nvar blackPrevMoves = make([]Move, 1)\n\n// InCheck checks whether the king with the given color is in check.\n// If such king does not exist on the board, InCheck returns false.\n//\n// A king is in check if the move from a piece of the other color\n// towards the king is valid, ignoring any checks on the other color's king.\n//\n// NOTE: the last remark is important:\n// https://lichess.org/analysis/4k3/8/4b3/8/8/8/K3R3/8_w_-_-_0_1?color=white\n// -- this is still a check for white, even if _technically_ black couldn't\n// move the bishop (as that would check its own king)\nfunc (b Board) InCheck(color Color) bool {\n\tpWant := PieceKing | color.Piece()\n\tkingp := b.findPiece(pWant)\n\tif kingp == SquareInvalid {\n\t\treturn false\n\t}\n\n\tpos := Position{B: b}\n\tif color == White {\n\t\t// color == White -\u003e simulate a move by black player -\u003e pos.Moves odd\n\t\tpos.Moves = blackPrevMoves\n\t}\n\n\tfor sq, piece := range b {\n\t\tif piece == PieceEmpty || piece.Color() == color {\n\t\t\tcontinue\n\t\t}\n\t\t_, ok := pos.validateMove(Move{\n\t\t\tFrom: Square(sq),\n\t\t\tTo:   kingp,\n\t\t\t// validateMove (unexp) understands that moves to capture a king are\n\t\t\t// pseudo moves, so it doesn't check for checking on its own king,\n\t\t\t// or promotion.\n\t\t})\n\t\tif ok {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// Board is a representation of a chess board.\n// Details on how to transform a chess algebraic position into an index\n// can be found at [Square].\ntype Board [64]Piece\n\n// NewBoard returns a Board normally set up at the initial position for standard\n// chess.\nfunc NewBoard() Board {\n\treturn Board{\n\t\t// row 1\n\t\tp['R'], p['N'], p['B'], p['Q'],\n\t\tp['K'], p['B'], p['N'], p['R'],\n\t\t// row 2\n\t\tp['P'], p['P'], p['P'], p['P'],\n\t\tp['P'], p['P'], p['P'], p['P'],\n\n\t\t// rows 3, 4, 5, 6\n\t\t0, 0, 0, 0, 0, 0, 0, 0,\n\t\t0, 0, 0, 0, 0, 0, 0, 0,\n\t\t0, 0, 0, 0, 0, 0, 0, 0,\n\t\t0, 0, 0, 0, 0, 0, 0, 0,\n\n\t\t// row 7\n\t\tp['p'], p['p'], p['p'], p['p'],\n\t\tp['p'], p['p'], p['p'], p['p'],\n\t\t// row 8\n\t\tp['r'], p['n'], p['b'], p['q'],\n\t\tp['k'], p['b'], p['n'], p['r'],\n\t}\n}\n\nfunc (b Board) findPiece(pWant Piece) Square {\n\tfor sq, p := range b {\n\t\tif p == pWant {\n\t\t\treturn Square(sq)\n\t\t}\n\t}\n\treturn SquareInvalid\n}\n\nfunc (p Position) checkCastlingSetup(typ byte) bool {\n\t// set up correct row and piece flags according to color\n\tc := p.Color()\n\tb := p.B\n\tvar fg Square\n\tvar pfg Piece\n\tif c == Black {\n\t\tfg, pfg = 7\u003c\u003c3, PieceBlack\n\t}\n\n\t// _cross are the squares that the king starts from,\n\t// crosses and \"lands\". they are recorded as they must all be\n\t// not in check by any opponent piece.\n\tvar _cross [3]Square\n\n\tif typ == 'K' {\n\t\tif !(b[fg|4] == pfg|PieceKing \u0026\u0026\n\t\t\tb[fg|5] == PieceEmpty \u0026\u0026\n\t\t\tb[fg|6] == PieceEmpty \u0026\u0026\n\t\t\tb[fg|7] == pfg|PieceRook) {\n\t\t\treturn false\n\t\t}\n\t\t_cross = [3]Square{fg | 4, fg | 5, fg | 6}\n\t} else {\n\t\tif !(b[fg|4] == pfg|PieceKing \u0026\u0026\n\t\t\tb[fg|3] == PieceEmpty \u0026\u0026\n\t\t\tb[fg|2] == PieceEmpty \u0026\u0026\n\t\t\tb[fg|1] == PieceEmpty \u0026\u0026\n\t\t\tb[fg|0] == pfg|PieceRook) {\n\t\t\treturn false\n\t\t}\n\t\t_cross = [3]Square{fg | 4, fg | 3, fg | 2}\n\t}\n\n\ttestb := p.B\n\tfor _, sq := range _cross {\n\t\ttestb[sq] = pfg | PieceKing\n\t\tif testb.InCheck(c) {\n\t\t\treturn false\n\t\t}\n\t\ttestb[sq] = PieceEmpty\n\t}\n\n\treturn true\n}\n\nfunc (b Board) castleRookMove(c Color, typ byte) Board {\n\tvar fg Square\n\tvar pfg Piece\n\tif c == Black {\n\t\tfg, pfg = 7\u003c\u003c3, PieceBlack\n\t}\n\n\tif typ == 'K' {\n\t\tb[fg|7], b[fg|5] = PieceEmpty, PieceRook|pfg\n\t} else {\n\t\tb[fg|0], b[fg|3] = PieceEmpty, PieceRook|pfg\n\t}\n\treturn b\n}\n\nfunc (p Position) castlingDisabled(color Color, kind byte) bool {\n\tif kind != 'K' \u0026\u0026 kind != 'Q' {\n\t\treturn false\n\t}\n\n\t// Determine what flag we're looking for.\n\tvar want PositionFlags\n\tswitch {\n\tcase color == White \u0026\u0026 kind == 'K':\n\t\twant = NoCastleWK\n\tcase color == White \u0026\u0026 kind == 'Q':\n\t\twant = NoCastleWQ\n\tcase color == Black \u0026\u0026 kind == 'K':\n\t\twant = NoCastleBK\n\tcase color == Black \u0026\u0026 kind == 'Q':\n\t\twant = NoCastleBQ\n\t}\n\n\treturn p.Flags\u0026want != 0\n}\n\nfunc (p Position) checkEnPassant(c Color, sq Square) Square {\n\trow, col := sq.Split()\n\tif p.Flags\u0026EnPassant == 0 ||\n\t\t(c == White \u0026\u0026 row != 5) ||\n\t\t(c == Black \u0026\u0026 row != 2) {\n\t\treturn SquareInvalid\n\t}\n\twantCol := byte(p.Flags \u0026 MaskEnPassant)\n\n\tif col != wantCol {\n\t\treturn SquareInvalid\n\t}\n\n\tif c == White {\n\t\treturn Square(4\u003c\u003c3 | col)\n\t}\n\treturn Square(3\u003c\u003c3 | col)\n}\n\n// InsufficientMaterial tests for insufficient material on the board, in which\n// case it returns true.\n//\n// See this reference for the rules used:\n// https://www.chess.com/article/view/how-chess-games-can-end-8-ways-explained#insufficient-material\n//\n//   - king vs king\n//   - king+N/B vs king\n//   - king+NN vs king\n//   - king+N/B vs king+N/B\nfunc (bd Board) InsufficientMaterial() bool {\n\t// strategy:\n\t// store the pieces which could count for an insufficient material\n\t// scenario in w, b.\n\t// if we encounter any pawn, queen, or rook, material is sufficient.\n\t// if we encounter 2 bishops, material is sufficient.\n\t// afterwards, we verify that w and b are one of the possible insuf material\n\t// scenarios.\n\n\tconst (\n\t\timN  byte = 1 \u003c\u003c iota // knight\n\t\timN2                  // second knight\n\t\timB                   // bishop\n\t)\n\tvar w, b byte\n\tfor _, p := range bd {\n\t\tstrip := p.StripColor()\n\t\tswitch strip {\n\t\tcase PieceQueen, PiecePawn, PieceRook:\n\t\t\treturn false\n\t\tcase PieceKing, PieceEmpty:\n\t\t\tcontinue\n\t\t}\n\t\t// strip is one of PieceBishop PieceKnight\n\t\tt := \u0026w\n\t\tif p.Color() == Black {\n\t\t\tt = \u0026b\n\t\t}\n\t\tif strip == PieceBishop {\n\t\t\tif *t\u0026imB != 0 {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\t*t |= imB\n\t\t} else {\n\t\t\tif *t\u0026imN2 != 0 {\n\t\t\t\t// third knight (ie. from pawn promotion)\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif *t\u0026imN != 0 {\n\t\t\t\t*t |= imN2\n\t\t\t} else {\n\t\t\t\t*t |= imN\n\t\t\t}\n\t\t}\n\t}\n\t// make it so that w is bigger than b\n\tif b \u003e w {\n\t\tw, b = b, w\n\t}\n\tswitch [2]byte{w, b} {\n\tcase [2]byte{0, 0}, // king vs king\n\t\t[2]byte{imN, 0}, // king+N/B vs king\n\t\t[2]byte{imB, 0},\n\t\t[2]byte{imN | imN2, 0}, // king+NN vs king\n\t\t[2]byte{imN, imN},      // king+N/B vs king+N/B\n\t\t[2]byte{imB, imB},\n\t\t[2]byte{imB, imN}: // imB \u003e imN, so [2]byte{imN, imB} cannot happen\n\t\treturn true\n\t}\n\treturn false\n}\n\n// GenMoves implements a rudimentary move generator.\n// This is not used beyond aiding in determing stalemate and doing perft tests.\n// Each generated move is passed to cb.\n// If cb returns an error, it is returned without processing further moves.\nfunc (p Position) GenMoves(cb func(Position, Move) error) error {\n\tcolor := p.Color()\n\tfor sq, piece := range p.B {\n\t\tif piece == PieceEmpty || piece.Color() != color {\n\t\t\tcontinue\n\t\t}\n\n\t\tfrom := Square(sq)\n\n\t\tpstrip := piece.StripColor()\n\t\t// If the piece is a pawn, and they are on the second last row, we know\n\t\t// that whatever move they do (advance, or take diagonally) they're going\n\t\t// to promote.\n\t\tprom := pstrip == PiecePawn \u0026\u0026\n\t\t\t((color == White \u0026\u0026 from\u003e\u003e3 == 6) ||\n\t\t\t\t(color == Black \u0026\u0026 from\u003e\u003e3 == 1))\n\n\t\t// delta generator needs to know if p is black\n\t\tif pstrip == PiecePawn \u0026\u0026 color == Black {\n\t\t\tpstrip |= Black.Piece()\n\t\t}\n\n\t\tvar err error\n\t\tdeltaGenerator(pstrip, func(delta Delta) byte {\n\t\t\t// create move; if the resulting square is oob, continue\n\t\t\tm := Move{\n\t\t\t\tFrom: from,\n\t\t\t\tTo:   from.Apply(delta),\n\t\t\t}\n\t\t\tif m.To == SquareInvalid ||\n\t\t\t\t(p.B[m.To] != PieceEmpty \u0026\u0026 p.B[m.To].Color() == color) {\n\t\t\t\treturn deltaGenStopLinear\n\t\t\t}\n\n\t\t\t// handle promotion case\n\t\t\tif prom {\n\t\t\t\tm.Promotion = PieceQueen\n\t\t\t}\n\n\t\t\t// if it's a valid move, call cb on it\n\t\t\tnewp, ok := p.ValidateMove(m)\n\t\t\tif !ok {\n\t\t\t\treturn deltaGenOK\n\t\t\t}\n\t\t\tif err = cb(newp, m); err != nil {\n\t\t\t\treturn deltaGenStop\n\t\t\t}\n\n\t\t\t// if we've promoted, handle the cases where we've promoted to a non-queen.\n\t\t\tif !prom {\n\t\t\t\treturn deltaGenOK\n\t\t\t}\n\n\t\t\tfor _, promPiece := range [...]Piece{PieceRook, PieceKnight, PieceBishop} {\n\t\t\t\tnewp.B[m.To] = promPiece | color.Piece()\n\t\t\t\tm.Promotion = promPiece\n\t\t\t\tif err = cb(newp, m); err != nil {\n\t\t\t\t\treturn deltaGenStop\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn deltaGenOK\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nconst (\n\t// carry on normally\n\tdeltaGenOK = iota\n\t// if the generator is doing a linear attack (ie. rook, bishop, queen),\n\t// then stop that (there is a piece of same colour in the way.)\n\tdeltaGenStopLinear\n\t// abort generation asap.\n\tdeltaGenStop\n)\n\n/*func init() {\n\tfor i := PiecePawn; i \u003c= PieceKing; i++ {\n\t\tprintln(\"generator \", i.String())\n\t\tdeltaGenerator(i, func(d Delta) byte {\n\t\t\tprintln(\"  \", d[0], d[1])\n\t\t\treturn deltaGenOK\n\t\t})\n\t}\n}*/\n\n// deltaGenerator generates the possible ways in which p can move.\n// the callback may return one of the three deltaGen* values.\nfunc deltaGenerator(p Piece, cb func(d Delta) byte) {\n\tdoLinear := func(d Delta) bool {\n\t\tfor i := int8(1); i \u003c= 7; i++ {\n\t\t\tswitch cb(d.Mul(i)) {\n\t\t\tcase deltaGenStopLinear:\n\t\t\t\treturn false\n\t\t\tcase deltaGenStop:\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}\n\trotate := func(d Delta, lin bool) bool {\n\t\tfor i := 0; i \u003c 4; i++ {\n\t\t\tif lin {\n\t\t\t\tif doLinear(d) {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif cb(d) == deltaGenStop {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\n\t\t\td = d.Rot()\n\t\t}\n\t\treturn false\n\t}\n\n\t// In the following, we use logical OR's to do conditional evaluation\n\t// (if the first item returns true, the second won't be evaluated)\n\tswitch p {\n\tcase PiecePawn, PiecePawn | PieceBlack:\n\t\tdir := int8(1)\n\t\tif p.Color() == Black {\n\t\t\tdir = -1\n\t\t}\n\t\t// try moving 1sq forward; if we get StopLinear, don't try to do 2sq.\n\t\tfw := cb(Delta{dir, 0})\n\t\tif fw == deltaGenStop {\n\t\t\treturn\n\t\t}\n\t\tif fw != deltaGenStopLinear {\n\t\t\tif cb(Delta{dir * 2, 0}) == deltaGenStop {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\t_ = cb(Delta{dir, 1}) == deltaGenStop ||\n\t\t\tcb(Delta{dir, -1}) == deltaGenStop\n\n\tcase PieceRook:\n\t\trotate(Delta{0, 1}, true)\n\tcase PieceBishop:\n\t\trotate(Delta{1, 1}, true)\n\tcase PieceKnight:\n\t\t_ = rotate(Delta{1, 2}, false) ||\n\t\t\trotate(Delta{2, 1}, false)\n\tcase PieceQueen:\n\t\t_ = rotate(Delta{0, 1}, true) ||\n\t\t\trotate(Delta{1, 1}, true)\n\tcase PieceKing:\n\t\t_ = rotate(Delta{0, 1}, false) ||\n\t\t\trotate(Delta{1, 1}, false) ||\n\t\t\tcb(Delta{0, 2}) == deltaGenStop ||\n\t\t\tcb(Delta{0, -2}) == deltaGenStop\n\t}\n}\n\n// Outcome is returned by IsFinished, and indicates if the game has finished,\n// and additionally if either of the player can claim draw by the 50-move rule\n// or by 3-fold repetition.\n//\n// Either one of the sequential outcomes will be set (values 1-5), indicating\n// that the game is terminated, or the low 3 bits will be 0, and\n// the Can* flags may be set.\ntype Outcome byte\n\nconst (\n\tNotFinished Outcome = iota\n\tCheckmate\n\tStalemate\n\tDrawn75Move\n\tDrawn5Fold\n\n\tCan50Move       Outcome = 8\n\tCan3Fold        Outcome = 16\n\tCanInsufficient Outcome = 32\n)\n\nvar errFound = errors.New(\"found\")\n\n// IsFinished determines if the game at the given position can be considered\n// \"finished\".\n// See [Outcome] for the possible results.\nfunc (p Position) IsFinished() Outcome {\n\terr := p.GenMoves(func(Position, Move) error {\n\t\treturn errFound\n\t})\n\t// If there is any legal move, this is not any kind of mate.\n\tif err != nil {\n\t\tmhc := p.maxHashCount()\n\t\tswitch {\n\t\tcase mhc \u003e= 5:\n\t\t\treturn Drawn5Fold\n\t\tcase p.HalfMoveClock \u003e= 150:\n\t\t\treturn Drawn75Move\n\t\t}\n\t\tvar o Outcome\n\t\tif mhc \u003e= 3 {\n\t\t\to |= Can3Fold\n\t\t}\n\t\tif p.HalfMoveClock \u003e= 100 {\n\t\t\to |= Can50Move\n\t\t}\n\t\tif p.B.InsufficientMaterial() {\n\t\t\to |= CanInsufficient\n\t\t}\n\t\treturn o\n\t}\n\n\t// No legal moves. Is the king in check?\n\tif p.B.InCheck(p.Color()) {\n\t\treturn Checkmate\n\t}\n\treturn Stalemate\n}\n\n// Color determines a player's color -- either white or black.\ntype Color bool\n\nconst (\n\tWhite Color = false\n\tBlack Color = true\n)\n\n// Piece returns the color as a piece to be OR'd into a Piece;\n// ie. 0 on White, and [PieceBlack] on black.\nfunc (c Color) Piece() Piece {\n\tif c == White {\n\t\treturn 0\n\t}\n\treturn PieceBlack\n}\n\n// Piece represents a piece on the board.\ntype Piece byte\n\n// PieceFromChar returns the piece corresponding to the given character.\n// White pieces are uppercase, and black pieces are lowercase.\n// If a piece is invalid, PieceInvalid is returned.\nfunc PieceFromChar(b byte) Piece {\n\treturn p[b]\n}\n\n// piece character to internal piece\nvar p = [256]Piece{\n\t'P': PiecePawn,\n\t'R': PieceRook,\n\t'N': PieceKnight,\n\t'B': PieceBishop,\n\t'Q': PieceQueen,\n\t'K': PieceKing,\n\n\t'p': PieceBlack | PiecePawn,\n\t'r': PieceBlack | PieceRook,\n\t'n': PieceBlack | PieceKnight,\n\t'b': PieceBlack | PieceBishop,\n\t'q': PieceBlack | PieceQueen,\n\t'k': PieceBlack | PieceKing,\n}\n\nvar pstring = [PieceBlack | PieceKing + 1]byte{\n\tPiecePawn:                'P',\n\tPieceRook:                'R',\n\tPieceKnight:              'N',\n\tPieceBishop:              'B',\n\tPieceQueen:               'Q',\n\tPieceKing:                'K',\n\tPieceBlack | PiecePawn:   'p',\n\tPieceBlack | PieceRook:   'r',\n\tPieceBlack | PieceKnight: 'n',\n\tPieceBlack | PieceBishop: 'b',\n\tPieceBlack | PieceQueen:  'q',\n\tPieceBlack | PieceKing:   'k',\n}\n\nfunc (p Piece) String() string {\n\tif int(p) \u003e= len(pstring) {\n\t\treturn \"\"\n\t}\n\tv := pstring[p]\n\tif v == 0 {\n\t\treturn \"\"\n\t}\n\treturn string(v)\n}\n\n// Possible values of Piece. Within the context of Board, Piece is assumed to\n// be white, unless p\u0026PieceBlack != 0. Note PieceBlack is not a valid piece; it\n// must be bitwise OR'd to a non-empty piece.\nconst (\n\tPieceEmpty Piece = iota\n\n\tPiecePawn\n\tPieceRook\n\tPieceKnight\n\tPieceBishop\n\tPieceQueen\n\tPieceKing\n\n\tPieceBlack Piece = 8 // bit-flag\n)\n\n// Color returns the color of the piece.\nfunc (p Piece) Color() Color { return Color(p\u0026PieceBlack != 0) }\n\n// Piece returns the given Piece without color information.\nfunc (p Piece) StripColor() Piece { return p \u0026^ PieceBlack }\n\n// Switch switches the color of the given piece.\nfunc (p Piece) Switch() Piece {\n\tif p.Color() == Black {\n\t\treturn p \u0026^ PieceBlack\n\t}\n\treturn p | PieceBlack\n}\n\n// Delta represents a 2d vector for indicating a movement from one square\n// to another. The first value indicates the change in column, the second the\n// change in rows.\ntype Delta [2]int8\n\n// Valid ensures the two values of delta are valid.\nfunc (d Delta) Valid() bool {\n\treturn d[0] \u003e= -7 \u0026\u0026 d[0] \u003c= 7 \u0026\u0026\n\t\td[1] \u003e= -7 \u0026\u0026 d[1] \u003c= 7 \u0026\u0026\n\t\t!(d[0] == 0 \u0026\u0026 d[1] == 0)\n}\n\n// Rot applies a 90 degree anti-clockwise rotation to d.\nfunc (d Delta) Rot() Delta {\n\t// Rationale: this is just matrix-vector multiplication.\n\t// 90 deg rotation is just the matrix {0, -1; 1, 0}.\n\treturn Delta{d[1], -d[0]}\n}\n\n// Mul multiplies both values by n, otherwise known as scalar product.\nfunc (d Delta) Mul(n int8) Delta {\n\treturn Delta{d[0] * n, d[1] * n}\n}\n\n// Square encodes piece position information, in chess the \"square\" the piece is on.\n// Indexing 0 as the LSB, bits 0-3 indicate the column and bits 4-6 indicate\n// the row. For instance, square 44 (decimal) is:\n//\n//\t44 = 0b00 101  100  = d5\n//\t          ^row ^col\n//\n// (note: in algebraic notation, this is swapped: the letter represents the\n// column, and the number represents the row).\ntype Square byte\n\n// SquareInvalid is returned by some Square-related methods to indicate\n// invalid parameters.\nconst SquareInvalid Square = 255\n\n// String returns p in algebraic notation.\nfunc (q Square) String() string {\n\tif q \u003e= 64 {\n\t\treturn \"\u003cinvalid\u003e\"\n\t}\n\treturn string(q\u00267+'a') + string(q\u003e\u003e3+'1')\n}\n\n// SquareFromString returns Square, reading the human-readable algebraic\n// notation in s. s must be 2 bytes long, with the first byte a letter included\n// between ['a'; 'h'], and the second a number included between ['1';'8'].\n// If s is invalid, SquareInvalid is returned.\nfunc SquareFromString(s string) Square {\n\tif len(s) != 2 {\n\t\treturn SquareInvalid\n\t}\n\tcol, row := s[0]-'a', s[1]-'1'\n\t// because s[0] is a byte, if s[0] \u003c 'a' then the above will underflow and\n\t// row will be \u003e= 8 (same for col).\n\tif row \u003e= 8 || col \u003e= 8 {\n\t\treturn SquareInvalid\n\t}\n\treturn Square(row\u003c\u003c3 | col)\n}\n\n// Move changes the square of q, moving it vertically according to dr\n// (delta row) and horizontally according to dc (delta column).\n// If the resulting square is not on the board, then SquareInvalid is returned.\nfunc (q Square) Move(dr, dc int8) Square {\n\tif q == SquareInvalid || !(Delta{dr, dc}).Valid() {\n\t\treturn SquareInvalid\n\t}\n\n\trow, col := int8(q\u003e\u003e3), int8(q\u00267)\n\trow += dr\n\tcol += dc\n\n\tnr, nc := Square(row), Square(col)\n\tif nr \u003e= 8 || nc \u003e= 8 {\n\t\treturn SquareInvalid\n\t}\n\treturn nr\u003c\u003c3 | nc\n}\n\n// Apply applies the given delta to the square.\n// It is shorthand for q.Move(d[0], d[1]).\nfunc (q Square) Apply(d Delta) Square { return q.Move(d[0], d[1]) }\n\n// Split splits Square into its components.\n// This function does not check if p is invalid.\nfunc (q Square) Split() (row, col byte) {\n\treturn byte(q \u003e\u003e 3), byte(q \u0026 7)\n}\n\n// SplitI works like [Square.Split], but returns int8's instead\n// of bytes.\nfunc (q Square) SplitI() (row, col int8) {\n\treturn int8(q \u003e\u003e 3), int8(q \u0026 7)\n}\n\n// Sub calculates the difference between the two squares.\n// q is the originating square, s is the ending square. The difference in\n// rows and columns from q to s is returned; for instance, d1.Sub(a4) yields\n// Delta{3, -3}.\nfunc (q Square) Sub(s Square) Delta {\n\tfr, fc := q.SplitI()\n\ttr, tc := s.SplitI()\n\treturn Delta{tr - fr, tc - fc}\n}\n\n// Move represents a chess game move.\ntype Move struct {\n\tFrom, To  Square\n\tPromotion Piece\n}\n\n// String returns a string representation of Move.\n// It is in the form of \"Long Algebraic Notation\".\n// https://backscattering.de/chess/uci/#move-lan\nfunc (m Move) String() string {\n\tp := \"\"\n\tif m.Promotion != 0 {\n\t\tp = string(m.Promotion.String()[0] + ('a' - 'A'))\n\t}\n\treturn m.From.String() + m.To.String() + p\n}\n\nvar (\n\tcastleWhiteQ = Move{From: SquareFromString(\"e1\"), To: SquareFromString(\"c1\")}\n\tcastleWhiteK = Move{From: SquareFromString(\"e1\"), To: SquareFromString(\"g1\")}\n\tcastleBlackQ = Move{From: SquareFromString(\"e8\"), To: SquareFromString(\"c8\")}\n\tcastleBlackK = Move{From: SquareFromString(\"e8\"), To: SquareFromString(\"g8\")}\n)\n\n// returns 0, 'K' or 'Q'.\nfunc (m Move) isCastle(c Color) (kind byte) {\n\tif c == White {\n\t\tswitch m {\n\t\tcase castleWhiteQ:\n\t\t\treturn 'Q'\n\t\tcase castleWhiteK:\n\t\t\treturn 'K'\n\t\t}\n\t} else {\n\t\tswitch m {\n\t\tcase castleBlackQ:\n\t\t\treturn 'Q'\n\t\tcase castleBlackK:\n\t\t\treturn 'K'\n\t\t}\n\t}\n\treturn 0\n}\n"},{"name":"engine_test.gno","body":"package chess\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc movePromo(from, to string, promo Piece) Move {\n\tm := move(from, to)\n\tm.Promotion = promo\n\treturn m\n}\n\nfunc move(from, to string) Move {\n\treturn Move{\n\t\tFrom: SquareFromString(strings.ToLower(from)),\n\t\tTo:   SquareFromString(strings.ToLower(to)),\n\t}\n}\n\nfunc TestCheckmate(t *testing.T) {\n\tfp := unsafeFEN(\"rn1qkbnr/pbpp1ppp/1p6/4p3/2B1P3/5Q2/PPPP1PPP/RNB1K1NR w KQkq - 0 1\")\n\tm := move(\"f3\", \"f7\")\n\tnewp, ok := fp.ValidateMove(m)\n\tif !ok {\n\t\tt.Fatal(\"ValidateMove returned false\")\n\t}\n\tmr := newp.IsFinished()\n\tif mr != Checkmate {\n\t\tt.Fatalf(\"expected Checkmate (%d), got %d\", Checkmate, mr)\n\t}\n}\n\nfunc TestCheckmateFromFEN(t *testing.T) {\n\tfp := unsafeFEN(\"rn1qkbnr/pbpp1Qpp/1p6/4p3/2B1P3/8/PPPP1PPP/RNB1K1NR b KQkq - 0 1\")\n\tmr := fp.IsFinished()\n\tif mr != Checkmate {\n\t\tt.Fatalf(\"expected Checkmate (%d), got %d\", Checkmate, mr)\n\t}\n}\n\nfunc TestStalemate(t *testing.T) {\n\tfp := unsafeFEN(\"k1K5/8/8/8/8/8/8/1Q6 w - - 0 1\")\n\tm := move(\"b1\", \"b6\")\n\tnewp, ok := fp.ValidateMove(m)\n\tif !ok {\n\t\tt.Fatal(\"ValidateMove rejected move\")\n\t}\n\tmr := newp.IsFinished()\n\tif mr != Stalemate {\n\t\tt.Fatalf(\"expected Stalemate (%d), got %d\", Stalemate, mr)\n\t}\n}\n\n// position shouldn't result in check/stalemate\n// because pawn can move http://en.lichess.org/Pc6mJDZN#138\nfunc TestNotMate(t *testing.T) {\n\tfp := unsafeFEN(\"8/3P4/8/8/8/7k/7p/7K w - - 2 70\")\n\tm := movePromo(\"d7\", \"d8\", PieceQueen)\n\tnewp, ok := fp.ValidateMove(m)\n\tif !ok {\n\t\tt.Fatal(\"ValidateMove returned false\")\n\t}\n\tmr := newp.IsFinished()\n\tif mr != NotFinished {\n\t\tt.Fatalf(\"expected NotFinished (%d), got %d\", NotFinished, mr)\n\t}\n}\n\nfunc TestXFoldRepetition(t *testing.T) {\n\tp := NewPosition()\n\tloop := [...]Move{\n\t\tmove(\"g1\", \"f3\"),\n\t\tmove(\"g8\", \"f6\"),\n\t\tmove(\"f3\", \"g1\"),\n\t\tmove(\"f6\", \"g8\"),\n\t}\n\tvar valid bool\n\tfor i := 0; i \u003c 5; i++ {\n\t\tfor j, m := range loop {\n\t\t\tp, valid = p.ValidateMove(m)\n\t\t\tif !valid {\n\t\t\t\tt.Fatalf(\"move %s not considered valid\", m.String())\n\t\t\t}\n\t\t\tfini := p.IsFinished()\n\t\t\tswitch {\n\t\t\tcase (i == 3 \u0026\u0026 j == 3) || i == 4:\n\t\t\t\t// after the fourth full iteration, it should be marked as \"drawn\" for 5-fold.\n\t\t\t\tif fini != Drawn5Fold {\n\t\t\t\t\tt.Errorf(\"i: %d j: %d; expect %d got %d\", i, j, Drawn5Fold, fini)\n\t\t\t\t}\n\t\t\tcase (i == 1 \u0026\u0026 j == 3) || i \u003e= 2:\n\t\t\t\t// After the second full iteration, IsFinished should mark this as \"can 3 fold\"\n\t\t\t\tif fini != Can3Fold {\n\t\t\t\t\tt.Errorf(\"i: %d j: %d; expect %d got %d\", i, j, Can3Fold, fini)\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\tif fini != NotFinished {\n\t\t\t\t\tt.Errorf(\"i: %d j: %d; expect %d got %d\", i, j, NotFinished, fini)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc assertMoves(p Position, moves ...Move) Position {\n\tvar valid bool\n\tfor _, move := range moves {\n\t\tp, valid = p.ValidateMove(move)\n\t\tif !valid {\n\t\t\tpanic(\"invalid move\")\n\t\t}\n\t}\n\treturn p\n}\n\nfunc TestXFoldRepetition2(t *testing.T) {\n\t// Like TestXFoldRepetition, but starts after the initial position.\n\n\tp := assertMoves(\n\t\tNewPosition(),\n\t\tmove(\"f2\", \"f4\"),\n\t\tmove(\"c7\", \"c5\"),\n\t)\n\n\tloop := [...]Move{\n\t\tmove(\"g1\", \"f3\"),\n\t\tmove(\"g8\", \"f6\"),\n\t\tmove(\"f3\", \"g1\"),\n\t\tmove(\"f6\", \"g8\"),\n\t}\n\tvar valid bool\n\tfor i := 0; i \u003c 5; i++ {\n\t\tfor j, m := range loop {\n\t\t\tp, valid = p.ValidateMove(m)\n\t\t\tif !valid {\n\t\t\t\tt.Fatalf(\"move %s not considered valid\", m.String())\n\t\t\t}\n\t\t\tfini := p.IsFinished()\n\t\t\tswitch {\n\t\t\tcase i == 4:\n\t\t\t\tif fini != Drawn5Fold {\n\t\t\t\t\tt.Errorf(\"i: %d j: %d; expect %d got %d\", i, j, Drawn5Fold, fini)\n\t\t\t\t}\n\t\t\tcase i \u003e= 2:\n\t\t\t\tif fini != Can3Fold {\n\t\t\t\t\tt.Errorf(\"i: %d j: %d; expect %d got %d\", i, j, Can3Fold, fini)\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\tif fini != NotFinished {\n\t\t\t\t\tt.Errorf(\"i: %d j: %d; expect %d got %d\", i, j, NotFinished, fini)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestXMoveRule(t *testing.T) {\n\tp := NewPosition()\n\n\tp.HalfMoveClock = 99\n\tnewp := assertMoves(p, move(\"g1\", \"f3\"))\n\tfini := newp.IsFinished()\n\tif fini != Can50Move {\n\t\tt.Errorf(\"want %d got %d\", Can50Move, fini)\n\t}\n\n\tp.HalfMoveClock = 149\n\tnewp = assertMoves(p, move(\"g1\", \"f3\"))\n\tfini = newp.IsFinished()\n\tif fini != Drawn75Move {\n\t\tt.Errorf(\"want %d got %d\", Drawn75Move, fini)\n\t}\n}\n\nfunc TestInsufficientMaterial(t *testing.T) {\n\tfens := []string{\n\t\t\"8/2k5/8/8/8/3K4/8/8 w - - 1 1\",\n\t\t\"8/2k5/8/8/8/3K1N2/8/8 w - - 1 1\",\n\t\t\"8/2k5/8/8/8/3K1B2/8/8 w - - 1 1\",\n\t\t\"8/2k5/2b5/8/8/3K1B2/8/8 w - - 1 1\",\n\t\t\"8/2k1n1n1/8/8/8/3K4/8/8 w - - 1 1\",\n\t\t\"8/2k1b3/8/8/8/3K1B2/8/8 w - - 1 1\",\n\t}\n\tfor _, f := range fens {\n\t\tpos := unsafeFEN(f)\n\t\to := pos.IsFinished()\n\t\tif o != CanInsufficient {\n\t\t\tt.Errorf(\"fen %q: want %d got %d\", f, CanInsufficient, o)\n\t\t}\n\t}\n}\n\nfunc TestSufficientMaterial(t *testing.T) {\n\tfens := []string{\n\t\t\"8/2k5/8/8/8/3K1B2/4N3/8 w - - 1 1\",\n\t\t\"8/2k5/8/8/8/3KBB2/8/8 w - - 1 1\",\n\t\t\"8/2k5/8/8/4P3/3K4/8/8 w - - 1 1\",\n\t\t\"8/2k5/8/8/8/3KQ3/8/8 w - - 1 1\",\n\t\t\"8/2k5/8/8/8/3KR3/8/8 w - - 1 1\",\n\t}\n\tfor _, f := range fens {\n\t\tpos := unsafeFEN(f)\n\t\to := pos.IsFinished()\n\t\tif o != NotFinished {\n\t\t\tt.Errorf(\"fen %q: want %d got %d\", f, NotFinished, o)\n\t\t}\n\t}\n}\n\ntype moveTest struct {\n\tpos     unsafeFENRes\n\tm       Move\n\tpostPos unsafeFENRes\n}\n\nvar (\n\tinvalidMoves = []moveTest{\n\t\t// out of turn moves\n\t\t{m: move(\"E7\", \"E5\"), pos: unsafeFEN(\"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1\")},\n\t\t{m: move(\"E2\", \"E4\"), pos: unsafeFEN(\"rnbqkbnr/1ppppppp/p7/8/8/8/PPPPPPPP/RNBQKBNR b KQkq - 0 1\")},\n\t\t// pawn moves\n\t\t{m: move(\"E2\", \"D3\"), pos: unsafeFEN(\"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1\")},\n\t\t{m: move(\"E2\", \"F3\"), pos: unsafeFEN(\"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1\")},\n\t\t{m: move(\"E2\", \"E5\"), pos: unsafeFEN(\"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1\")},\n\t\t{m: move(\"A2\", \"A1\"), pos: unsafeFEN(\"8/8/8/8/8/8/p7/8 b - - 0 1\")},\n\t\t{m: move(\"E6\", \"E5\"), pos: unsafeFEN(`2b1r3/2k2p1B/p2np3/4B3/8/5N2/PP1K1PPP/3R4 b - - 2 1`)},\n\t\t{m: move(\"H7\", \"H5\"), pos: unsafeFEN(`2bqkbnr/rpppp2p/2n2p2/p5pB/5P2/4P3/PPPP2PP/RNBQK1NR b KQk - 4 6`)},\n\t\t// knight moves\n\t\t{m: move(\"E4\", \"F2\"), pos: unsafeFEN(\"8/8/8/3pp3/4N3/8/5B2/8 w - - 0 1\")},\n\t\t{m: move(\"E4\", \"F3\"), pos: unsafeFEN(\"8/8/8/3pp3/4N3/8/5B2/8 w - - 0 1\")},\n\t\t// bishop moves\n\t\t{m: move(\"E4\", \"C6\"), pos: unsafeFEN(\"8/8/8/3pp3/4B3/5N2/8/8 w - - 0 1\")},\n\t\t{m: move(\"E4\", \"E5\"), pos: unsafeFEN(\"8/8/8/3pp3/4B3/5N2/8/8 w - - 0 1\")},\n\t\t{m: move(\"E4\", \"E4\"), pos: unsafeFEN(\"8/8/8/3pp3/4B3/5N2/8/8 w - - 0 1\")},\n\t\t{m: move(\"E4\", \"F3\"), pos: unsafeFEN(\"8/8/8/3pp3/4B3/5N2/8/8 w - - 0 1\")},\n\t\t// rook moves\n\t\t{m: move(\"B2\", \"B1\"), pos: unsafeFEN(\"8/1p5b/4N3/4p3/8/8/1R6/1B6 w - - 0 1\")},\n\t\t{m: move(\"B2\", \"C3\"), pos: unsafeFEN(\"8/1p5b/4N3/4p3/8/8/1R6/1B6 w - - 0 1\")},\n\t\t{m: move(\"B2\", \"B8\"), pos: unsafeFEN(\"8/1p5b/4N3/4p3/8/8/1R6/1B6 w - - 0 1\")},\n\t\t{m: move(\"B2\", \"G7\"), pos: unsafeFEN(\"8/1p5b/4N3/4p3/8/8/1R6/1B6 w - - 0 1\")},\n\t\t// queen moves\n\t\t{m: move(\"B2\", \"B1\"), pos: unsafeFEN(\"8/1p5b/4N3/4p3/8/8/1Q6/1B6 w - - 0 1\")},\n\t\t{m: move(\"B2\", \"C4\"), pos: unsafeFEN(\"8/1p5b/4N3/4p3/8/8/1Q6/1B6 w - - 0 1\")},\n\t\t{m: move(\"B2\", \"B8\"), pos: unsafeFEN(\"8/1p5b/4N3/4p3/8/8/1Q6/1B6 w - - 0 1\")},\n\t\t{m: move(\"B2\", \"G7\"), pos: unsafeFEN(\"8/1p5b/4N3/4p3/8/8/1Q6/1B6 w - - 0 1\")},\n\t\t// king moves\n\t\t{m: move(\"E4\", \"F3\"), pos: unsafeFEN(\"5r2/8/8/8/4K3/8/8/8 w - - 0 1\")},\n\t\t{m: move(\"E4\", \"F4\"), pos: unsafeFEN(\"5r2/8/8/8/4K3/8/8/8 w - - 0 1\")},\n\t\t{m: move(\"E4\", \"F5\"), pos: unsafeFEN(\"5r2/8/8/8/4K3/8/8/8 w - - 0 1\")},\n\t\t// castleing\n\t\t{m: move(\"E1\", \"B1\"), pos: unsafeFEN(\"r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1\")},\n\t\t{m: move(\"E8\", \"B8\"), pos: unsafeFEN(\"r3k2r/8/8/8/8/8/8/R3K2R b KQkq - 0 1\")},\n\t\t{m: move(\"E1\", \"C1\"), pos: unsafeFEN(\"r3k2r/8/8/8/8/8/8/R2QK2R w KQkq - 0 1\")},\n\t\t{m: move(\"E1\", \"C1\"), pos: unsafeFEN(\"2r1k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1\")},\n\t\t{m: move(\"E1\", \"C1\"), pos: unsafeFEN(\"3rk2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1\")},\n\t\t{m: move(\"E1\", \"G1\"), pos: unsafeFEN(\"r3k2r/8/8/8/8/8/8/R3K2R w Qkq - 0 1\")},\n\t\t{m: move(\"E1\", \"C1\"), pos: unsafeFEN(\"r3k2r/8/8/8/8/8/8/R3K2R w Kkq - 0 1\")},\n\t\t// invalid promotion for non-pawn move\n\t\t{m: movePromo(\"B8\", \"D7\", PieceQueen), pos: unsafeFEN(\"rn1qkb1r/pp3ppp/2p1pn2/3p4/2PP4/2NQPN2/PP3PPP/R1B1K2R b KQkq - 0 7\")},\n\t\t// en passant on doubled pawn file http://en.lichess.org/TnRtrHxf#24\n\t\t{m: move(\"E3\", \"F6\"), pos: unsafeFEN(\"r1b2rk1/pp2b1pp/1qn1p3/3pPp2/1P1P4/P2BPN2/6PP/RN1Q1RK1 w - f6 0 13\")},\n\t\t// can't move piece out of pin (even if checking enemy king) http://en.lichess.org/JCRBhXH7#62\n\t\t{m: move(\"E1\", \"E7\"), pos: unsafeFEN(\"4R3/1r1k2pp/p1p5/1pP5/8/8/1PP3PP/2K1Rr2 w - - 5 32\")},\n\t\t// invalid one up pawn capture\n\t\t{m: move(\"E6\", \"E5\"), pos: unsafeFEN(`2b1r3/2k2p1B/p2np3/4B3/8/5N2/PP1K1PPP/3R4 b - - 2 1`)},\n\t\t// invalid two up pawn capture\n\t\t{m: move(\"H7\", \"H5\"), pos: unsafeFEN(`2bqkbnr/rpppp2p/2n2p2/p5pB/5P2/4P3/PPPP2PP/RNBQK1NR b KQk - 4 6`)},\n\t\t// invalid pawn move d5e4\n\t\t{m: move(\"D5\", \"E4\"), pos: unsafeFEN(`rnbqkbnr/pp2pppp/8/2pp4/3P4/4PN2/PPP2PPP/RNBQKB1R b KQkq - 0 3`)},\n\t}\n\n\tpositionUpdates = []moveTest{\n\t\t{\n\t\t\tm:       move(\"E2\", \"E4\"),\n\t\t\tpos:     unsafeFEN(\"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1\"),\n\t\t\tpostPos: unsafeFEN(\"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1\"),\n\t\t},\n\t\t{\n\t\t\tm:       move(\"E1\", \"G1\"),\n\t\t\tpos:     unsafeFEN(\"r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1\"),\n\t\t\tpostPos: unsafeFEN(\"r3k2r/8/8/8/8/8/8/R4RK1 b kq - 1 1\"),\n\t\t},\n\t\t{\n\t\t\tm:       move(\"A4\", \"B3\"),\n\t\t\tpos:     unsafeFEN(\"2r3k1/1q1nbppp/r3p3/3pP3/pPpP4/P1Q2N2/2RN1PPP/2R4K b - b3 0 23\"),\n\t\t\tpostPos: unsafeFEN(\"2r3k1/1q1nbppp/r3p3/3pP3/11pP4/PpQ2N2/2RN1PPP/2R4K w - - 0 24\"),\n\t\t},\n\t\t{\n\t\t\tm:       move(\"E1\", \"G1\"),\n\t\t\tpos:     unsafeFEN(\"r2qk2r/pp1n1ppp/2pbpn2/3p4/2PP4/1PNQPN2/P4PPP/R1B1K2R w KQkq - 1 9\"),\n\t\t\tpostPos: unsafeFEN(\"r2qk2r/pp1n1ppp/2pbpn2/3p4/2PP4/1PNQPN2/P4PPP/R1B2RK1 b kq - 2 9\"),\n\t\t},\n\t\t// half move clock - knight move to f3 from starting position\n\t\t{\n\t\t\tm:       move(\"G1\", \"F3\"),\n\t\t\tpos:     unsafeFEN(\"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1\"),\n\t\t\tpostPos: unsafeFEN(\"rnbqkbnr/pppppppp/8/8/8/5N2/PPPPPPPP/RNBQKB1R b KQkq - 1 1\"),\n\t\t},\n\t\t// half move clock - king side castle\n\t\t{\n\t\t\tm:       move(\"E1\", \"G1\"),\n\t\t\tpos:     unsafeFEN(\"r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1\"),\n\t\t\tpostPos: unsafeFEN(\"r3k2r/8/8/8/8/8/8/R4RK1 b kq - 1 1\"),\n\t\t},\n\t\t// half move clock - queen side castle\n\t\t{\n\t\t\tm:       move(\"E1\", \"C1\"),\n\t\t\tpos:     unsafeFEN(\"r3k2r/ppqn1ppp/2pbpn2/3p4/2PP4/1PNQPN2/P2B1PPP/R3K2R w KQkq - 3 10\"),\n\t\t\tpostPos: unsafeFEN(\"r3k2r/ppqn1ppp/2pbpn2/3p4/2PP4/1PNQPN2/P2B1PPP/2KR3R b kq - 4 10\"),\n\t\t},\n\t\t// half move clock - pawn push\n\t\t{\n\t\t\tm:       move(\"E2\", \"E4\"),\n\t\t\tpos:     unsafeFEN(\"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1\"),\n\t\t\tpostPos: unsafeFEN(\"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1\"),\n\t\t},\n\t\t// half move clock - pawn capture\n\t\t{\n\t\t\tm:       move(\"E4\", \"D5\"),\n\t\t\tpos:     unsafeFEN(\"r1bqkbnr/ppp1pppp/2n5/3p4/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 2 3\"),\n\t\t\tpostPos: unsafeFEN(\"r1bqkbnr/ppp1pppp/2n5/3P4/8/5N2/PPPP1PPP/RNBQKB1R b KQkq - 0 3\"),\n\t\t},\n\t\t// half move clock - en passant\n\t\t{\n\t\t\tm:       move(\"E5\", \"F6\"),\n\t\t\tpos:     unsafeFEN(\"r1bqkbnr/ppp1p1pp/2n5/3pPp2/8/5N2/PPPP1PPP/RNBQKB1R w KQkq f6 0 4\"),\n\t\t\tpostPos: unsafeFEN(\"r1bqkbnr/ppp1p1pp/2n2P2/3p4/8/5N2/PPPP1PPP/RNBQKB1R b KQkq - 0 4\"),\n\t\t},\n\t\t// half move clock - piece captured by knight\n\t\t{\n\t\t\tm:       move(\"C6\", \"D4\"),\n\t\t\tpos:     unsafeFEN(\"r1bqkbnr/ppp1p1pp/2n5/3pPp2/3N4/8/PPPP1PPP/RNBQKB1R b KQkq - 1 4\"),\n\t\t\tpostPos: unsafeFEN(\"r1bqkbnr/ppp1p1pp/8/3pPp2/3n4/8/PPPP1PPP/RNBQKB1R w KQkq - 0 5\"),\n\t\t},\n\t}\n)\n\nfunc TestInvalidMoves(t *testing.T) {\n\tfor _, mt := range invalidMoves {\n\t\t_, ok := mt.pos.ValidateMove(mt.m)\n\t\tif ok {\n\t\t\tt.Errorf(\"fen %q: unexpected valid move (%s)\", mt.pos.orig, mt.m.String())\n\t\t}\n\t}\n}\n\nfunc TestPositionUpdates(t *testing.T) {\n\tfor _, mt := range positionUpdates {\n\t\tnp, ok := mt.pos.ValidateMove(mt.m)\n\t\tif !ok {\n\t\t\tt.Errorf(\"fen %q: rejected valid move (%s)\", mt.pos.orig, mt.m.String())\n\t\t\tcontinue\n\t\t}\n\t\tif np.B != mt.postPos.B {\n\t\t\tt.Errorf(\"%q: boards don't match\", mt.pos.orig)\n\t\t}\n\t\tif np.HalfMoveClock != mt.postPos.HalfMoveClock {\n\t\t\tt.Errorf(\"%q: hmc doesn't match; want %d got %d\", mt.pos.orig, mt.postPos.HalfMoveClock, np.HalfMoveClock)\n\t\t}\n\t\tif np.HalfMoveClock == 0 \u0026\u0026 len(np.Hashes) != 1 {\n\t\t\tt.Errorf(\"%q: hashes not reset\", mt.pos.orig)\n\t\t}\n\t\tif np.Flags != mt.postPos.Flags {\n\t\t\tt.Errorf(\"%q: flags don't match; want %d got %d\", mt.pos.orig, mt.postPos.Flags, np.Flags)\n\t\t}\n\t}\n}\n\nfunc TestPerft(t *testing.T) {\n\tmoves := make([]Move, 0, 10)\n\tfor n, res := range perfResults {\n\t\tt.Run(fmt.Sprintf(\"n%d\", n), func(t *testing.T) {\n\t\t\tif testing.Short() {\n\t\t\t\tt.Skip(\"skipping perft in short tests\")\n\t\t\t}\n\t\t\tres.pos.Moves = append(moves[:0], res.pos.Moves...)\n\t\t\tcounts := make([]int, len(res.nodesPerDepth))\n\t\t\tCountMoves(res.pos.Position, len(res.nodesPerDepth), counts)\n\t\t\tt.Logf(\"counts: %v\", counts)\n\t\t\tif !intsMatch(counts, res.nodesPerDepth) {\n\t\t\t\tt.Errorf(\"counts don't match: got %v want %v\", counts, res.nodesPerDepth)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc intsMatch(xx, yy []int) bool {\n\tif len(xx) != len(yy) {\n\t\treturn false\n\t}\n\tfor i := range xx {\n\t\tif xx[i] != yy[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nconst perftDebug = false\n\nfunc CountMoves(p Position, depth int, counts []int) {\n\ttotal := 0\n\tl := len(counts) - depth\n\tp.GenMoves(func(newp Position, m Move) error {\n\t\tcounts[l]++\n\t\tif depth \u003e 1 {\n\t\t\tcountMoves(newp, depth-1, counts)\n\t\t}\n\t\tdelta := counts[len(counts)-1] - total\n\t\tif perftDebug {\n\t\t\tfmt.Printf(\"%s%s: %d\\n\", m.From.String(), m.To.String(), delta)\n\t\t}\n\t\ttotal += delta\n\t\treturn nil\n\t})\n}\n\nfunc countMoves(p Position, depth int, counts []int) {\n\tl := len(counts) - depth\n\tp.GenMoves(func(newp Position, m Move) error {\n\t\tcounts[l]++\n\t\tif depth \u003e 1 {\n\t\t\tcountMoves(newp, depth-1, counts)\n\t\t}\n\t\treturn nil\n\t})\n}\n\ntype perfTest struct {\n\tpos           unsafeFENRes\n\tnodesPerDepth []int\n}\n\n/* https://www.chessprogramming.org/Perft_Results */\nvar perfResults = []perfTest{\n\t{pos: unsafeFEN(\"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1\"), nodesPerDepth: []int{\n\t\t20, 400, 8902, // 197281,\n\t\t// 4865609, 119060324, 3195901860, 84998978956, 2439530234167, 69352859712417\n\t}},\n\t{pos: unsafeFEN(\"r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1\"), nodesPerDepth: []int{\n\t\t48, 2039, 97862,\n\t\t// 4085603, 193690690\n\t}},\n\t{pos: unsafeFEN(\"8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1\"), nodesPerDepth: []int{\n\t\t14, 191, 2812, 43238, // 674624,\n\t\t// 11030083, 178633661\n\t}},\n\t{pos: unsafeFEN(\"r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1\"), nodesPerDepth: []int{\n\t\t6, 264, 9467, // 422333,\n\t\t// 15833292, 706045033\n\t}},\n\t{pos: unsafeFEN(\"r2q1rk1/pP1p2pp/Q4n2/bbp1p3/Np6/1B3NBn/pPPP1PPP/R3K2R b KQ - 0 1\"), nodesPerDepth: []int{\n\t\t6, 264, 9467, // 422333,\n\t\t// 15833292, 706045033\n\t}},\n\t{pos: unsafeFEN(\"rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8\"), nodesPerDepth: []int{\n\t\t44, 1486, 62379,\n\t\t// 2103487, 89941194\n\t}},\n\t{pos: unsafeFEN(\"r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10\"), nodesPerDepth: []int{\n\t\t46, 2079, // 89890,\n\t\t// 3894594, 164075551, 6923051137, 287188994746, 11923589843526, 490154852788714\n\t}},\n}\n\n// ---\n// testing utility functions\n\n// FEN decoding: see https://www.chessprogramming.org/Forsyth-Edwards_Notation\n// copied mostly from notnil/chess and adapted to our own system.\n\ntype unsafeFENRes struct {\n\tPosition\n\torig string\n}\n\nfunc unsafeFEN(fen string) unsafeFENRes {\n\tp, e := decodeFEN(fen)\n\tif e != nil {\n\t\tpanic(e)\n\t}\n\treturn unsafeFENRes{p, fen}\n}\n\n// Decodes FEN into Board and previous moves.\nfunc decodeFEN(fen string) (p Position, err error) {\n\tfen = strings.TrimSpace(fen)\n\tparts := strings.Split(fen, \" \")\n\tif len(parts) != 6 {\n\t\terr = fmt.Errorf(\"chess: fen invalid notation %s must have 6 sections\", fen)\n\t\treturn\n\t}\n\n\tp = NewPosition()\n\n\t// fen board\n\tvar ok bool\n\tp.B, ok = fenBoard(parts[0])\n\tif !ok {\n\t\terr = fmt.Errorf(\"chess: invalid fen board %s\", parts[0])\n\t\treturn\n\t}\n\n\t// do castling rights first (more convenient to set prev)\n\tif parts[2] != \"KQkq\" {\n\t\tp.Flags = castleRightsToPositionFlags(parts[2])\n\t}\n\n\t// color to play\n\tcolor := Color(parts[1] == \"b\")\n\tif color == Black {\n\t\t// add fake move to make len(prev) odd\n\t\tp.Moves = append(p.Moves, Move{})\n\t}\n\n\t// en passant\n\tif parts[3] != \"-\" {\n\t\tf, e := parseEnPassant(parts[3])\n\t\tif e != nil {\n\t\t\terr = e\n\t\t\treturn\n\t\t}\n\t\tp.Flags |= f\n\t}\n\n\thalfMove, _ := strconv.Atoi(parts[4])\n\tp.HalfMoveClock = uint16(halfMove)\n\n\t// parts[5]: full move counter, probably never implementing\n\n\treturn\n}\n\n// generates board from fen format: rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR\nfunc fenBoard(boardStr string) (Board, bool) {\n\trankStrs := strings.Split(boardStr, \"/\")\n\tif len(rankStrs) != 8 {\n\t\treturn Board{}, false\n\t}\n\tvar b Board\n\tfor idx, pieces := range rankStrs {\n\t\trank := (7 - Square(idx)) \u003c\u003c 3\n\t\tfile := Square(0)\n\t\tfor _, ch := range pieces {\n\t\t\tif ch \u003e= '1' \u0026\u0026 ch \u003c= '8' {\n\t\t\t\tdelta := byte(ch) - '0'\n\t\t\t\tfile += Square(delta)\n\t\t\t\tif file \u003e 8 {\n\t\t\t\t\treturn b, false\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tpiece := p[byte(ch)]\n\t\t\tif piece == PieceEmpty || file \u003e= 8 {\n\t\t\t\treturn b, false\n\t\t\t}\n\t\t\tb[rank|file] = piece\n\t\t\tfile++\n\t\t}\n\t\tif file != 8 {\n\t\t\treturn b, false\n\t\t}\n\t}\n\treturn b, true\n}\n\nfunc castleRightsToPositionFlags(cr string) (pf PositionFlags) {\n\tpf = NoCastleWQ | NoCastleWK | NoCastleBQ | NoCastleBK\n\tif cr == \"-\" {\n\t\treturn\n\t}\n\tfor _, ch := range cr {\n\t\tswitch ch {\n\t\tcase 'K':\n\t\t\tpf \u0026^= NoCastleWK\n\t\tcase 'Q':\n\t\t\tpf \u0026^= NoCastleWQ\n\t\tcase 'k':\n\t\t\tpf \u0026^= NoCastleBK\n\t\tcase 'q':\n\t\t\tpf \u0026^= NoCastleBQ\n\t\t}\n\t}\n\treturn\n}\n\nfunc parseEnPassant(strpos string) (PositionFlags, error) {\n\teppos := SquareFromString(strpos)\n\tif eppos == SquareInvalid {\n\t\treturn 0, fmt.Errorf(\"invalid pos: %s\", eppos)\n\t}\n\trow, col := eppos.Split()\n\tif row != 5 \u0026\u0026 row != 2 {\n\t\treturn 0, fmt.Errorf(\"invalid en passant pos: %s\", eppos)\n\t}\n\treturn EnPassant | PositionFlags(col), nil\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/morgan/chess\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"R5UbIam/cMArm20xaLnJns8pQAPp9IP7nBdae9KKgvhJIHqt3X0SrRAragRbXYm5EQTmTS8/jVBr1f3buU3r/A=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"glicko2","path":"gno.land/p/morgan/chess/glicko2","files":[{"name":"glicko2.gno","body":"package glicko2\n\nimport (\n\t\"math\"\n)\n\n// http://www.glicko.net/glicko/glicko2.pdf\n\nconst (\n\t// Step 1.\n\t// Determine a rating and RD for each player at the onset of the rating period. The\n\t// system constant, τ , which constrains the change in volatility over time, needs to be\n\t// set prior to application of the system. Reasonable choices are between 0.3 and 1.2,\n\t// though the system should be tested to decide which value results in greatest predictive\n\t// accuracy. Smaller values of τ prevent the volatility measures from changing by large\n\t// amounts, which in turn prevent enormous changes in ratings based on very improbable\n\t// results. If the application of Glicko-2 is expected to involve extremely improbable\n\t// collections of game outcomes, then τ should be set to a small value, even as small as,\n\t// say, τ = 0.2.\n\tGlickoTau = 0.5\n\n\tGlickoInitialRating     = 1500\n\tGlickoInitialRD         = 350\n\tGlickoInitialVolatility = 0.06\n\n\tglickoScaleFactor = 173.7178\n)\n\ntype PlayerRating struct {\n\tID address\n\n\tRating           float64\n\tRatingDeviation  float64\n\tRatingVolatility float64\n\n\t// working values, these are referred to as μ, φ and σ in the paper.\n\twr, wrd, wrv float64\n}\n\nfunc NewPlayerRating(addr address) *PlayerRating {\n\treturn \u0026PlayerRating{\n\t\tID:               addr,\n\t\tRating:           GlickoInitialRating,\n\t\tRatingDeviation:  GlickoInitialRD,\n\t\tRatingVolatility: GlickoInitialVolatility,\n\t}\n}\n\n// RatingScore is the outcome of a game between two players.\ntype RatingScore struct {\n\tWhite, Black address\n\tScore        float64 // 0 = black win, 0.5 = draw, 1 = white win\n}\n\nfunc getRatingScore(scores []RatingScore, player, opponent address) float64 {\n\tfor _, score := range scores {\n\t\tif score.White == player \u0026\u0026 score.Black == opponent {\n\t\t\treturn score.Score\n\t\t}\n\t\tif score.Black == player \u0026\u0026 score.White == opponent {\n\t\t\treturn 1 - score.Score\n\t\t}\n\t}\n\treturn -1\n}\n\nfunc UpdateRatings(source []*PlayerRating, scores []RatingScore) {\n\t// step 2: assign working wr/wrd/wrv\n\tfor _, player := range source {\n\t\tplayer.wr = (player.Rating - GlickoInitialRating) / glickoScaleFactor\n\t\tplayer.wrd = player.RatingDeviation / glickoScaleFactor\n\t\tplayer.wrv = player.RatingVolatility\n\t}\n\n\tfor _, player := range source {\n\t\t// step 3: compute the quantity v. This is the estimated variance of the team’s/player’s\n\t\t// rating based only on game outcomes\n\t\t// step 4: compute the quantity ∆, the estimated improvement in rating\n\t\tv, delta, competed := glickoVDelta(player, source, scores)\n\n\t\tnewRDPart := math.Sqrt(player.wrd*player.wrd + player.wrv*player.wrv)\n\t\tif !competed {\n\t\t\t// fast path for players who have not competed:\n\t\t\t// update only rating deviation\n\t\t\tplayer.RatingDeviation = newRDPart\n\t\t\tplayer.wr, player.wrd, player.wrv = 0, 0, 0\n\t\t\tcontinue\n\t\t}\n\n\t\t// step 5: determine the new value, σ′, of the volatility.\n\t\tplayer.wrv = glickoVolatility(player, delta, v)\n\t\t// step 6 and 7\n\t\tplayer.wrd = 1 / math.Sqrt(1/v+1/(newRDPart*newRDPart))\n\t\tplayer.wr = player.wr + player.wrd*player.wrd*(delta/v)\n\n\t\t// step 8\n\t\tplayer.Rating = player.wr*glickoScaleFactor + 1500\n\t\tplayer.RatingDeviation = player.wrd * glickoScaleFactor\n\t\tplayer.RatingVolatility = player.wrv\n\t\tplayer.wrv, player.wrd, player.wr = 0, 0, 0\n\t}\n}\n\nfunc glickoVDelta(player *PlayerRating, source []*PlayerRating, scores []RatingScore) (v, delta float64, competed bool) {\n\tvar deltaPartial float64\n\tfor _, opponent := range source {\n\t\tif opponent.ID == player.ID {\n\t\t\tcontinue\n\t\t}\n\t\tscore := getRatingScore(scores, player.ID, opponent.ID)\n\t\tif score == -1 { // not found\n\t\t\tcontinue\n\t\t}\n\t\tcompeted = true\n\n\t\t// Step 3\n\t\tgTheta := 1 / math.Sqrt(1+(3*opponent.wrd*opponent.wrd)/(math.Pi*math.Pi))\n\t\teMu := 1 / (1 + math.Exp(-gTheta*(player.wr-opponent.wr)))\n\t\tv += gTheta * gTheta * eMu * (1 - eMu)\n\n\t\t// step 4\n\t\tdeltaPartial += gTheta * (score - eMu)\n\t}\n\tv = 1 / v\n\treturn v, v * deltaPartial, competed\n}\n\nconst glickoVolatilityEps = 0.000001\n\n// Step 5\nfunc glickoVolatility(player *PlayerRating, delta, v float64) float64 {\n\taInit := math.Log(player.wrv * player.wrv)\n\tA := aInit\n\tvar B float64\n\trdp2 := player.wrd * player.wrd\n\tif delta*delta \u003e rdp2+v {\n\t\tB = math.Log(delta*delta - rdp2 - v)\n\t} else {\n\t\tk := float64(1)\n\t\tfor ; glickoVolatilityF(A-k*GlickoTau, aInit, rdp2, delta, v) \u003c 0; k++ {\n\t\t}\n\t\tB = A - k*GlickoTau\n\t}\n\tfA, fB := glickoVolatilityF(A, aInit, rdp2, delta, v), glickoVolatilityF(B, aInit, rdp2, delta, v)\n\tfor math.Abs(B-A) \u003e glickoVolatilityEps {\n\t\tC := A + ((A-B)*fA)/(fB-fA)\n\t\tfC := glickoVolatilityF(C, aInit, rdp2, delta, v)\n\t\tif fC*fB \u003c= 0 {\n\t\t\tA, fA = B, fB\n\t\t} else {\n\t\t\tfA = fA / 2\n\t\t}\n\t\tB, fB = C, fC\n\t}\n\treturn math.Exp(A / 2)\n}\n\n// rdp2: player.rd, power 2\nfunc glickoVolatilityF(x, a, rdp2, delta, v float64) float64 {\n\texpX := math.Exp(x)\n\treturn (expX*(delta*delta-rdp2-v-expX))/(2*pow2(rdp2+v+expX)) -\n\t\t(x-a)/(GlickoTau*GlickoTau)\n}\n\nfunc pow2(f float64) float64 { return f * f }\n"},{"name":"glicko2_test.gno","body":"package glicko2\n\nimport (\n\t\"testing\"\n)\n\nfunc TestExampleCalculations(t *testing.T) {\n\t// These are from the example in prof. Glickman's paper.\n\t// At the end, t.Log should print for the first player updated values\n\t// for Rating, RatingDeviation and RatingVolatility matching those in the\n\t// examples.\n\tratings := []*PlayerRating{\n\t\t{ID: \"1\", Rating: 1500, RatingDeviation: 200, RatingVolatility: 0.06},\n\t\t{ID: \"2\", Rating: 1400, RatingDeviation: 30, RatingVolatility: 0.06},\n\t\t{ID: \"3\", Rating: 1550, RatingDeviation: 100, RatingVolatility: 0.06},\n\t\t{ID: \"4\", Rating: 1700, RatingDeviation: 300, RatingVolatility: 0.06},\n\t}\n\tscores := []RatingScore{\n\t\t{White: \"1\", Black: \"2\", Score: 1},\n\t\t{White: \"1\", Black: \"3\", Score: 0},\n\t\t{White: \"1\", Black: \"4\", Score: 0},\n\t}\n\tUpdateRatings(ratings, scores)\n\tr := ratings[0]\n\tt.Logf(\"%.4f (± %.4f, volatility: %.4f); working values: %.2f / %.2f / %.2f\\n\",\n\t\tr.Rating, r.RatingDeviation, r.RatingVolatility,\n\t\tr.wr, r.wrd, r.wrv,\n\t)\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/morgan/chess/glicko2\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"6RGolUjF58XfBDZ85AjT4Gzth3sOraoWU3KZQ6ru+kYOcbpXze1dTl7LgJevgX8G9au7+IeHhIQuJaNKvE4+bw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","package":{"name":"cow","path":"gno.land/p/moul/cow","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/cow\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"},{"name":"node.gno","body":"package cow\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 nodes 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\n// _copy creates a copy of the node (excluding value).\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}\n\tif key \u003c node.key {\n\t\treturn node.getLeftNode().Has(key)\n\t}\n\treturn node.getRightNode().Has(key)\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}\n\t\tif node.key \u003c key {\n\t\t\treturn 1, nil, false\n\t\t}\n\t\treturn 0, nil, false\n\t}\n\n\tif key \u003c node.key {\n\t\treturn node.getLeftNode().Get(key)\n\t}\n\n\trightNode := node.getRightNode()\n\tindex, value, exists = rightNode.Get(key)\n\tindex += node.size - rightNode.size\n\treturn index, value, exists\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 node.height == 0 {\n\t\tif index == 0 {\n\t\t\treturn node.key, node.value\n\t\t}\n\t\tpanic(\"GetByIndex asked for invalid index\")\n\t}\n\t// TODO: could improve this by storing the sizes\n\tleftNode := node.getLeftNode()\n\tif index \u003c leftNode.size {\n\t\treturn leftNode.GetByIndex(index)\n\t}\n\treturn node.getRightNode().GetByIndex(index - leftNode.size)\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\n\t// Always create a new node for leaf nodes\n\tif node.height == 0 {\n\t\treturn node.setLeaf(key, value)\n\t}\n\n\t// Copy the node before modifying\n\tnewNode := node._copy()\n\tif key \u003c node.key {\n\t\tnewNode.leftNode, updated = node.getLeftNode().Set(key, value)\n\t} else {\n\t\tnewNode.rightNode, updated = node.getRightNode().Set(key, value)\n\t}\n\n\tif !updated {\n\t\tnewNode.calcHeightAndSize()\n\t\treturn newNode.balance(), updated\n\t}\n\n\treturn newNode, updated\n}\n\n// setLeaf inserts a new leaf 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.\nfunc (node *Node) setLeaf(key string, value any) (newSelf *Node, updated bool) {\n\tif key == node.key {\n\t\treturn NewNode(key, value), true\n\t}\n\n\tif key \u003c node.key {\n\t\treturn \u0026Node{\n\t\t\tkey:       node.key,\n\t\t\theight:    1,\n\t\t\tsize:      2,\n\t\t\tleftNode:  NewNode(key, value),\n\t\t\trightNode: node,\n\t\t}, false\n\t}\n\n\treturn \u0026Node{\n\t\tkey:       key,\n\t\theight:    1,\n\t\tsize:      2,\n\t\tleftNode:  node,\n\t\trightNode: NewNode(key, value),\n\t}, false\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}\n\t\treturn node, \"\", nil, false\n\t}\n\tif key \u003c node.key {\n\t\tvar newLeftNode *Node\n\t\tnewLeftNode, newKey, value, removed = node.getLeftNode().Remove(key)\n\t\tif !removed {\n\t\t\treturn node, \"\", value, false\n\t\t}\n\t\tif newLeftNode == nil { // left node held value, was removed\n\t\t\treturn node.rightNode, node.key, value, true\n\t\t}\n\t\tnode = node._copy()\n\t\tnode.leftNode = newLeftNode\n\t\tnode.calcHeightAndSize()\n\t\tnode = node.balance()\n\t\treturn node, newKey, value, true\n\t}\n\n\tvar newRightNode *Node\n\tnewRightNode, newKey, value, removed = node.getRightNode().Remove(key)\n\tif !removed {\n\t\treturn node, \"\", value, false\n\t}\n\tif newRightNode == nil { // right node held value, was removed\n\t\treturn node.leftNode, \"\", value, true\n\t}\n\tnode = node._copy()\n\tnode.rightNode = newRightNode\n\tif newKey != \"\" {\n\t\tnode.key = newKey\n\t}\n\tnode.calcHeightAndSize()\n\tnode = node.balance()\n\treturn node, \"\", value, true\n}\n\n// getLeftNode returns the left child of the node.\nfunc (node *Node) getLeftNode() *Node {\n\treturn node.leftNode\n}\n\n// getRightNode returns the right child of the node.\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\treturn node\n\t}\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}\n\t\t// Left Right Case\n\t\tleft := node.getLeftNode()\n\t\tnode.leftNode = left.rotateLeft()\n\t\treturn node.rotateRight()\n\t}\n\n\tif node.getRightNode().calcBalance() \u003c= 0 {\n\t\t// Right Right Case\n\t\treturn node.rotateLeft()\n\t}\n\n\t// Right Left Case\n\tright := node.getRightNode()\n\tnode.rightNode = right.rotateRight()\n\treturn node.rotateLeft()\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, descending bool, leavesOnly bool, cb func(*Node) bool) bool {\n\tif node == nil {\n\t\treturn false\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, descending, 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, descending 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\n\t\t}\n\t}\n\tfirst, second := node.getLeftNode(), node.getRightNode()\n\tif descending {\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\tcb(first)\n\t\t\tlimit--\n\t\t\tif limit \u003c= 0 {\n\t\t\t\treturn false\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, descending, 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, descending, 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\n// Equal compares two nodes for structural equality.\n// WARNING: This is an expensive operation that recursively traverses the entire tree structure.\n// It should only be used in tests or when absolutely necessary.\nfunc (node *Node) Equal(other *Node) bool {\n\t// Handle nil cases\n\tif node == nil || other == nil {\n\t\treturn node == other\n\t}\n\n\t// Compare node properties\n\tif node.key != other.key ||\n\t\tnode.value != other.value ||\n\t\tnode.height != other.height ||\n\t\tnode.size != other.size {\n\t\treturn false\n\t}\n\n\t// Compare children\n\tleftEqual := (node.leftNode == nil \u0026\u0026 other.leftNode == nil) ||\n\t\t(node.leftNode != nil \u0026\u0026 other.leftNode != nil \u0026\u0026 node.leftNode.Equal(other.leftNode))\n\tif !leftEqual {\n\t\treturn false\n\t}\n\n\trightEqual := (node.rightNode == nil \u0026\u0026 other.rightNode == nil) ||\n\t\t(node.rightNode != nil \u0026\u0026 other.rightNode != nil \u0026\u0026 node.rightNode.Equal(other.rightNode))\n\treturn rightEqual\n}\n"},{"name":"node_test.gno","body":"package cow\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestTraverseByOffset(t *testing.T) {\n\tconst testStrings = `Alfa\nAlfred\nAlpha\nAlphabet\nBeta\nBeth\nBook\nBrowser`\n\ttt := []struct {\n\t\tname string\n\t\tdesc bool\n\t}{\n\t\t{\"ascending\", false},\n\t\t{\"descending\", true},\n\t}\n\n\tfor _, tt := range tt {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tsl := strings.Split(testStrings, \"\\n\")\n\n\t\t\t// sort a first time in the order opposite to how we'll be traversing\n\t\t\t// the tree, to ensure that we are not just iterating through with\n\t\t\t// insertion order.\n\t\t\tsort.Strings(sl)\n\t\t\tif !tt.desc {\n\t\t\t\treverseSlice(sl)\n\t\t\t}\n\n\t\t\tr := NewNode(sl[0], nil)\n\t\t\tfor _, v := range sl[1:] {\n\t\t\t\tr, _ = r.Set(v, nil)\n\t\t\t}\n\n\t\t\t// then sort sl in the order we'll be traversing it, so that we can\n\t\t\t// compare the result with sl.\n\t\t\treverseSlice(sl)\n\n\t\t\tvar result []string\n\t\t\tfor i := 0; i \u003c len(sl); i++ {\n\t\t\t\tr.TraverseByOffset(i, 1, tt.desc, true, func(n *Node) bool {\n\t\t\t\t\tresult = append(result, n.Key())\n\t\t\t\t\treturn false\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif !slicesEqual(sl, result) {\n\t\t\t\tt.Errorf(\"want %v got %v\", sl, result)\n\t\t\t}\n\n\t\t\tfor l := 2; l \u003c= len(sl); l++ {\n\t\t\t\t// \"slices\"\n\t\t\t\tfor i := 0; i \u003c= len(sl); i++ {\n\t\t\t\t\tmax := i + l\n\t\t\t\t\tif max \u003e len(sl) {\n\t\t\t\t\t\tmax = len(sl)\n\t\t\t\t\t}\n\t\t\t\t\texp := sl[i:max]\n\t\t\t\t\tactual := []string{}\n\n\t\t\t\t\tr.TraverseByOffset(i, l, tt.desc, true, func(tr *Node) bool {\n\t\t\t\t\t\tactual = append(actual, tr.Key())\n\t\t\t\t\t\treturn false\n\t\t\t\t\t})\n\t\t\t\t\tif !slicesEqual(exp, actual) {\n\t\t\t\t\t\tt.Errorf(\"want %v got %v\", exp, actual)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHas(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []string\n\t\thasKey   string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\t\"has key in non-empty tree\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"B\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"does not have key in non-empty tree\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"F\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"has key in single-node tree\",\n\t\t\t[]string{\"A\"},\n\t\t\t\"A\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"does not have key in single-node tree\",\n\t\t\t[]string{\"A\"},\n\t\t\t\"B\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"does not have key in empty tree\",\n\t\t\t[]string{},\n\t\t\t\"A\",\n\t\t\tfalse,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\tresult := tree.Has(tt.hasKey)\n\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"Expected %v, got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGet(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tinput        []string\n\t\tgetKey       string\n\t\texpectIdx    int\n\t\texpectVal    any\n\t\texpectExists bool\n\t}{\n\t\t{\n\t\t\t\"get existing key\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"B\",\n\t\t\t1,\n\t\t\tnil,\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"get non-existent key (smaller)\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"@\",\n\t\t\t0,\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"get non-existent key (larger)\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"F\",\n\t\t\t5,\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"get from empty tree\",\n\t\t\t[]string{},\n\t\t\t\"A\",\n\t\t\t0,\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\tidx, val, exists := tree.Get(tt.getKey)\n\n\t\t\tif idx != tt.expectIdx {\n\t\t\t\tt.Errorf(\"Expected index %d, got %d\", tt.expectIdx, idx)\n\t\t\t}\n\n\t\t\tif val != tt.expectVal {\n\t\t\t\tt.Errorf(\"Expected value %v, got %v\", tt.expectVal, val)\n\t\t\t}\n\n\t\t\tif exists != tt.expectExists {\n\t\t\t\tt.Errorf(\"Expected exists %t, got %t\", tt.expectExists, exists)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetByIndex(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tinput       []string\n\t\tidx         int\n\t\texpectKey   string\n\t\texpectVal   any\n\t\texpectPanic bool\n\t}{\n\t\t{\n\t\t\t\"get by valid index\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t2,\n\t\t\t\"C\",\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"get by valid index (smallest)\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t0,\n\t\t\t\"A\",\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"get by valid index (largest)\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t4,\n\t\t\t\"E\",\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"get by invalid index (negative)\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t-1,\n\t\t\t\"\",\n\t\t\tnil,\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"get by invalid index (out of range)\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t5,\n\t\t\t\"\",\n\t\t\tnil,\n\t\t\ttrue,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\tif tt.expectPanic {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r == nil {\n\t\t\t\t\t\tt.Errorf(\"Expected a panic but didn't get one\")\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t}\n\n\t\t\tkey, val := tree.GetByIndex(tt.idx)\n\n\t\t\tif !tt.expectPanic {\n\t\t\t\tif key != tt.expectKey {\n\t\t\t\t\tt.Errorf(\"Expected key %s, got %s\", tt.expectKey, key)\n\t\t\t\t}\n\n\t\t\t\tif val != tt.expectVal {\n\t\t\t\t\tt.Errorf(\"Expected value %v, got %v\", tt.expectVal, val)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRemove(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tinput     []string\n\t\tremoveKey string\n\t\texpected  []string\n\t}{\n\t\t{\n\t\t\t\"remove leaf node\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"D\"},\n\t\t\t\"B\",\n\t\t\t[]string{\"A\", \"C\", \"D\"},\n\t\t},\n\t\t{\n\t\t\t\"remove node with one child\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"D\"},\n\t\t\t\"A\",\n\t\t\t[]string{\"B\", \"C\", \"D\"},\n\t\t},\n\t\t{\n\t\t\t\"remove node with two children\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"C\",\n\t\t\t[]string{\"A\", \"B\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"remove root node\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"C\",\n\t\t\t[]string{\"A\", \"B\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"remove non-existent key\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"F\",\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\ttree, _, _, _ = tree.Remove(tt.removeKey)\n\n\t\t\tresult := make([]string, 0)\n\t\t\ttree.Iterate(\"\", \"\", func(n *Node) bool {\n\t\t\t\tresult = append(result, n.Key())\n\t\t\t\treturn false\n\t\t\t})\n\n\t\t\tif !slicesEqual(tt.expected, result) {\n\t\t\t\tt.Errorf(\"want %v got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTraverse(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\t\"empty tree\",\n\t\t\t[]string{},\n\t\t\t[]string{},\n\t\t},\n\t\t{\n\t\t\t\"single node tree\",\n\t\t\t[]string{\"A\"},\n\t\t\t[]string{\"A\"},\n\t\t},\n\t\t{\n\t\t\t\"small tree\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"large tree\",\n\t\t\t[]string{\"H\", \"D\", \"L\", \"B\", \"F\", \"J\", \"N\", \"A\", \"C\", \"E\", \"G\", \"I\", \"K\", \"M\", \"O\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\", \"F\", \"G\", \"H\", \"I\", \"J\", \"K\", \"L\", \"M\", \"N\", \"O\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\tt.Run(\"iterate\", func(t *testing.T) {\n\t\t\t\tvar result []string\n\t\t\t\ttree.Iterate(\"\", \"\", func(n *Node) bool {\n\t\t\t\t\tresult = append(result, n.Key())\n\t\t\t\t\treturn false\n\t\t\t\t})\n\t\t\t\tif !slicesEqual(tt.expected, result) {\n\t\t\t\t\tt.Errorf(\"want %v got %v\", tt.expected, result)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tt.Run(\"ReverseIterate\", func(t *testing.T) {\n\t\t\t\tvar result []string\n\t\t\t\ttree.ReverseIterate(\"\", \"\", func(n *Node) bool {\n\t\t\t\t\tresult = append(result, n.Key())\n\t\t\t\t\treturn false\n\t\t\t\t})\n\t\t\t\texpected := make([]string, len(tt.expected))\n\t\t\t\tcopy(expected, tt.expected)\n\t\t\t\tfor i, j := 0, len(expected)-1; i \u003c j; i, j = i+1, j-1 {\n\t\t\t\t\texpected[i], expected[j] = expected[j], expected[i]\n\t\t\t\t}\n\t\t\t\tif !slicesEqual(expected, result) {\n\t\t\t\t\tt.Errorf(\"want %v got %v\", expected, result)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tt.Run(\"TraverseInRange\", func(t *testing.T) {\n\t\t\t\tvar result []string\n\t\t\t\tstart, end := \"C\", \"M\"\n\t\t\t\ttree.TraverseInRange(start, end, true, true, func(n *Node) bool {\n\t\t\t\t\tresult = append(result, n.Key())\n\t\t\t\t\treturn false\n\t\t\t\t})\n\t\t\t\texpected := make([]string, 0)\n\t\t\t\tfor _, key := range tt.expected {\n\t\t\t\t\tif key \u003e= start \u0026\u0026 key \u003c end {\n\t\t\t\t\t\texpected = append(expected, key)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif !slicesEqual(expected, result) {\n\t\t\t\t\tt.Errorf(\"want %v got %v\", expected, result)\n\t\t\t\t}\n\t\t\t})\n\t\t})\n\t}\n}\n\nfunc TestRotateWhenHeightDiffers(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\t\"right rotation when left subtree is higher\",\n\t\t\t[]string{\"E\", \"C\", \"A\", \"B\", \"D\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"E\", \"D\"},\n\t\t},\n\t\t{\n\t\t\t\"left rotation when right subtree is higher\",\n\t\t\t[]string{\"A\", \"C\", \"E\", \"D\", \"F\"},\n\t\t\t[]string{\"A\", \"C\", \"D\", \"E\", \"F\"},\n\t\t},\n\t\t{\n\t\t\t\"left-right rotation\",\n\t\t\t[]string{\"E\", \"A\", \"C\", \"B\", \"D\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"E\", \"D\"},\n\t\t},\n\t\t{\n\t\t\t\"right-left rotation\",\n\t\t\t[]string{\"A\", \"E\", \"C\", \"B\", \"D\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"E\", \"D\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\t// perform rotation or balance\n\t\t\ttree = tree.balance()\n\n\t\t\t// check tree structure\n\t\t\tvar result []string\n\t\t\ttree.Iterate(\"\", \"\", func(n *Node) bool {\n\t\t\t\tresult = append(result, n.Key())\n\t\t\t\treturn false\n\t\t\t})\n\n\t\t\tif !slicesEqual(tt.expected, result) {\n\t\t\t\tt.Errorf(\"want %v got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRotateAndBalance(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\t\"right rotation\",\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"left rotation\",\n\t\t\t[]string{\"E\", \"D\", \"C\", \"B\", \"A\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"left-right rotation\",\n\t\t\t[]string{\"C\", \"A\", \"E\", \"B\", \"D\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"right-left rotation\",\n\t\t\t[]string{\"C\", \"E\", \"A\", \"D\", \"B\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\ttree = tree.balance()\n\n\t\t\tvar result []string\n\t\t\ttree.Iterate(\"\", \"\", func(n *Node) bool {\n\t\t\t\tresult = append(result, n.Key())\n\t\t\t\treturn false\n\t\t\t})\n\n\t\t\tif !slicesEqual(tt.expected, result) {\n\t\t\t\tt.Errorf(\"want %v got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc slicesEqual(w1, w2 []string) bool {\n\tif len(w1) != len(w2) {\n\t\treturn false\n\t}\n\tfor i := 0; i \u003c len(w1); i++ {\n\t\tif w1[0] != w2[0] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc maxint8(a, b int8) int8 {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc reverseSlice(ss []string) {\n\tfor i := 0; i \u003c len(ss)/2; i++ {\n\t\tj := len(ss) - 1 - i\n\t\tss[i], ss[j] = ss[j], ss[i]\n\t}\n}\n\nfunc TestNodeStructuralSharing(t *testing.T) {\n\tt.Run(\"unmodified paths remain shared\", func(t *testing.T) {\n\t\troot := NewNode(\"B\", 2)\n\t\troot, _ = root.Set(\"A\", 1)\n\t\troot, _ = root.Set(\"C\", 3)\n\n\t\toriginalRight := root.rightNode\n\t\tnewRoot, _ := root.Set(\"A\", 10)\n\n\t\tif newRoot.rightNode != originalRight {\n\t\t\tt.Error(\"Unmodified right subtree should remain shared\")\n\t\t}\n\t})\n\n\tt.Run(\"multiple modifications reuse shared structure\", func(t *testing.T) {\n\t\t// Create initial tree\n\t\troot := NewNode(\"B\", 2)\n\t\troot, _ = root.Set(\"A\", 1)\n\t\troot, _ = root.Set(\"C\", 3)\n\n\t\t// Store original nodes\n\t\toriginalRight := root.rightNode\n\n\t\t// First modification\n\t\tmod1, _ := root.Set(\"A\", 10)\n\n\t\t// Second modification\n\t\tmod2, _ := mod1.Set(\"C\", 30)\n\n\t\t// Check sharing in first modification\n\t\tif mod1.rightNode != originalRight {\n\t\t\tt.Error(\"First modification should share unmodified right subtree\")\n\t\t}\n\n\t\t// Check that second modification creates new right node\n\t\tif mod2.rightNode == originalRight {\n\t\t\tt.Error(\"Second modification should create new right node\")\n\t\t}\n\t})\n}\n\nfunc TestNodeCopyOnWrite(t *testing.T) {\n\tt.Run(\"copy preserves structure\", func(t *testing.T) {\n\t\troot := NewNode(\"B\", 2)\n\t\troot, _ = root.Set(\"A\", 1)\n\t\troot, _ = root.Set(\"C\", 3)\n\n\t\t// Only copy non-leaf nodes\n\t\tif !root.IsLeaf() {\n\t\t\tcopied := root._copy()\n\t\t\tif copied == root {\n\t\t\t\tt.Error(\"Copy should create new instance\")\n\t\t\t}\n\n\t\t\t// Create temporary trees to use Equal method\n\t\t\toriginal := \u0026Tree{node: root}\n\t\t\tcopiedTree := \u0026Tree{node: copied}\n\t\t\tif !original.Equal(copiedTree) {\n\t\t\t\tt.Error(\"Copied node should preserve structure\")\n\t\t\t}\n\t\t}\n\t})\n\n\tt.Run(\"removal copy pattern\", func(t *testing.T) {\n\t\t// Create a more complex tree to test removal\n\t\troot := NewNode(\"B\", 2)\n\t\troot, _ = root.Set(\"A\", 1)\n\t\troot, _ = root.Set(\"C\", 3)\n\t\troot, _ = root.Set(\"D\", 4) // Add this to ensure proper tree structure\n\n\t\t// Store references to original nodes\n\t\toriginalRight := root.rightNode\n\t\toriginalRightRight := originalRight.rightNode\n\n\t\t// Remove \"A\" which should only affect the left subtree\n\t\tnewRoot, _, _, _ := root.Remove(\"A\")\n\n\t\t// Verify right subtree remains unchanged and shared\n\t\tif newRoot.rightNode != originalRight {\n\t\t\tt.Error(\"Right subtree should remain shared during removal of left node\")\n\t\t}\n\n\t\t// Also verify deeper nodes remain shared\n\t\tif newRoot.rightNode.rightNode != originalRightRight {\n\t\t\tt.Error(\"Deep right subtree should remain shared during removal\")\n\t\t}\n\n\t\t// Verify original tree is unchanged\n\t\tif _, _, exists := root.Get(\"A\"); !exists {\n\t\t\tt.Error(\"Original tree should remain unchanged\")\n\t\t}\n\t})\n\n\tt.Run(\"copy leaf node panic\", func(t *testing.T) {\n\t\tleaf := NewNode(\"A\", 1)\n\n\t\tdefer func() {\n\t\t\tif r := recover(); r == nil {\n\t\t\t\tt.Error(\"Expected panic when copying leaf node\")\n\t\t\t}\n\t\t}()\n\n\t\t// This should panic with our specific message\n\t\tleaf._copy()\n\t})\n}\n\nfunc TestNodeEqual(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tnode1    func() *Node\n\t\tnode2    func() *Node\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"nil nodes\",\n\t\t\tnode1:    func() *Node { return nil },\n\t\t\tnode2:    func() *Node { return nil },\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"one nil node\",\n\t\t\tnode1:    func() *Node { return NewNode(\"A\", 1) },\n\t\t\tnode2:    func() *Node { return nil },\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"single leaf nodes equal\",\n\t\t\tnode1:    func() *Node { return NewNode(\"A\", 1) },\n\t\t\tnode2:    func() *Node { return NewNode(\"A\", 1) },\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"single leaf nodes different key\",\n\t\t\tnode1:    func() *Node { return NewNode(\"A\", 1) },\n\t\t\tnode2:    func() *Node { return NewNode(\"B\", 1) },\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"single leaf nodes different value\",\n\t\t\tnode1:    func() *Node { return NewNode(\"A\", 1) },\n\t\t\tnode2:    func() *Node { return NewNode(\"A\", 2) },\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"complex trees equal\",\n\t\t\tnode1: func() *Node {\n\t\t\t\tn, _ := NewNode(\"B\", 2).Set(\"A\", 1)\n\t\t\t\tn, _ = n.Set(\"C\", 3)\n\t\t\t\treturn n\n\t\t\t},\n\t\t\tnode2: func() *Node {\n\t\t\t\tn, _ := NewNode(\"B\", 2).Set(\"A\", 1)\n\t\t\t\tn, _ = n.Set(\"C\", 3)\n\t\t\t\treturn n\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"complex trees different structure\",\n\t\t\tnode1: func() *Node {\n\t\t\t\t// Create a tree with structure:\n\t\t\t\t//     B\n\t\t\t\t//    / \\\n\t\t\t\t//   A   D\n\t\t\t\tn := NewNode(\"B\", 2)\n\t\t\t\tn, _ = n.Set(\"A\", 1)\n\t\t\t\tn, _ = n.Set(\"D\", 4)\n\t\t\t\treturn n\n\t\t\t},\n\t\t\tnode2: func() *Node {\n\t\t\t\t// Create a tree with structure:\n\t\t\t\t//     C\n\t\t\t\t//    / \\\n\t\t\t\t//   A   D\n\t\t\t\tn := NewNode(\"C\", 3)\n\t\t\t\tn, _ = n.Set(\"A\", 1)\n\t\t\t\tn, _ = n.Set(\"D\", 4)\n\t\t\t\treturn n\n\t\t\t},\n\t\t\texpected: false, // These trees should be different\n\t\t},\n\t\t{\n\t\t\tname: \"complex trees same structure despite different insertion order\",\n\t\t\tnode1: func() *Node {\n\t\t\t\tn, _ := NewNode(\"B\", 2).Set(\"A\", 1)\n\t\t\t\tn, _ = n.Set(\"C\", 3)\n\t\t\t\treturn n\n\t\t\t},\n\t\t\tnode2: func() *Node {\n\t\t\t\tn, _ := NewNode(\"A\", 1).Set(\"B\", 2)\n\t\t\t\tn, _ = n.Set(\"C\", 3)\n\t\t\t\treturn n\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"truly different structures\",\n\t\t\tnode1: func() *Node {\n\t\t\t\tn, _ := NewNode(\"B\", 2).Set(\"A\", 1)\n\t\t\t\treturn n // Tree with just two nodes\n\t\t\t},\n\t\t\tnode2: func() *Node {\n\t\t\t\tn, _ := NewNode(\"B\", 2).Set(\"C\", 3)\n\t\t\t\treturn n // Different two-node tree\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tnode1 := tt.node1()\n\t\t\tnode2 := tt.node2()\n\t\t\tresult := node1.Equal(node2)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"Expected Equal to return %v for %s\", tt.expected, tt.name)\n\t\t\t\tprintln(\"\\nComparison failed:\")\n\t\t\t\tprintln(\"Tree 1:\")\n\t\t\t\tprintTree(node1, 0)\n\t\t\t\tprintln(\"Tree 2:\")\n\t\t\t\tprintTree(node2, 0)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Helper function to print tree structure\nfunc printTree(node *Node, level int) {\n\tif node == nil {\n\t\treturn\n\t}\n\tindent := strings.Repeat(\"  \", level)\n\tprintln(fmt.Sprintf(\"%sKey: %s, Value: %v, Height: %d, Size: %d\",\n\t\tindent, node.key, node.value, node.height, node.size))\n\tprintTree(node.leftNode, level+1)\n\tprintTree(node.rightNode, level+1)\n}\n"},{"name":"tree.gno","body":"// Package cow provides a Copy-on-Write (CoW) AVL tree implementation.\n// This is a fork of gno.land/p/nt/avl/v0 that adds CoW functionality\n// while maintaining the original AVL tree interface and properties.\n//\n// Copy-on-Write creates a copy of a data structure only when it is modified,\n// while still presenting the appearance of a full copy. When a tree is cloned,\n// it initially shares all its nodes with the original tree. Only when a\n// modification is made to either the original or the clone are new nodes created,\n// and only along the path from the root to the modified node.\n//\n// Key features:\n//   - O(1) cloning operation\n//   - Minimal memory usage through structural sharing\n//   - Full AVL tree functionality (self-balancing, ordered operations)\n//   - Thread-safe for concurrent reads of shared structures\n//\n// While the CoW mechanism handles structural copying automatically, users need\n// to consider how to handle the values stored in the tree:\n//\n//  1. Simple Values (int, string, etc.):\n//     - These are copied by value automatically\n//     - No additional handling needed\n//\n//  2. Complex Values (structs, pointers):\n//     - Only the reference is copied by default\n//     - Users must implement their own deep copy mechanism if needed\n//\n// Example:\n//\n//\t// Create original tree\n//\toriginal := cow.NewTree()\n//\toriginal.Set(\"key1\", \"value1\")\n//\n//\t// Create a clone - O(1) operation\n//\tclone := original.Clone()\n//\n//\t// Modify clone - only affected nodes are copied\n//\tclone.Set(\"key1\", \"modified\")\n//\n//\t// Original remains unchanged\n//\tval := original.Get(\"key1\") // Returns \"value1\"\npackage cow\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.\nfunc (tree *Tree) Get(key string) any {\n\t_, value, exists := tree.node.Get(key)\n\tif !exists {\n\t\treturn nil\n\t}\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// Equal returns true if the two trees contain the same key-value pairs.\n// WARNING: This is an expensive operation that recursively traverses the entire tree structure.\n// It should only be used in tests or when absolutely necessary.\nfunc (tree *Tree) Equal(other *Tree) bool {\n\tif tree == nil || other == nil {\n\t\treturn tree == other\n\t}\n\treturn tree.node.Equal(other.node)\n}\n\n// Clone creates a shallow copy of the tree\nfunc (tree *Tree) Clone() *Tree {\n\tif tree == nil {\n\t\treturn nil\n\t}\n\treturn \u0026Tree{\n\t\tnode: tree.node,\n\t}\n}\n"},{"name":"tree_test.gno","body":"package cow\n\nimport (\n\t\"testing\"\n)\n\nfunc TestNewTree(t *testing.T) {\n\ttree := NewTree()\n\tif tree.node != nil {\n\t\tt.Error(\"Expected tree.node to be nil\")\n\t}\n}\n\nfunc TestTreeSize(t *testing.T) {\n\ttree := NewTree()\n\tif tree.Size() != 0 {\n\t\tt.Error(\"Expected empty tree size to be 0\")\n\t}\n\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\tif tree.Size() != 2 {\n\t\tt.Error(\"Expected tree size to be 2\")\n\t}\n}\n\nfunc TestTreeHas(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\n\tif !tree.Has(\"key1\") {\n\t\tt.Error(\"Expected tree to have key1\")\n\t}\n\n\tif tree.Has(\"key2\") {\n\t\tt.Error(\"Expected tree to not have key2\")\n\t}\n}\n\nfunc TestTreeGet(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\n\tvalue := tree.Get(\"key1\")\n\tif value == nil || value != \"value1\" {\n\t\tt.Error(\"Expected Get to return value1 and true\")\n\t}\n\n\tif tree.Has(\"key2\") {\n\t\tt.Error(\"Expected Get to return false for non-existent key\")\n\t}\n}\n\nfunc TestTreeGetByIndex(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\n\tkey, value := tree.GetByIndex(0)\n\tif key != \"key1\" || value != \"value1\" {\n\t\tt.Error(\"Expected GetByIndex(0) to return key1 and value1\")\n\t}\n\n\tkey, value = tree.GetByIndex(1)\n\tif key != \"key2\" || value != \"value2\" {\n\t\tt.Error(\"Expected GetByIndex(1) to return key2 and value2\")\n\t}\n\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Error(\"Expected GetByIndex to panic for out-of-range index\")\n\t\t}\n\t}()\n\ttree.GetByIndex(2)\n}\n\nfunc TestTreeRemove(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\n\tvalue, removed := tree.Remove(\"key1\")\n\tif !removed || value != \"value1\" || tree.Size() != 0 {\n\t\tt.Error(\"Expected Remove to remove key-value pair\")\n\t}\n\n\t_, removed = tree.Remove(\"key2\")\n\tif removed {\n\t\tt.Error(\"Expected Remove to return false for non-existent key\")\n\t}\n}\n\nfunc TestTreeIterate(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\ttree.Set(\"key3\", \"value3\")\n\n\tvar keys []string\n\ttree.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\tkeys = append(keys, key)\n\t\treturn false\n\t})\n\n\texpectedKeys := []string{\"key1\", \"key2\", \"key3\"}\n\tif !slicesEqual(keys, expectedKeys) {\n\t\tt.Errorf(\"Expected keys %v, got %v\", expectedKeys, keys)\n\t}\n}\n\nfunc TestTreeReverseIterate(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\ttree.Set(\"key3\", \"value3\")\n\n\tvar keys []string\n\ttree.ReverseIterate(\"\", \"\", func(key string, value any) bool {\n\t\tkeys = append(keys, key)\n\t\treturn false\n\t})\n\n\texpectedKeys := []string{\"key3\", \"key2\", \"key1\"}\n\tif !slicesEqual(keys, expectedKeys) {\n\t\tt.Errorf(\"Expected keys %v, got %v\", expectedKeys, keys)\n\t}\n}\n\nfunc TestTreeIterateByOffset(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\ttree.Set(\"key3\", \"value3\")\n\n\tvar keys []string\n\ttree.IterateByOffset(1, 2, func(key string, value any) bool {\n\t\tkeys = append(keys, key)\n\t\treturn false\n\t})\n\n\texpectedKeys := []string{\"key2\", \"key3\"}\n\tif !slicesEqual(keys, expectedKeys) {\n\t\tt.Errorf(\"Expected keys %v, got %v\", expectedKeys, keys)\n\t}\n}\n\nfunc TestTreeReverseIterateByOffset(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\ttree.Set(\"key3\", \"value3\")\n\n\tvar keys []string\n\ttree.ReverseIterateByOffset(1, 2, func(key string, value any) bool {\n\t\tkeys = append(keys, key)\n\t\treturn false\n\t})\n\n\texpectedKeys := []string{\"key2\", \"key1\"}\n\tif !slicesEqual(keys, expectedKeys) {\n\t\tt.Errorf(\"Expected keys %v, got %v\", expectedKeys, keys)\n\t}\n}\n\n// Verify that Tree implements avl.ITree\n// var _ avl.ITree = (*Tree)(nil) // TODO: fix gnovm bug: ./examples/gno.land/p/moul/cow: test pkg: panic: gno.land/p/moul/cow/tree_test.gno:166:5: name avl not defined in fileset with files [node.gno tree.gno node_test.gno tree_test.gno]:\n\nfunc TestCopyOnWrite(t *testing.T) {\n\t// Create original tree\n\toriginal := NewTree()\n\toriginal.Set(\"A\", 1)\n\toriginal.Set(\"B\", 2)\n\toriginal.Set(\"C\", 3)\n\n\t// Create a clone\n\tclone := original.Clone()\n\n\t// Modify clone\n\tclone.Set(\"B\", 20)\n\tclone.Set(\"D\", 4)\n\n\t// Verify original is unchanged\n\tif val := original.Get(\"B\"); val != 2 {\n\t\tt.Errorf(\"Original tree was modified: expected B=2, got B=%v\", val)\n\t}\n\tif original.Has(\"D\") {\n\t\tt.Error(\"Original tree was modified: found key D\")\n\t}\n\n\t// Verify clone has new values\n\tif val := clone.Get(\"B\"); val != 20 {\n\t\tt.Errorf(\"Clone not updated: expected B=20, got B=%v\", val)\n\t}\n\tif val := clone.Get(\"D\"); val != 4 {\n\t\tt.Errorf(\"Clone not updated: expected D=4, got D=%v\", val)\n\t}\n}\n\nfunc TestCopyOnWriteEdgeCases(t *testing.T) {\n\tt.Run(\"nil tree clone\", func(t *testing.T) {\n\t\tvar original *Tree\n\t\tclone := original.Clone()\n\t\tif clone != nil {\n\t\t\tt.Error(\"Expected nil clone from nil tree\")\n\t\t}\n\t})\n\n\tt.Run(\"empty tree clone\", func(t *testing.T) {\n\t\toriginal := NewTree()\n\t\tclone := original.Clone()\n\n\t\t// Modify clone\n\t\tclone.Set(\"A\", 1)\n\n\t\tif original.Size() != 0 {\n\t\t\tt.Error(\"Original empty tree was modified\")\n\t\t}\n\t\tif clone.Size() != 1 {\n\t\t\tt.Error(\"Clone was not modified\")\n\t\t}\n\t})\n\n\tt.Run(\"multiple clones\", func(t *testing.T) {\n\t\toriginal := NewTree()\n\t\toriginal.Set(\"A\", 1)\n\t\toriginal.Set(\"B\", 2)\n\n\t\t// Create multiple clones\n\t\tclone1 := original.Clone()\n\t\tclone2 := original.Clone()\n\t\tclone3 := clone1.Clone()\n\n\t\t// Modify each clone differently\n\t\tclone1.Set(\"A\", 10)\n\t\tclone2.Set(\"B\", 20)\n\t\tclone3.Set(\"C\", 30)\n\n\t\t// Check original remains unchanged\n\t\tif val := original.Get(\"A\"); val != 1 {\n\t\t\tt.Errorf(\"Original modified: expected A=1, got A=%v\", val)\n\t\t}\n\t\tif val := original.Get(\"B\"); val != 2 {\n\t\t\tt.Errorf(\"Original modified: expected B=2, got B=%v\", val)\n\t\t}\n\n\t\t// Verify each clone has correct values\n\t\tif val := clone1.Get(\"A\"); val != 10 {\n\t\t\tt.Errorf(\"Clone1 incorrect: expected A=10, got A=%v\", val)\n\t\t}\n\t\tif val := clone2.Get(\"B\"); val != 20 {\n\t\t\tt.Errorf(\"Clone2 incorrect: expected B=20, got B=%v\", val)\n\t\t}\n\t\tif val := clone3.Get(\"C\"); val != 30 {\n\t\t\tt.Errorf(\"Clone3 incorrect: expected C=30, got C=%v\", val)\n\t\t}\n\t})\n\n\tt.Run(\"clone after removal\", func(t *testing.T) {\n\t\toriginal := NewTree()\n\t\toriginal.Set(\"A\", 1)\n\t\toriginal.Set(\"B\", 2)\n\t\toriginal.Set(\"C\", 3)\n\n\t\t// Remove a node and then clone\n\t\toriginal.Remove(\"B\")\n\t\tclone := original.Clone()\n\n\t\t// Modify clone\n\t\tclone.Set(\"B\", 20)\n\n\t\t// Verify original state\n\t\tif original.Has(\"B\") {\n\t\t\tt.Error(\"Original tree should not have key B\")\n\t\t}\n\n\t\t// Verify clone state\n\t\tif val := clone.Get(\"B\"); val != 20 {\n\t\t\tt.Errorf(\"Clone incorrect: expected B=20, got B=%v\", val)\n\t\t}\n\t})\n\n\tt.Run(\"concurrent modifications\", func(t *testing.T) {\n\t\toriginal := NewTree()\n\t\toriginal.Set(\"A\", 1)\n\t\toriginal.Set(\"B\", 2)\n\n\t\tclone1 := original.Clone()\n\t\tclone2 := original.Clone()\n\n\t\t// Modify same key in different clones\n\t\tclone1.Set(\"B\", 20)\n\t\tclone2.Set(\"B\", 30)\n\n\t\t// Each clone should have its own value\n\t\tif val := clone1.Get(\"B\"); val != 20 {\n\t\t\tt.Errorf(\"Clone1 incorrect: expected B=20, got B=%v\", val)\n\t\t}\n\t\tif val := clone2.Get(\"B\"); val != 30 {\n\t\t\tt.Errorf(\"Clone2 incorrect: expected B=30, got B=%v\", val)\n\t\t}\n\t})\n\n\tt.Run(\"deep tree modifications\", func(t *testing.T) {\n\t\toriginal := NewTree()\n\t\t// Create a deeper tree\n\t\tkeys := []string{\"M\", \"F\", \"T\", \"B\", \"H\", \"P\", \"Z\"}\n\t\tfor _, k := range keys {\n\t\t\toriginal.Set(k, k)\n\t\t}\n\n\t\tclone := original.Clone()\n\n\t\t// Modify a deep node\n\t\tclone.Set(\"H\", \"modified\")\n\n\t\t// Check original remains unchanged\n\t\tif val := original.Get(\"H\"); val != \"H\" {\n\t\t\tt.Errorf(\"Original modified: expected H='H', got H=%v\", val)\n\t\t}\n\n\t\t// Verify clone modification\n\t\tif val := clone.Get(\"H\"); val != \"modified\" {\n\t\t\tt.Errorf(\"Clone incorrect: expected H='modified', got H=%v\", val)\n\t\t}\n\t})\n\n\tt.Run(\"rebalancing test\", func(t *testing.T) {\n\t\toriginal := NewTree()\n\t\t// Insert nodes that will cause rotations\n\t\tkeys := []string{\"A\", \"B\", \"C\", \"D\", \"E\"}\n\t\tfor _, k := range keys {\n\t\t\toriginal.Set(k, k)\n\t\t}\n\n\t\tclone := original.Clone()\n\n\t\t// Add more nodes to clone to trigger rebalancing\n\t\tclone.Set(\"F\", \"F\")\n\t\tclone.Set(\"G\", \"G\")\n\n\t\t// Verify original structure remains unchanged\n\t\toriginalKeys := collectKeys(original)\n\t\texpectedOriginal := []string{\"A\", \"B\", \"C\", \"D\", \"E\"}\n\t\tif !slicesEqual(originalKeys, expectedOriginal) {\n\t\t\tt.Errorf(\"Original tree structure changed: got %v, want %v\", originalKeys, expectedOriginal)\n\t\t}\n\n\t\t// Verify clone has all keys\n\t\tcloneKeys := collectKeys(clone)\n\t\texpectedClone := []string{\"A\", \"B\", \"C\", \"D\", \"E\", \"F\", \"G\"}\n\t\tif !slicesEqual(cloneKeys, expectedClone) {\n\t\t\tt.Errorf(\"Clone tree structure incorrect: got %v, want %v\", cloneKeys, expectedClone)\n\t\t}\n\t})\n\n\tt.Run(\"value mutation test\", func(t *testing.T) {\n\t\ttype MutableValue struct {\n\t\t\tData string\n\t\t}\n\n\t\toriginal := NewTree()\n\t\tmutable := \u0026MutableValue{Data: \"original\"}\n\t\toriginal.Set(\"key\", mutable)\n\n\t\tclone := original.Clone()\n\n\t\t// Modify the mutable value\n\t\tmutable.Data = \"modified\"\n\n\t\t// Both original and clone should see the modification\n\t\t// because we're not deep copying values\n\t\torigVal := original.Get(\"key\")\n\t\tcloneVal := clone.Get(\"key\")\n\n\t\tif origVal.(*MutableValue).Data != \"modified\" {\n\t\t\tt.Error(\"Original value not modified as expected\")\n\t\t}\n\t\tif cloneVal.(*MutableValue).Data != \"modified\" {\n\t\t\tt.Error(\"Clone value not modified as expected\")\n\t\t}\n\t})\n}\n\n// Helper function to collect all keys in order\nfunc collectKeys(tree *Tree) []string {\n\tvar keys []string\n\ttree.Iterate(\"\", \"\", func(key string, _ any) bool {\n\t\tkeys = append(keys, key)\n\t\treturn false\n\t})\n\treturn keys\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"FvX+XnDCPPwpwPFjPDFIJXuSszAkOsbjuoxeu3KunyFesgmr5LpVdZxdoLiZcpHt1QfzXM1qjq9l5pJL2TIxag=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","package":{"name":"debug","path":"gno.land/p/moul/debug","files":[{"name":"debug.gno","body":"// Package debug provides utilities for logging and displaying debug information\n// within Gno realms. It supports conditional rendering of logs and metadata,\n// toggleable via query parameters.\n//\n// Key Features:\n// - Log collection and display using Markdown formatting.\n// - Metadata display for realm path, address, and height.\n// - Collapsible debug section for cleaner presentation.\n// - Query-based debug toggle using `?debug=1`.\npackage debug\n\nimport (\n\t\"chain/runtime\"\n\t\"chain/runtime/unsafe\"\n\t\"time\"\n\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/moul/mdtable/v0\"\n\t\"gno.land/p/moul/realmpath/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Debug encapsulates debug information, including logs and metadata.\ntype Debug struct {\n\tLogs         []string\n\tHideMetadata bool\n}\n\n// Log appends a new line of debug information to the Logs slice.\nfunc (d *Debug) Log(line string) {\n\td.Logs = append(d.Logs, line)\n}\n\n// Render generates the debug content as a collapsible Markdown section.\n// It conditionally renders logs and metadata if enabled via the `?debug=1` query parameter.\nfunc (d Debug) Render(path string) string {\n\tif realmpath.Parse(path).Query.Get(\"debug\") != \"1\" {\n\t\treturn \"\"\n\t}\n\n\tvar content string\n\n\tif d.Logs != nil {\n\t\tcontent += md.H3(\"Logs\")\n\t\tcontent += md.BulletList(d.Logs)\n\t}\n\n\tif !d.HideMetadata {\n\t\tcontent += md.H3(\"Metadata\")\n\t\ttable := mdtable.Table{\n\t\t\tHeaders: []string{\"Key\", \"Value\"},\n\t\t}\n\t\ttable.Append([]string{\"`unsafe.CurrentRealm().PkgPath()`\", string(unsafe.CurrentRealm().PkgPath())})\n\t\ttable.Append([]string{\"`unsafe.CurrentRealm().Address()`\", string(unsafe.CurrentRealm().Address())})\n\t\ttable.Append([]string{\"`unsafe.PreviousRealm().PkgPath()`\", string(unsafe.PreviousRealm().PkgPath())})\n\t\ttable.Append([]string{\"`unsafe.PreviousRealm().Address()`\", string(unsafe.PreviousRealm().Address())})\n\t\ttable.Append([]string{\"`runtime.ChainHeight()`\", ufmt.Sprintf(\"%d\", runtime.ChainHeight())})\n\t\ttable.Append([]string{\"`time.Now().Format(time.RFC3339)`\", time.Now().Format(time.RFC3339)})\n\t\tcontent += table.String()\n\t}\n\n\tif content == \"\" {\n\t\treturn \"\"\n\t}\n\n\treturn md.CollapsibleSection(\"debug\", content)\n}\n\n// Render displays metadata about the current realm but does not display logs.\n// This function uses a default Debug struct with metadata enabled and no logs.\nfunc Render(path string) string {\n\treturn Debug{}.Render(path)\n}\n\n// IsEnabled checks if the `?debug=1` query parameter is set in the given path.\n// Returns true if debugging is enabled, otherwise false.\nfunc IsEnabled(path string) bool {\n\treq := realmpath.Parse(path)\n\treturn req.Query.Get(\"debug\") == \"1\"\n}\n\n// ToggleURL modifies the given path's query string to toggle the `?debug=1` parameter.\n// If debugging is currently enabled, it removes the parameter.\n// If debugging is disabled, it adds the parameter.\nfunc ToggleURL(path string) string {\n\treq := realmpath.Parse(path)\n\tif IsEnabled(path) {\n\t\treq.Query.Del(\"debug\")\n\t} else {\n\t\treq.Query.Add(\"debug\", \"1\")\n\t}\n\treturn req.String()\n}\n"},{"name":"debug_test.gno","body":"package debug\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nfunc TestPackage(t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1user\"))\n\n\ttestPackage(t)\n}\n\nfunc testPackage(t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/test/test\"))\n\n\t// no debug\n\tgot := Render(\"\")\n\texpected := ``\n\tuassert.Equal(t, expected, got)\n\n\t// debug without logs\n\tgot = Render(\"?debug=1\")\n\texpected = `\u003cdetails\u003e\u003csummary\u003edebug\u003c/summary\u003e\n\n### Metadata\n| Key | Value |\n| --- | --- |\n| ±unsafe.CurrentRealm().PkgPath()± | gno.land/r/test/test |\n| ±unsafe.CurrentRealm().Address()± | g1z7fga7u94pdmamlvcrtvsfwxgsye0qv3rres7n |\n| ±unsafe.PreviousRealm().PkgPath()± |  |\n| ±unsafe.PreviousRealm().Address()± | g1user |\n| ±runtime.ChainHeight()± | 123 |\n| ±time.Now().Format(time.RFC3339)± | 2009-02-13T23:31:30Z |\n\n\u003c/details\u003e\n`\n\texpected = strings.ReplaceAll(expected, \"±\", \"`\")\n\n\tprintln(\"###################\")\n\tprintln(got)\n\tprintln(\"###################\")\n\tprintln(expected)\n\tprintln(\"###################\")\n\n\tuassert.Equal(t, expected, got)\n\n\t// debug with logs\n\tvar d Debug\n\td.Log(\"hello world!\")\n\td.Log(\"foobar\")\n\tgot = d.Render(\"?debug=1\")\n\texpected = `\u003cdetails\u003e\u003csummary\u003edebug\u003c/summary\u003e\n\n### Logs\n- hello world!\n- foobar\n### Metadata\n| Key | Value |\n| --- | --- |\n| ±unsafe.CurrentRealm().PkgPath()± | gno.land/r/test/test |\n| ±unsafe.CurrentRealm().Address()± | g1z7fga7u94pdmamlvcrtvsfwxgsye0qv3rres7n |\n| ±unsafe.PreviousRealm().PkgPath()± |  |\n| ±unsafe.PreviousRealm().Address()± | g1user |\n| ±runtime.ChainHeight()± | 123 |\n| ±time.Now().Format(time.RFC3339)± | 2009-02-13T23:31:30Z |\n\n\u003c/details\u003e\n`\n\texpected = strings.ReplaceAll(expected, \"±\", \"`\")\n\tuassert.Equal(t, got, expected)\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/debug\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"F0Fli7yTNxQZJfGLzM9or9bwTE+80wbut3e1ZhMt/70tXhDXuvn3+JqPRzk8pwcKIYbeCT5p7o4vtEwrqOy+xQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","package":{"name":"deque","path":"gno.land/p/moul/deque","files":[{"name":"deque.gno","body":"// Package deque provides a doubly-linked deque with optional size limits,\n// optimized for minimal storage updates and O(1) operations in Gno environments.\n//\n// This implementation uses a doubly-linked list structure that provides:\n// - O(1) push back operations\n// - O(1) pop front operations\n// - O(1) access to first/last elements\n// - No array resizing or slice shifting\n// - Minimal allocations (one node per element)\n// - Minimal storage updates (typically 1-2 pointer updates per operation)\n// - Optional max-size eviction policy\n//\n// Optimized for Gno blockchain storage: each operation updates only specific nodes\n// rather than entire data structures, making it ideal for storage-efficient lists\n// that need simple read operations like first/last access, size checks, list\n// enumeration, and iteration with minimal storage update overhead.\n//\n// Example usage:\n//\n//\t// Unbounded deque\n//\td := deque.New()\n//\td.PushBack(\"a\", \"b\", \"c\")\n//\n//\t// Bounded deque with automatic eviction\n//\td = deque.NewBounded(3)\n//\td.PushBack(\"a\", \"b\", \"c\", \"d\")  // \"a\" gets evicted\n//\n//\tfirst := d.PopFront()  // Returns \"b\"\n//\tlast := d.Last()       // Returns \"d\"\n//\n//\t// Iterate forward\n//\titer := d.All()\n//\titer(func(val any) bool {\n//\t    println(val)\n//\t    return true // continue\n//\t})\n//\n//\t// Iterate backward\n//\titer = d.Backward()\n//\titer(func(val any) bool {\n//\t    println(val)\n//\t    return true // continue\n//\t})\npackage deque\n\n// Node represents a single element in the doubly-linked list\ntype Node struct {\n\tvalue any\n\tprev  *Node\n\tnext  *Node\n}\n\n// Deque represents a doubly-linked deque with optional size limits\ntype Deque struct {\n\thead *Node\n\ttail *Node\n\tsize int\n\tmax  int // 0 = unlimited\n}\n\n// New creates a new unbounded deque.\nfunc New() *Deque {\n\treturn \u0026Deque{\n\t\tmax: 0, // unlimited\n\t}\n}\n\n// NewBounded creates a new bounded deque with the specified maximum size.\n// When the deque exceeds maxSize, elements are removed from the front.\nfunc NewBounded(maxSize int) *Deque {\n\tif maxSize \u003c= 0 {\n\t\tmaxSize = 1\n\t}\n\treturn \u0026Deque{\n\t\tmax: maxSize,\n\t}\n}\n\n// PushBack adds one or more elements to the back of the deque.\n// If the deque exceeds maxSize, elements are removed from the front.\nfunc (d *Deque) PushBack(items ...any) {\n\tfor _, item := range items {\n\t\td.pushBackSingle(item)\n\t}\n}\n\n// pushBackSingle adds a single element to the back\nfunc (d *Deque) pushBackSingle(item any) {\n\tnode := \u0026Node{value: item}\n\n\tif d.tail == nil {\n\t\t// First element\n\t\td.head = node\n\t\td.tail = node\n\t} else {\n\t\t// Link to existing tail\n\t\td.tail.next = node\n\t\tnode.prev = d.tail\n\t\td.tail = node\n\t}\n\n\td.size++\n\n\t// Handle size limit by removing from front\n\tif d.max \u003e 0 \u0026\u0026 d.size \u003e d.max {\n\t\td.popFrontNode()\n\t}\n}\n\n// PopFront removes and returns the front element.\n// Returns nil if the deque is empty.\nfunc (d *Deque) PopFront() any {\n\tif d.head == nil {\n\t\treturn nil\n\t}\n\n\tvalue := d.head.value\n\td.popFrontNode()\n\treturn value\n}\n\n// popFrontNode removes the front node\nfunc (d *Deque) popFrontNode() {\n\tif d.head == nil {\n\t\treturn\n\t}\n\n\tif d.head == d.tail {\n\t\t// Only one element\n\t\td.head = nil\n\t\td.tail = nil\n\t} else {\n\t\t// Move head forward\n\t\td.head = d.head.next\n\t\td.head.prev = nil\n\t}\n\n\td.size--\n}\n\n// PopBack removes and returns the back element.\n// Returns nil if the deque is empty.\nfunc (d *Deque) PopBack() any {\n\tif d.tail == nil {\n\t\treturn nil\n\t}\n\n\tvalue := d.tail.value\n\td.popBackNode()\n\treturn value\n}\n\n// popBackNode removes the back node\nfunc (d *Deque) popBackNode() {\n\tif d.tail == nil {\n\t\treturn\n\t}\n\n\tif d.head == d.tail {\n\t\t// Only one element\n\t\td.head = nil\n\t\td.tail = nil\n\t} else {\n\t\t// Move tail backward\n\t\td.tail = d.tail.prev\n\t\td.tail.next = nil\n\t}\n\n\td.size--\n}\n\n// PushFront adds one or more elements to the front of the deque.\n// If bounded and size limit is exceeded, elements are removed from the back.\nfunc (d *Deque) PushFront(items ...any) {\n\tfor _, item := range items {\n\t\td.pushFrontSingle(item)\n\t}\n}\n\n// pushFrontSingle adds a single element to the front\nfunc (d *Deque) pushFrontSingle(item any) {\n\tnode := \u0026Node{value: item}\n\n\tif d.head == nil {\n\t\t// First element\n\t\td.head = node\n\t\td.tail = node\n\t} else {\n\t\t// Link to existing head\n\t\td.head.prev = node\n\t\tnode.next = d.head\n\t\td.head = node\n\t}\n\n\td.size++\n\n\t// Handle size limit by removing from back\n\tif d.max \u003e 0 \u0026\u0026 d.size \u003e d.max {\n\t\td.popBackNode()\n\t}\n}\n\n// Size returns the current number of elements\nfunc (d *Deque) Size() int {\n\treturn d.size\n}\n\n// MaxSize returns the maximum size limit (0 = unlimited)\nfunc (d *Deque) MaxSize() int {\n\treturn d.max\n}\n\n// IsEmpty returns true if the deque is empty\nfunc (d *Deque) IsEmpty() bool {\n\treturn d.size == 0\n}\n\n// IsBounded returns true if the deque has a size limit\nfunc (d *Deque) IsBounded() bool {\n\treturn d.max \u003e 0\n}\n\n// First returns the front element without removing it.\n// Returns nil if the deque is empty.\nfunc (d *Deque) First() any {\n\tif d.head == nil {\n\t\treturn nil\n\t}\n\treturn d.head.value\n}\n\n// Last returns the back element without removing it.\n// Returns nil if the deque is empty.\nfunc (d *Deque) Last() any {\n\tif d.tail == nil {\n\t\treturn nil\n\t}\n\treturn d.tail.value\n}\n\n// Get returns the element at the specified index (0 = front).\n// Returns nil if index is out of bounds.\nfunc (d *Deque) Get(index int) any {\n\tif index \u003c 0 || index \u003e= d.size {\n\t\treturn nil\n\t}\n\n\tnode := d.head\n\tfor i := 0; i \u003c index; i++ {\n\t\tnode = node.next\n\t}\n\treturn node.value\n}\n\n// List returns all elements as a slice, ordered from front to back\nfunc (d *Deque) List() []any {\n\tif d.size == 0 {\n\t\treturn nil\n\t}\n\n\tresult := make([]any, d.size)\n\tnode := d.head\n\tfor i := 0; i \u003c d.size; i++ {\n\t\tresult[i] = node.value\n\t\tnode = node.next\n\t}\n\treturn result\n}\n\n// Enumerate calls fn for each element with its index (0-based from front)\nfunc (d *Deque) Enumerate(fn func(index int, value any) bool) {\n\tnode := d.head\n\tfor i := 0; i \u003c d.size; i++ {\n\t\tif fn(i, node.value) {\n\t\t\tbreak\n\t\t}\n\t\tnode = node.next\n\t}\n}\n\n// Clear removes all elements from the deque\nfunc (d *Deque) Clear() {\n\td.head = nil\n\td.tail = nil\n\td.size = 0\n}\n\n// SetMaxSize changes the maximum size limit.\n// If the new limit is smaller than current size, elements are removed from front.\nfunc (d *Deque) SetMaxSize(maxSize int) {\n\td.max = maxSize\n\n\tif maxSize \u003e 0 {\n\t\tfor d.size \u003e maxSize {\n\t\t\td.popFrontNode()\n\t\t}\n\t}\n}\n\n// All returns an iterator function for forward traversal (Go 1.23+ compatible)\n// Note: range-over-func syntax is not yet supported in Gno, use iter() directly\nfunc (d *Deque) All() func(yield func(any) bool) {\n\treturn func(yield func(any) bool) {\n\t\tcurrent := d.head\n\t\tfor current != nil {\n\t\t\tif !yield(current.value) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tcurrent = current.next\n\t\t}\n\t}\n}\n\n// Backward returns an iterator function for backward traversal (Go 1.23+ compatible)\n// Note: range-over-func syntax is not yet supported in Gno, use iter() directly\nfunc (d *Deque) Backward() func(yield func(any) bool) {\n\treturn func(yield func(any) bool) {\n\t\tcurrent := d.tail\n\t\tfor current != nil {\n\t\t\tif !yield(current.value) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tcurrent = current.prev\n\t\t}\n\t}\n}\n"},{"name":"deque_test.gno","body":"package deque\n\nimport \"testing\"\n\nfunc TestNew(t *testing.T) {\n\td := New()\n\tif d.Size() != 0 {\n\t\tt.Errorf(\"Expected size 0, got %d\", d.Size())\n\t}\n\tif d.MaxSize() != 0 {\n\t\tt.Errorf(\"Expected max size 0 (unlimited), got %d\", d.MaxSize())\n\t}\n\tif d.IsBounded() {\n\t\tt.Error(\"Expected unbounded deque\")\n\t}\n}\n\nfunc TestNewBounded(t *testing.T) {\n\td := NewBounded(5)\n\tif d.MaxSize() != 5 {\n\t\tt.Errorf(\"Expected max size 5, got %d\", d.MaxSize())\n\t}\n\tif d.Size() != 0 {\n\t\tt.Errorf(\"Expected size 0, got %d\", d.Size())\n\t}\n\tif !d.IsBounded() {\n\t\tt.Error(\"Expected bounded deque\")\n\t}\n}\n\nfunc TestPushBackPopFront(t *testing.T) {\n\td := New() // unbounded\n\n\t// Test empty pop\n\tif d.PopFront() != nil {\n\t\tt.Error(\"Expected nil when popping from empty deque\")\n\t}\n\n\td.PushBack(\"a\", \"b\", \"c\")\n\tif d.Size() != 3 {\n\t\tt.Errorf(\"Expected size 3, got %d\", d.Size())\n\t}\n\n\titem := d.PopFront()\n\tif item != \"a\" {\n\t\tt.Errorf(\"Expected 'a', got %v\", item)\n\t}\n\tif d.Size() != 2 {\n\t\tt.Errorf(\"Expected size 2, got %d\", d.Size())\n\t}\n}\n\nfunc TestPushFrontPopBack(t *testing.T) {\n\td := New() // unbounded\n\n\td.PushFront(\"a\", \"b\", \"c\")\n\tif d.Size() != 3 {\n\t\tt.Errorf(\"Expected size 3, got %d\", d.Size())\n\t}\n\n\t// Elements should be in reverse order: [c, b, a]\n\tif d.First() != \"c\" {\n\t\tt.Errorf(\"Expected 'c' as first, got %v\", d.First())\n\t}\n\tif d.Last() != \"a\" {\n\t\tt.Errorf(\"Expected 'a' as last, got %v\", d.Last())\n\t}\n\n\titem := d.PopBack()\n\tif item != \"a\" {\n\t\tt.Errorf(\"Expected 'a', got %v\", item)\n\t}\n\tif d.Size() != 2 {\n\t\tt.Errorf(\"Expected size 2, got %d\", d.Size())\n\t}\n}\n\nfunc TestBoundedEvictionBack(t *testing.T) {\n\td := NewBounded(3)\n\n\t// Fill to capacity\n\td.PushBack(\"a\", \"b\", \"c\")\n\tif d.Size() != 3 {\n\t\tt.Errorf(\"Expected size 3, got %d\", d.Size())\n\t}\n\n\t// Push beyond capacity - should evict \"a\" from front\n\td.PushBack(\"d\")\n\tif d.Size() != 3 {\n\t\tt.Errorf(\"Expected size 3, got %d\", d.Size())\n\t}\n\tif d.First() != \"b\" {\n\t\tt.Errorf(\"Expected 'b' (oldest after eviction), got %v\", d.First())\n\t}\n\tif d.Last() != \"d\" {\n\t\tt.Errorf(\"Expected 'd' (newest), got %v\", d.Last())\n\t}\n}\n\nfunc TestBoundedEvictionFront(t *testing.T) {\n\td := NewBounded(3)\n\n\t// Fill to capacity\n\td.PushBack(\"a\", \"b\", \"c\")\n\n\t// Push to front beyond capacity - should evict \"c\" from back\n\td.PushFront(\"x\")\n\tif d.Size() != 3 {\n\t\tt.Errorf(\"Expected size 3, got %d\", d.Size())\n\t}\n\tif d.First() != \"x\" {\n\t\tt.Errorf(\"Expected 'x' (newest at front), got %v\", d.First())\n\t}\n\tif d.Last() != \"b\" {\n\t\tt.Errorf(\"Expected 'b' (oldest after back eviction), got %v\", d.Last())\n\t}\n}\n\nfunc TestFirstLast(t *testing.T) {\n\td := New()\n\n\t// Empty deque\n\tif d.First() != nil {\n\t\tt.Error(\"Expected nil for empty deque first\")\n\t}\n\tif d.Last() != nil {\n\t\tt.Error(\"Expected nil for empty deque last\")\n\t}\n\n\td.PushBack(\"x\", \"y\", \"z\")\n\tif d.First() != \"x\" {\n\t\tt.Errorf(\"Expected 'x', got %v\", d.First())\n\t}\n\tif d.Last() != \"z\" {\n\t\tt.Errorf(\"Expected 'z', got %v\", d.Last())\n\t}\n}\n\nfunc TestGet(t *testing.T) {\n\td := New()\n\td.PushBack(\"a\", \"b\", \"c\")\n\n\tif d.Get(0) != \"a\" {\n\t\tt.Errorf(\"Expected 'a' at index 0, got %v\", d.Get(0))\n\t}\n\tif d.Get(1) != \"b\" {\n\t\tt.Errorf(\"Expected 'b' at index 1, got %v\", d.Get(1))\n\t}\n\tif d.Get(2) != \"c\" {\n\t\tt.Errorf(\"Expected 'c' at index 2, got %v\", d.Get(2))\n\t}\n\tif d.Get(3) != nil {\n\t\tt.Error(\"Expected nil for out-of-bounds index\")\n\t}\n\tif d.Get(-1) != nil {\n\t\tt.Error(\"Expected nil for negative index\")\n\t}\n}\n\nfunc TestList(t *testing.T) {\n\td := New()\n\n\t// Empty deque\n\tlist := d.List()\n\tif list != nil {\n\t\tt.Errorf(\"Expected nil for empty deque, got %v\", list)\n\t}\n\n\td.PushBack(\"x\", \"y\", \"z\")\n\tlist = d.List()\n\tif len(list) != 3 {\n\t\tt.Errorf(\"Expected length 3, got %d\", len(list))\n\t}\n\tif list[0] != \"x\" || list[1] != \"y\" || list[2] != \"z\" {\n\t\tt.Errorf(\"Expected [x, y, z], got %v\", list)\n\t}\n}\n\nfunc TestEnumerate(t *testing.T) {\n\td := New()\n\td.PushBack(\"a\", \"b\", \"c\")\n\n\tvar enumerated []string\n\tvar indices []int\n\n\td.Enumerate(func(index int, item any) bool {\n\t\tindices = append(indices, index)\n\t\tenumerated = append(enumerated, item.(string))\n\t\treturn false // continue\n\t})\n\n\tif len(enumerated) != 3 {\n\t\tt.Errorf(\"Expected 3 enumerated items, got %d\", len(enumerated))\n\t}\n\tif enumerated[0] != \"a\" || enumerated[1] != \"b\" || enumerated[2] != \"c\" {\n\t\tt.Errorf(\"Expected [a, b, c], got %v\", enumerated)\n\t}\n\tif indices[0] != 0 || indices[1] != 1 || indices[2] != 2 {\n\t\tt.Errorf(\"Expected [0, 1, 2], got %v\", indices)\n\t}\n}\n\nfunc TestEnumerateEarlyStop(t *testing.T) {\n\td := New()\n\td.PushBack(\"a\", \"b\", \"c\")\n\n\tvar enumerated []string\n\n\td.Enumerate(func(index int, item any) bool {\n\t\tenumerated = append(enumerated, item.(string))\n\t\treturn item == \"b\" // stop after b\n\t})\n\n\tif len(enumerated) != 2 {\n\t\tt.Errorf(\"Expected 2 enumerated items, got %d\", len(enumerated))\n\t}\n\tif enumerated[0] != \"a\" || enumerated[1] != \"b\" {\n\t\tt.Errorf(\"Expected [a, b], got %v\", enumerated)\n\t}\n}\n\nfunc TestSetMaxSize(t *testing.T) {\n\td := New()\n\td.PushBack(\"a\", \"b\", \"c\", \"d\", \"e\")\n\n\t// Reduce max size - should trim from front\n\td.SetMaxSize(3)\n\tif d.Size() != 3 {\n\t\tt.Errorf(\"Expected size 3 after reducing max size, got %d\", d.Size())\n\t}\n\tif d.First() != \"c\" {\n\t\tt.Errorf(\"Expected 'c' as first after trim, got %v\", d.First())\n\t}\n\n\t// Increase max size\n\td.SetMaxSize(10)\n\tif d.MaxSize() != 10 {\n\t\tt.Errorf(\"Expected max size 10, got %d\", d.MaxSize())\n\t}\n}\n\nfunc TestClear(t *testing.T) {\n\td := New()\n\td.PushBack(\"a\", \"b\", \"c\")\n\n\td.Clear()\n\n\tif d.Size() != 0 {\n\t\tt.Errorf(\"Expected size 0 after clear, got %d\", d.Size())\n\t}\n\tif d.First() != nil {\n\t\tt.Error(\"Expected nil first after clear\")\n\t}\n\tif d.Last() != nil {\n\t\tt.Error(\"Expected nil last after clear\")\n\t}\n\tif !d.IsEmpty() {\n\t\tt.Error(\"Expected deque to be empty after clear\")\n\t}\n}\n\nfunc TestUnboundedGrowth(t *testing.T) {\n\td := New() // unbounded\n\n\t// Add many items\n\tfor i := 0; i \u003c 100; i++ {\n\t\td.PushBack(i)\n\t}\n\n\tif d.Size() != 100 {\n\t\tt.Errorf(\"Expected size 100, got %d\", d.Size())\n\t}\n\tif d.First() != 0 {\n\t\tt.Errorf(\"Expected 0 as first, got %v\", d.First())\n\t}\n\tif d.Last() != 99 {\n\t\tt.Errorf(\"Expected 99 as last, got %v\", d.Last())\n\t}\n}\n\nfunc TestMixedOperations(t *testing.T) {\n\td := NewBounded(4)\n\n\t// Test mixed push front and back operations\n\td.PushBack(\"b\", \"c\")\n\td.PushFront(\"a\")\n\td.PushBack(\"d\")\n\n\t// Should have [a, b, c, d]\n\tlist := d.List()\n\tif len(list) != 4 || list[0] != \"a\" || list[1] != \"b\" || list[2] != \"c\" || list[3] != \"d\" {\n\t\tt.Errorf(\"Expected [a, b, c, d], got %v\", list)\n\t}\n\n\t// Push beyond capacity\n\td.PushFront(\"x\") // Should evict \"d\" from back\n\n\t// Should have [x, a, b, c]\n\tlist = d.List()\n\tif len(list) != 4 || list[0] != \"x\" || list[1] != \"a\" || list[2] != \"b\" || list[3] != \"c\" {\n\t\tt.Errorf(\"Expected [x, a, b, c], got %v\", list)\n\t}\n\n\t// Mixed pop operations\n\tfront := d.PopFront() // \"x\"\n\tback := d.PopBack()   // \"c\"\n\n\tif front != \"x\" {\n\t\tt.Errorf(\"Expected 'x' from front, got %v\", front)\n\t}\n\tif back != \"c\" {\n\t\tt.Errorf(\"Expected 'c' from back, got %v\", back)\n\t}\n\n\t// Should have [a, b]\n\tlist = d.List()\n\tif len(list) != 2 || list[0] != \"a\" || list[1] != \"b\" {\n\t\tt.Errorf(\"Expected [a, b], got %v\", list)\n\t}\n}\n\nfunc TestAllIterator(t *testing.T) {\n\td := New()\n\td.PushBack(\"x\", \"y\", \"z\")\n\n\tvar items []any\n\titer := d.All()\n\titer(func(value any) bool {\n\t\titems = append(items, value)\n\t\treturn true // continue\n\t})\n\n\tif len(items) != 3 {\n\t\tt.Errorf(\"Expected 3 items, got %d\", len(items))\n\t}\n\tif items[0] != \"x\" || items[1] != \"y\" || items[2] != \"z\" {\n\t\tt.Errorf(\"Expected [x, y, z], got %v\", items)\n\t}\n}\n\nfunc TestBackwardIterator(t *testing.T) {\n\td := New()\n\td.PushBack(\"x\", \"y\", \"z\")\n\n\tvar items []any\n\titer := d.Backward()\n\titer(func(value any) bool {\n\t\titems = append(items, value)\n\t\treturn true // continue\n\t})\n\n\tif len(items) != 3 {\n\t\tt.Errorf(\"Expected 3 items, got %d\", len(items))\n\t}\n\tif items[0] != \"z\" || items[1] != \"y\" || items[2] != \"x\" {\n\t\tt.Errorf(\"Expected [z, y, x] (reverse), got %v\", items)\n\t}\n}\n\nfunc TestAllIteratorEarlyBreak(t *testing.T) {\n\td := New()\n\td.PushBack(\"a\", \"b\", \"c\", \"d\", \"e\")\n\n\tvar items []any\n\titer := d.All()\n\titer(func(value any) bool {\n\t\titems = append(items, value)\n\t\tif value == \"c\" {\n\t\t\treturn false // stop\n\t\t}\n\t\treturn true // continue\n\t})\n\n\tif len(items) != 3 {\n\t\tt.Errorf(\"Expected 3 items after break, got %d\", len(items))\n\t}\n\tif items[0] != \"a\" || items[1] != \"b\" || items[2] != \"c\" {\n\t\tt.Errorf(\"Expected [a, b, c], got %v\", items)\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/deque\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"jDYlrwp1ysYusqPtBxDmLuQGyArqbjDlcYCRg95fTtsSQcRO3P29pv8rfu4MA2XujskKYH0wvhPiP2npN8uH7A=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","package":{"name":"errs","path":"gno.land/p/moul/errs","files":[{"name":"errs.gno","body":"// Package errs provides utilities for combining multiple errors.\n// This is a simplified version of https://github.com/uber-go/multierr (MIT License),\n// adapted for the Gno programming language with a focus on core functionality\n// and idiomatic usage patterns.\n//\n// Example usage:\n//\n//\terr1 := doSomething()\n//\terr2 := doSomethingElse()\n//\tif err := errs.Combine(err1, err2); err != nil {\n//\t    return err // Returns combined errors or single error\n//\t}\npackage errs\n\nimport (\n\t\"strings\"\n)\n\n// multiError represents multiple errors combined into one.\ntype multiError struct {\n\terrors []error\n}\n\n// Error implements the error interface by returning a single-line representation\n// of all contained errors, separated by semicolons.\nfunc (m *multiError) Error() string {\n\tif m == nil || len(m.errors) == 0 {\n\t\treturn \"\"\n\t}\n\n\tvar b strings.Builder\n\tfirst := true\n\tfor _, err := range m.errors {\n\t\tif first {\n\t\t\tfirst = false\n\t\t} else {\n\t\t\tb.WriteString(\"; \")\n\t\t}\n\t\tb.WriteString(err.Error())\n\t}\n\treturn b.String()\n}\n\n// String returns a multi-line representation of the error.\nfunc (m *multiError) String() string {\n\tif m == nil || len(m.errors) == 0 {\n\t\treturn \"\"\n\t}\n\n\tvar b strings.Builder\n\tb.WriteString(\"the following errors occurred:\")\n\tfor _, err := range m.errors {\n\t\tb.WriteString(\"\\n -  \")\n\t\tb.WriteString(err.Error())\n\t}\n\treturn b.String()\n}\n\n// Unwrap returns the slice of underlying errors contained in this multiError.\n// Returns nil if the receiver is nil.\nfunc (m *multiError) Unwrap() []error {\n\tif m == nil {\n\t\treturn nil\n\t}\n\treturn m.errors\n}\n\n// Errors extracts the underlying errors from an error interface.\n// If the error is a multiError, it returns its contained errors.\n// If the error is nil, returns nil.\n// If the error is a regular error, returns a slice containing just that error.\nfunc Errors(err error) []error {\n\tif err == nil {\n\t\treturn nil\n\t}\n\n\tif merr, ok := err.(*multiError); ok {\n\t\treturn merr.Unwrap()\n\t}\n\n\treturn []error{err}\n}\n\n// Combine merges multiple errors into a single error efficiently.\n// It handles several cases:\n//   - If all input errors are nil, returns nil\n//   - If there's exactly one non-nil error, returns that error directly\n//   - If there are multiple non-nil errors, returns a multiError containing them\n//   - Flattens any *multiError in the input\nfunc Combine(errs ...error) error {\n\tnonNil := make([]error, 0, len(errs))\n\tfor _, err := range errs {\n\t\tif err == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif m, ok := err.(*multiError); ok \u0026\u0026 m != nil {\n\t\t\tnonNil = append(nonNil, m.Unwrap()...)\n\t\t} else {\n\t\t\tnonNil = append(nonNil, err)\n\t\t}\n\t}\n\n\tswitch len(nonNil) {\n\tcase 0:\n\t\treturn nil\n\tcase 1:\n\t\treturn nonNil[0]\n\tdefault:\n\t\treturn \u0026multiError{errors: nonNil}\n\t}\n}\n"},{"name":"errs_test.gno","body":"package errs\n\nimport (\n\t\"testing\"\n)\n\n// testError is a simple error implementation for testing\ntype testError struct {\n\tmsg string\n}\n\nfunc (e *testError) Error() string {\n\treturn e.msg\n}\n\nfunc newError(msg string) error {\n\treturn \u0026testError{msg: msg}\n}\n\nfunc TestCombine(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\terrors   []error\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"single error\",\n\t\t\terrors:   []error{newError(\"error1\"), nil},\n\t\t\texpected: \"error1\",\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple errors\",\n\t\t\terrors:   []error{newError(\"error1\"), newError(\"error2\"), newError(\"error3\")},\n\t\t\texpected: \"error1; error2; error3\",\n\t\t},\n\t\t{\n\t\t\tname:     \"mixed nil and non-nil\",\n\t\t\terrors:   []error{nil, newError(\"error1\"), nil, newError(\"error2\")},\n\t\t\texpected: \"error1; error2\",\n\t\t},\n\t\t{\n\t\t\tname:     \"both nil (2)\",\n\t\t\terrors:   []error{nil, nil},\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"first nil (2)\",\n\t\t\terrors:   []error{nil, newError(\"error2\")},\n\t\t\texpected: \"error2\",\n\t\t},\n\t\t{\n\t\t\tname:     \"second nil (2)\",\n\t\t\terrors:   []error{newError(\"error1\"), nil},\n\t\t\texpected: \"error1\",\n\t\t},\n\t\t{\n\t\t\tname:     \"both non-nil (2)\",\n\t\t\terrors:   []error{newError(\"error1\"), newError(\"error2\")},\n\t\t\texpected: \"error1; error2\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\terr := Combine(tt.errors...)\n\t\tif tt.expected == \"\" {\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"%s: expected nil error, got %v\", tt.name, err)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif err == nil {\n\t\t\tt.Errorf(\"%s: expected non-nil error\", tt.name)\n\t\t\tcontinue\n\t\t}\n\n\t\tif got := err.Error(); got != tt.expected {\n\t\t\tt.Errorf(\"%s: expected %q, got %q\", tt.name, tt.expected, got)\n\t\t}\n\t}\n}\n\nfunc TestErrors(t *testing.T) {\n\terr1 := newError(\"error1\")\n\terr2 := newError(\"error2\")\n\tcombined := Combine(err1, err2)\n\n\ttests := []struct {\n\t\tname          string\n\t\terr           error\n\t\texpectedCount int\n\t}{\n\t\t{\n\t\t\tname:          \"nil error\",\n\t\t\terr:           nil,\n\t\t\texpectedCount: 0,\n\t\t},\n\t\t{\n\t\t\tname:          \"single error\",\n\t\t\terr:           err1,\n\t\t\texpectedCount: 1,\n\t\t},\n\t\t{\n\t\t\tname:          \"multiple errors\",\n\t\t\terr:           combined,\n\t\t\texpectedCount: 2,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\terrs := Errors(tt.err)\n\t\tif len(errs) != tt.expectedCount {\n\t\t\tt.Errorf(\"%s: expected %d errors, got %d\", tt.name, tt.expectedCount, len(errs))\n\t\t}\n\t}\n}\n\nfunc TestMultiErrorString(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\terrors   []error\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"nil errors\",\n\t\t\terrors:   nil,\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"empty errors\",\n\t\t\terrors:   []error{},\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"single error\",\n\t\t\terrors:   []error{newError(\"error1\")},\n\t\t\texpected: \"the following errors occurred:\\n -  error1\",\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple errors\",\n\t\t\terrors:   []error{newError(\"error1\"), newError(\"error2\")},\n\t\t\texpected: \"the following errors occurred:\\n -  error1\\n -  error2\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmerr := \u0026multiError{errors: tt.errors}\n\t\t\tif got := merr.String(); got != tt.expected {\n\t\t\t\tt.Errorf(\"multiError.String() = %q, want %q\", got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCombineEdgeCases(t *testing.T) {\n\terr1 := newError(\"error1\")\n\terr2 := newError(\"error2\")\n\terr3 := newError(\"error3\")\n\n\tmerr := Combine(err1, err2)\n\n\ttests := []struct {\n\t\tname     string\n\t\terrors   []error\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"append to multiError\",\n\t\t\terrors:   []error{merr, err3},\n\t\t\texpected: \"error1; error2; error3\",\n\t\t},\n\t\t{\n\t\t\tname:     \"prepend to multiError\",\n\t\t\terrors:   []error{err3, merr},\n\t\t\texpected: \"error3; error1; error2\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := Combine(tt.errors...)\n\t\t\tif got := err.Error(); got != tt.expected {\n\t\t\t\tt.Errorf(\"Combine() = %q, want %q\", got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUnwrapNilReceiver(t *testing.T) {\n\tvar merr *multiError\n\terrs := merr.Unwrap()\n\tif errs != nil {\n\t\tt.Errorf(\"Expected nil slice for nil receiver, got %v\", errs)\n\t}\n}\n\nfunc TestMultiErrorUnwrap(t *testing.T) {\n\tvar nilMulti *multiError\n\tif got := nilMulti.Unwrap(); got != nil {\n\t\tt.Errorf(\"Unwrap on nil receiver: expected nil, got %v\", got)\n\t}\n\n\tempty := \u0026multiError{errors: nil}\n\tif got := empty.Unwrap(); got != nil {\n\t\tt.Errorf(\"Unwrap on empty: expected nil, got %v\", got)\n\t}\n\n\tsingle := \u0026multiError{errors: []error{newError(\"foo\")}}\n\tgot := single.Unwrap()\n\tif len(got) != 1 || got[0].Error() != \"foo\" {\n\t\tt.Errorf(\"Unwrap on single: expected [foo], got %v\", got)\n\t}\n\n\tmulti := \u0026multiError{errors: []error{newError(\"foo\"), newError(\"bar\")}}\n\tgot = multi.Unwrap()\n\tif len(got) != 2 || got[0].Error() != \"foo\" || got[1].Error() != \"bar\" {\n\t\tt.Errorf(\"Unwrap on multi: expected [foo bar], got %v\", got)\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/errs\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"IqTqboBoDC83fcSkm5eEJekJWXAwcGxKSPgcZxPn/yMHcTKt9rn5upRWAdsExGwEKQH/jCugYYEAAKMjIqIoww=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","package":{"name":"fp","path":"gno.land/p/moul/fp","files":[{"name":"fp.gno","body":"// Package fp provides functional programming utilities for Gno, enabling\n// transformations, filtering, and other operations on slices of any.\n//\n// Example of chaining operations:\n//\n//\tnumbers := []any{1, 2, 3, 4, 5, 6}\n//\n//\t// Define predicates, mappers and reducers\n//\tisEven := func(v any) bool { return v.(int)%2 == 0 }\n//\tdouble := func(v any) any { return v.(int) * 2 }\n//\tsum := func(a, b any) any { return a.(int) + b.(int) }\n//\n//\t// Chain operations: filter even numbers, double them, then sum\n//\tevenNums := Filter(numbers, isEven)        // [2, 4, 6]\n//\tdoubled := Map(evenNums, double)           // [4, 8, 12]\n//\tresult := Reduce(doubled, sum, 0)          // 24\n//\n//\t// Alternative: group by even/odd, then get even numbers\n//\tbyMod2 := func(v any) any { return v.(int) % 2 }\n//\tgrouped := GroupBy(numbers, byMod2)        // {0: [2,4,6], 1: [1,3,5]}\n//\tevens := grouped[0]                        // [2,4,6]\npackage fp\n\n// Mapper is a function type that maps an element to another element.\ntype Mapper func(any) any\n\n// Predicate is a function type that evaluates a condition on an element.\ntype Predicate func(any) bool\n\n// Reducer is a function type that reduces two elements to a single value.\ntype Reducer func(any, any) any\n\n// Filter filters elements from the slice that satisfy the given predicate.\n//\n// Example:\n//\n//\tnumbers := []any{-1, 0, 1, 2}\n//\tisPositive := func(v any) bool { return v.(int) \u003e 0 }\n//\tresult := Filter(numbers, isPositive) // [1, 2]\nfunc Filter(values []any, fn Predicate) []any {\n\tresult := []any{}\n\tfor _, v := range values {\n\t\tif fn(v) {\n\t\t\tresult = append(result, v)\n\t\t}\n\t}\n\treturn result\n}\n\n// Map applies a function to each element in the slice.\n//\n// Example:\n//\n//\tnumbers := []any{1, 2, 3}\n//\ttoString := func(v any) any { return fmt.Sprintf(\"%d\", v) }\n//\tresult := Map(numbers, toString) // [\"1\", \"2\", \"3\"]\nfunc Map(values []any, fn Mapper) []any {\n\tresult := make([]any, len(values))\n\tfor i, v := range values {\n\t\tresult[i] = fn(v)\n\t}\n\treturn result\n}\n\n// Reduce reduces a slice to a single value by applying a function.\n//\n// Example:\n//\n//\tnumbers := []any{1, 2, 3, 4}\n//\tsum := func(a, b any) any { return a.(int) + b.(int) }\n//\tresult := Reduce(numbers, sum, 0) // 10\nfunc Reduce(values []any, fn Reducer, initial any) any {\n\tacc := initial\n\tfor _, v := range values {\n\t\tacc = fn(acc, v)\n\t}\n\treturn acc\n}\n\n// FlatMap maps each element to a collection and flattens the results.\n//\n// Example:\n//\n//\twords := []any{\"hello\", \"world\"}\n//\tsplit := func(v any) any {\n//\t    chars := []any{}\n//\t    for _, c := range v.(string) {\n//\t        chars = append(chars, string(c))\n//\t    }\n//\t    return chars\n//\t}\n//\tresult := FlatMap(words, split) // [\"h\",\"e\",\"l\",\"l\",\"o\",\"w\",\"o\",\"r\",\"l\",\"d\"]\nfunc FlatMap(values []any, fn Mapper) []any {\n\tresult := []any{}\n\tfor _, v := range values {\n\t\tinner := fn(v).([]any)\n\t\tresult = append(result, inner...)\n\t}\n\treturn result\n}\n\n// All returns true if all elements satisfy the predicate.\n//\n// Example:\n//\n//\tnumbers := []any{2, 4, 6, 8}\n//\tisEven := func(v any) bool { return v.(int)%2 == 0 }\n//\tresult := All(numbers, isEven) // true\nfunc All(values []any, fn Predicate) bool {\n\tfor _, v := range values {\n\t\tif !fn(v) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// Any returns true if at least one element satisfies the predicate.\n//\n// Example:\n//\n//\tnumbers := []any{1, 3, 4, 7}\n//\tisEven := func(v any) bool { return v.(int)%2 == 0 }\n//\tresult := Any(numbers, isEven) // true (4 is even)\nfunc Any(values []any, fn Predicate) bool {\n\tfor _, v := range values {\n\t\tif fn(v) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// None returns true if no elements satisfy the predicate.\n//\n// Example:\n//\n//\tnumbers := []any{1, 3, 5, 7}\n//\tisEven := func(v any) bool { return v.(int)%2 == 0 }\n//\tresult := None(numbers, isEven) // true (no even numbers)\nfunc None(values []any, fn Predicate) bool {\n\tfor _, v := range values {\n\t\tif fn(v) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// Chunk splits a slice into chunks of the given size.\n//\n// Example:\n//\n//\tnumbers := []any{1, 2, 3, 4, 5}\n//\tresult := Chunk(numbers, 2) // [[1,2], [3,4], [5]]\nfunc Chunk(values []any, size int) [][]any {\n\tif size \u003c= 0 {\n\t\treturn nil\n\t}\n\tvar chunks [][]any\n\tfor i := 0; i \u003c len(values); i += size {\n\t\tend := i + size\n\t\tif end \u003e len(values) {\n\t\t\tend = len(values)\n\t\t}\n\t\tchunks = append(chunks, values[i:end])\n\t}\n\treturn chunks\n}\n\n// Find returns the first element that satisfies the predicate and a boolean indicating if an element was found.\n//\n// Example:\n//\n//\tnumbers := []any{1, 2, 3, 4}\n//\tisEven := func(v any) bool { return v.(int)%2 == 0 }\n//\tresult, found := Find(numbers, isEven) // 2, true\nfunc Find(values []any, fn Predicate) (any, bool) {\n\tfor _, v := range values {\n\t\tif fn(v) {\n\t\t\treturn v, true\n\t\t}\n\t}\n\treturn nil, false\n}\n\n// Reverse reverses the order of elements in a slice.\n//\n// Example:\n//\n//\tnumbers := []any{1, 2, 3}\n//\tresult := Reverse(numbers) // [3, 2, 1]\nfunc Reverse(values []any) []any {\n\tresult := make([]any, len(values))\n\tfor i, v := range values {\n\t\tresult[len(values)-1-i] = v\n\t}\n\treturn result\n}\n\n// Zip combines two slices into a slice of pairs. If the slices have different lengths,\n// extra elements from the longer slice are ignored.\n//\n// Example:\n//\n//\ta := []any{1, 2, 3}\n//\tb := []any{\"a\", \"b\", \"c\"}\n//\tresult := Zip(a, b) // [[1,\"a\"], [2,\"b\"], [3,\"c\"]]\nfunc Zip(a, b []any) [][2]any {\n\tlength := min(len(a), len(b))\n\tresult := make([][2]any, length)\n\tfor i := 0; i \u003c length; i++ {\n\t\tresult[i] = [2]any{a[i], b[i]}\n\t}\n\treturn result\n}\n\n// Unzip splits a slice of pairs into two separate slices.\n//\n// Example:\n//\n//\tpairs := [][2]any{{1,\"a\"}, {2,\"b\"}, {3,\"c\"}}\n//\tnumbers, letters := Unzip(pairs) // [1,2,3], [\"a\",\"b\",\"c\"]\nfunc Unzip(pairs [][2]any) ([]any, []any) {\n\ta := make([]any, len(pairs))\n\tb := make([]any, len(pairs))\n\tfor i, pair := range pairs {\n\t\ta[i] = pair[0]\n\t\tb[i] = pair[1]\n\t}\n\treturn a, b\n}\n\n// GroupBy groups elements based on a key returned by a Mapper.\n//\n// Example:\n//\n//\tnumbers := []any{1, 2, 3, 4, 5, 6}\n//\tbyMod3 := func(v any) any { return v.(int) % 3 }\n//\tresult := GroupBy(numbers, byMod3) // {0: [3,6], 1: [1,4], 2: [2,5]}\nfunc GroupBy(values []any, fn Mapper) map[any][]any {\n\tresult := make(map[any][]any)\n\tfor _, v := range values {\n\t\tkey := fn(v)\n\t\tresult[key] = append(result[key], v)\n\t}\n\treturn result\n}\n\n// Flatten flattens a slice of slices into a single slice.\n//\n// Example:\n//\n//\tnested := [][]any{{1,2}, {3,4}, {5}}\n//\tresult := Flatten(nested) // [1,2,3,4,5]\nfunc Flatten(values [][]any) []any {\n\tresult := []any{}\n\tfor _, v := range values {\n\t\tresult = append(result, v...)\n\t}\n\treturn result\n}\n\n// Helper functions\nfunc min(a, b int) int {\n\tif a \u003c b {\n\t\treturn a\n\t}\n\treturn b\n}\n"},{"name":"fp_test.gno","body":"package fp\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nfunc TestMap(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []any\n\t\tfn       func(any) any\n\t\texpected []any\n\t}{\n\t\t{\n\t\t\tname:     \"multiply numbers by 2\",\n\t\t\tinput:    []any{1, 2, 3},\n\t\t\tfn:       func(v any) any { return v.(int) * 2 },\n\t\t\texpected: []any{2, 4, 6},\n\t\t},\n\t\t{\n\t\t\tname:     \"empty slice\",\n\t\t\tinput:    []any{},\n\t\t\tfn:       func(v any) any { return v.(int) * 2 },\n\t\t\texpected: []any{},\n\t\t},\n\t\t{\n\t\t\tname:     \"convert numbers to strings\",\n\t\t\tinput:    []any{1, 2, 3},\n\t\t\tfn:       func(v any) any { return fmt.Sprintf(\"%d\", v.(int)) },\n\t\t\texpected: []any{\"1\", \"2\", \"3\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := Map(tt.input, tt.fn)\n\t\t\tif !equalSlices(result, tt.expected) {\n\t\t\t\tt.Errorf(\"Map failed, expected %v, got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFilter(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []any\n\t\tfn       func(any) bool\n\t\texpected []any\n\t}{\n\t\t{\n\t\t\tname:     \"filter even numbers\",\n\t\t\tinput:    []any{1, 2, 3, 4},\n\t\t\tfn:       func(v any) bool { return v.(int)%2 == 0 },\n\t\t\texpected: []any{2, 4},\n\t\t},\n\t\t{\n\t\t\tname:     \"empty slice\",\n\t\t\tinput:    []any{},\n\t\t\tfn:       func(v any) bool { return v.(int)%2 == 0 },\n\t\t\texpected: []any{},\n\t\t},\n\t\t{\n\t\t\tname:     \"no matches\",\n\t\t\tinput:    []any{1, 3, 5},\n\t\t\tfn:       func(v any) bool { return v.(int)%2 == 0 },\n\t\t\texpected: []any{},\n\t\t},\n\t\t{\n\t\t\tname:     \"all matches\",\n\t\t\tinput:    []any{2, 4, 6},\n\t\t\tfn:       func(v any) bool { return v.(int)%2 == 0 },\n\t\t\texpected: []any{2, 4, 6},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := Filter(tt.input, tt.fn)\n\t\t\tif !equalSlices(result, tt.expected) {\n\t\t\t\tt.Errorf(\"Filter failed, expected %v, got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestReduce(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []any\n\t\tfn       func(any, any) any\n\t\tinitial  any\n\t\texpected any\n\t}{\n\t\t{\n\t\t\tname:     \"sum numbers\",\n\t\t\tinput:    []any{1, 2, 3},\n\t\t\tfn:       func(a, b any) any { return a.(int) + b.(int) },\n\t\t\tinitial:  0,\n\t\t\texpected: 6,\n\t\t},\n\t\t{\n\t\t\tname:     \"empty slice\",\n\t\t\tinput:    []any{},\n\t\t\tfn:       func(a, b any) any { return a.(int) + b.(int) },\n\t\t\tinitial:  0,\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname:     \"concatenate strings\",\n\t\t\tinput:    []any{\"a\", \"b\", \"c\"},\n\t\t\tfn:       func(a, b any) any { return a.(string) + b.(string) },\n\t\t\tinitial:  \"\",\n\t\t\texpected: \"abc\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := Reduce(tt.input, tt.fn, tt.initial)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"Reduce failed, expected %v, got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFlatMap(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []any\n\t\tfn       func(any) any\n\t\texpected []any\n\t}{\n\t\t{\n\t\t\tname:  \"split words into chars\",\n\t\t\tinput: []any{\"go\", \"fn\"},\n\t\t\tfn: func(word any) any {\n\t\t\t\tchars := []any{}\n\t\t\t\tfor _, c := range word.(string) {\n\t\t\t\t\tchars = append(chars, string(c))\n\t\t\t\t}\n\t\t\t\treturn chars\n\t\t\t},\n\t\t\texpected: []any{\"g\", \"o\", \"f\", \"n\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"empty string handling\",\n\t\t\tinput: []any{\"\", \"a\", \"\"},\n\t\t\tfn: func(word any) any {\n\t\t\t\tchars := []any{}\n\t\t\t\tfor _, c := range word.(string) {\n\t\t\t\t\tchars = append(chars, string(c))\n\t\t\t\t}\n\t\t\t\treturn chars\n\t\t\t},\n\t\t\texpected: []any{\"a\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"nil handling\",\n\t\t\tinput: []any{nil, \"a\", nil},\n\t\t\tfn: func(word any) any {\n\t\t\t\tif word == nil {\n\t\t\t\t\treturn []any{}\n\t\t\t\t}\n\t\t\t\treturn []any{word}\n\t\t\t},\n\t\t\texpected: []any{\"a\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"empty slice result\",\n\t\t\tinput: []any{\"\", \"\", \"\"},\n\t\t\tfn: func(word any) any {\n\t\t\t\treturn []any{}\n\t\t\t},\n\t\t\texpected: []any{},\n\t\t},\n\t\t{\n\t\t\tname:  \"nested array flattening\",\n\t\t\tinput: []any{1, 2, 3},\n\t\t\tfn: func(n any) any {\n\t\t\t\treturn []any{n, n}\n\t\t\t},\n\t\t\texpected: []any{1, 1, 2, 2, 3, 3},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := FlatMap(tt.input, tt.fn)\n\t\t\tif !equalSlices(result, tt.expected) {\n\t\t\t\tt.Errorf(\"FlatMap failed, expected %v, got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAllAnyNone(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tinput        []any\n\t\tfn           func(any) bool\n\t\texpectedAll  bool\n\t\texpectedAny  bool\n\t\texpectedNone bool\n\t}{\n\t\t{\n\t\t\tname:         \"all even numbers\",\n\t\t\tinput:        []any{2, 4, 6, 8},\n\t\t\tfn:           func(x any) bool { return x.(int)%2 == 0 },\n\t\t\texpectedAll:  true,\n\t\t\texpectedAny:  true,\n\t\t\texpectedNone: false,\n\t\t},\n\t\t{\n\t\t\tname:         \"no even numbers\",\n\t\t\tinput:        []any{1, 3, 5, 7},\n\t\t\tfn:           func(x any) bool { return x.(int)%2 == 0 },\n\t\t\texpectedAll:  false,\n\t\t\texpectedAny:  false,\n\t\t\texpectedNone: true,\n\t\t},\n\t\t{\n\t\t\tname:         \"mixed even/odd numbers\",\n\t\t\tinput:        []any{1, 2, 3, 4},\n\t\t\tfn:           func(x any) bool { return x.(int)%2 == 0 },\n\t\t\texpectedAll:  false,\n\t\t\texpectedAny:  true,\n\t\t\texpectedNone: false,\n\t\t},\n\t\t{\n\t\t\tname:         \"empty slice\",\n\t\t\tinput:        []any{},\n\t\t\tfn:           func(x any) bool { return x.(int)%2 == 0 },\n\t\t\texpectedAll:  true,  // vacuously true\n\t\t\texpectedAny:  false, // vacuously false\n\t\t\texpectedNone: true,  // vacuously true\n\t\t},\n\t\t{\n\t\t\tname:         \"nil predicate handling\",\n\t\t\tinput:        []any{nil, nil, nil},\n\t\t\tfn:           func(x any) bool { return x == nil },\n\t\t\texpectedAll:  true,\n\t\t\texpectedAny:  true,\n\t\t\texpectedNone: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresultAll := All(tt.input, tt.fn)\n\t\t\tif resultAll != tt.expectedAll {\n\t\t\t\tt.Errorf(\"All failed, expected %v, got %v\", tt.expectedAll, resultAll)\n\t\t\t}\n\n\t\t\tresultAny := Any(tt.input, tt.fn)\n\t\t\tif resultAny != tt.expectedAny {\n\t\t\t\tt.Errorf(\"Any failed, expected %v, got %v\", tt.expectedAny, resultAny)\n\t\t\t}\n\n\t\t\tresultNone := None(tt.input, tt.fn)\n\t\t\tif resultNone != tt.expectedNone {\n\t\t\t\tt.Errorf(\"None failed, expected %v, got %v\", tt.expectedNone, resultNone)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestChunk(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []any\n\t\tsize     int\n\t\texpected [][]any\n\t}{\n\t\t{\n\t\t\tname:     \"normal chunks\",\n\t\t\tinput:    []any{1, 2, 3, 4, 5},\n\t\t\tsize:     2,\n\t\t\texpected: [][]any{{1, 2}, {3, 4}, {5}},\n\t\t},\n\t\t{\n\t\t\tname:     \"empty slice\",\n\t\t\tinput:    []any{},\n\t\t\tsize:     2,\n\t\t\texpected: [][]any{},\n\t\t},\n\t\t{\n\t\t\tname:     \"chunk size equals length\",\n\t\t\tinput:    []any{1, 2, 3},\n\t\t\tsize:     3,\n\t\t\texpected: [][]any{{1, 2, 3}},\n\t\t},\n\t\t{\n\t\t\tname:     \"chunk size larger than length\",\n\t\t\tinput:    []any{1, 2},\n\t\t\tsize:     3,\n\t\t\texpected: [][]any{{1, 2}},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := Chunk(tt.input, tt.size)\n\t\t\tif !equalNestedSlices(result, tt.expected) {\n\t\t\t\tt.Errorf(\"Chunk failed, expected %v, got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFind(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tinput       []any\n\t\tfn          func(any) bool\n\t\texpected    any\n\t\tshouldFound bool\n\t}{\n\t\t{\n\t\t\tname:        \"find first number greater than 2\",\n\t\t\tinput:       []any{1, 2, 3, 4},\n\t\t\tfn:          func(v any) bool { return v.(int) \u003e 2 },\n\t\t\texpected:    3,\n\t\t\tshouldFound: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"empty slice\",\n\t\t\tinput:       []any{},\n\t\t\tfn:          func(v any) bool { return v.(int) \u003e 2 },\n\t\t\texpected:    nil,\n\t\t\tshouldFound: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"no match\",\n\t\t\tinput:       []any{1, 2},\n\t\t\tfn:          func(v any) bool { return v.(int) \u003e 10 },\n\t\t\texpected:    nil,\n\t\t\tshouldFound: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult, found := Find(tt.input, tt.fn)\n\t\t\tif found != tt.shouldFound {\n\t\t\t\tt.Errorf(\"Find failed, expected found=%v, got found=%v\", tt.shouldFound, found)\n\t\t\t}\n\t\t\tif found \u0026\u0026 result != tt.expected {\n\t\t\t\tt.Errorf(\"Find failed, expected %v, got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestReverse(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []any\n\t\texpected []any\n\t}{\n\t\t{\n\t\t\tname:     \"normal sequence\",\n\t\t\tinput:    []any{1, 2, 3, 4},\n\t\t\texpected: []any{4, 3, 2, 1},\n\t\t},\n\t\t{\n\t\t\tname:     \"empty slice\",\n\t\t\tinput:    []any{},\n\t\t\texpected: []any{},\n\t\t},\n\t\t{\n\t\t\tname:     \"single element\",\n\t\t\tinput:    []any{1},\n\t\t\texpected: []any{1},\n\t\t},\n\t\t{\n\t\t\tname:     \"mixed types\",\n\t\t\tinput:    []any{1, \"a\", true, 2.5},\n\t\t\texpected: []any{2.5, true, \"a\", 1},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := Reverse(tt.input)\n\t\t\tif !equalSlices(result, tt.expected) {\n\t\t\t\tt.Errorf(\"Reverse failed, expected %v, got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestZipUnzip(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\ta           []any\n\t\tb           []any\n\t\texpectedZip [][2]any\n\t\texpectedA   []any\n\t\texpectedB   []any\n\t}{\n\t\t{\n\t\t\tname:        \"normal case\",\n\t\t\ta:           []any{1, 2, 3},\n\t\t\tb:           []any{\"a\", \"b\", \"c\"},\n\t\t\texpectedZip: [][2]any{{1, \"a\"}, {2, \"b\"}, {3, \"c\"}},\n\t\t\texpectedA:   []any{1, 2, 3},\n\t\t\texpectedB:   []any{\"a\", \"b\", \"c\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"empty slices\",\n\t\t\ta:           []any{},\n\t\t\tb:           []any{},\n\t\t\texpectedZip: [][2]any{},\n\t\t\texpectedA:   []any{},\n\t\t\texpectedB:   []any{},\n\t\t},\n\t\t{\n\t\t\tname:        \"different lengths - a shorter\",\n\t\t\ta:           []any{1, 2},\n\t\t\tb:           []any{\"a\", \"b\", \"c\"},\n\t\t\texpectedZip: [][2]any{{1, \"a\"}, {2, \"b\"}},\n\t\t\texpectedA:   []any{1, 2},\n\t\t\texpectedB:   []any{\"a\", \"b\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"different lengths - b shorter\",\n\t\t\ta:           []any{1, 2, 3},\n\t\t\tb:           []any{\"a\"},\n\t\t\texpectedZip: [][2]any{{1, \"a\"}},\n\t\t\texpectedA:   []any{1},\n\t\t\texpectedB:   []any{\"a\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"mixed types\",\n\t\t\ta:           []any{1, true, \"x\"},\n\t\t\tb:           []any{2.5, false, \"y\"},\n\t\t\texpectedZip: [][2]any{{1, 2.5}, {true, false}, {\"x\", \"y\"}},\n\t\t\texpectedA:   []any{1, true, \"x\"},\n\t\t\texpectedB:   []any{2.5, false, \"y\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tzipped := Zip(tt.a, tt.b)\n\t\t\tif len(zipped) != len(tt.expectedZip) {\n\t\t\t\tt.Errorf(\"Zip failed, expected length %v, got %v\", len(tt.expectedZip), len(zipped))\n\t\t\t}\n\t\t\tfor i, pair := range zipped {\n\t\t\t\tif pair[0] != tt.expectedZip[i][0] || pair[1] != tt.expectedZip[i][1] {\n\t\t\t\t\tt.Errorf(\"Zip failed at index %d, expected %v, got %v\", i, tt.expectedZip[i], pair)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tunzippedA, unzippedB := Unzip(zipped)\n\t\t\tif !equalSlices(unzippedA, tt.expectedA) {\n\t\t\t\tt.Errorf(\"Unzip failed for slice A, expected %v, got %v\", tt.expectedA, unzippedA)\n\t\t\t}\n\t\t\tif !equalSlices(unzippedB, tt.expectedB) {\n\t\t\t\tt.Errorf(\"Unzip failed for slice B, expected %v, got %v\", tt.expectedB, unzippedB)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGroupBy(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []any\n\t\tfn       func(any) any\n\t\texpected map[any][]any\n\t}{\n\t\t{\n\t\t\tname:  \"group by even/odd\",\n\t\t\tinput: []any{1, 2, 3, 4, 5, 6},\n\t\t\tfn:    func(v any) any { return v.(int) % 2 },\n\t\t\texpected: map[any][]any{\n\t\t\t\t0: {2, 4, 6},\n\t\t\t\t1: {1, 3, 5},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"empty slice\",\n\t\t\tinput:    []any{},\n\t\t\tfn:       func(v any) any { return v.(int) % 2 },\n\t\t\texpected: map[any][]any{},\n\t\t},\n\t\t{\n\t\t\tname:  \"single group\",\n\t\t\tinput: []any{2, 4, 6},\n\t\t\tfn:    func(v any) any { return v.(int) % 2 },\n\t\t\texpected: map[any][]any{\n\t\t\t\t0: {2, 4, 6},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"group by type\",\n\t\t\tinput: []any{1, \"a\", 2, \"b\", true},\n\t\t\tfn: func(v any) any {\n\t\t\t\tswitch v.(type) {\n\t\t\t\tcase int:\n\t\t\t\t\treturn \"int\"\n\t\t\t\tcase string:\n\t\t\t\t\treturn \"string\"\n\t\t\t\tdefault:\n\t\t\t\t\treturn \"other\"\n\t\t\t\t}\n\t\t\t},\n\t\t\texpected: map[any][]any{\n\t\t\t\t\"int\":    {1, 2},\n\t\t\t\t\"string\": {\"a\", \"b\"},\n\t\t\t\t\"other\":  {true},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := GroupBy(tt.input, tt.fn)\n\t\t\tif len(result) != len(tt.expected) {\n\t\t\t\tt.Errorf(\"GroupBy failed, expected %d groups, got %d\", len(tt.expected), len(result))\n\t\t\t}\n\t\t\tfor k, v := range tt.expected {\n\t\t\t\tif !equalSlices(result[k], v) {\n\t\t\t\t\tt.Errorf(\"GroupBy failed for key %v, expected %v, got %v\", k, v, result[k])\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFlatten(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    [][]any\n\t\texpected []any\n\t}{\n\t\t{\n\t\t\tname:     \"normal nested slices\",\n\t\t\tinput:    [][]any{{1, 2}, {3, 4}, {5}},\n\t\t\texpected: []any{1, 2, 3, 4, 5},\n\t\t},\n\t\t{\n\t\t\tname:     \"empty outer slice\",\n\t\t\tinput:    [][]any{},\n\t\t\texpected: []any{},\n\t\t},\n\t\t{\n\t\t\tname:     \"empty inner slices\",\n\t\t\tinput:    [][]any{{}, {}, {}},\n\t\t\texpected: []any{},\n\t\t},\n\t\t{\n\t\t\tname:     \"mixed types\",\n\t\t\tinput:    [][]any{{1, \"a\"}, {true, 2.5}, {nil}},\n\t\t\texpected: []any{1, \"a\", true, 2.5, nil},\n\t\t},\n\t\t{\n\t\t\tname:     \"single element slices\",\n\t\t\tinput:    [][]any{{1}, {2}, {3}},\n\t\t\texpected: []any{1, 2, 3},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := Flatten(tt.input)\n\t\t\tif !equalSlices(result, tt.expected) {\n\t\t\t\tt.Errorf(\"Flatten failed, expected %v, got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestContains(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tslice    []any\n\t\titem     any\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"contains integer\",\n\t\t\tslice:    []any{1, 2, 3},\n\t\t\titem:     2,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"does not contain integer\",\n\t\t\tslice:    []any{1, 2, 3},\n\t\t\titem:     4,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"contains string\",\n\t\t\tslice:    []any{\"a\", \"b\", \"c\"},\n\t\t\titem:     \"b\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"empty slice\",\n\t\t\tslice:    []any{},\n\t\t\titem:     1,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"contains nil\",\n\t\t\tslice:    []any{1, nil, 3},\n\t\t\titem:     nil,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"mixed types\",\n\t\t\tslice:    []any{1, \"a\", true},\n\t\t\titem:     true,\n\t\t\texpected: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := contains(tt.slice, tt.item)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"contains failed, expected %v, got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Helper function for testing\nfunc contains(slice []any, item any) bool {\n\tfor _, v := range slice {\n\t\tif v == item {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// Helper functions for comparing slices\nfunc equalSlices(a, b []any) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i := range a {\n\t\tif a[i] != b[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc equalNestedSlices(a, b [][]any) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i := range a {\n\t\tif !equalSlices(a[i], b[i]) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/fp\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"CvM0VW/1+8Sj0W8/Hp8C/tR+f+03/ff/fPVIUrlqRfk0Fm3FoRbto2dpL0LM/WOSYL9EUPXCNY9tvG068duk2w=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","package":{"name":"memo","path":"gno.land/p/moul/memo","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/memo\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"},{"name":"memo.gno","body":"// Package memo provides a simple memoization utility to cache function results.\n//\n// The package offers a Memoizer type that can cache function results based on keys,\n// with optional validation of cached values. This is useful for expensive computations\n// that need to be cached and potentially invalidated based on custom conditions.\n//\n// /!\\ Important Warning for Gno Usage:\n// In Gno, storage updates only persist during transactions. This means:\n//   - Cache entries created during queries will NOT persist\n//   - Creating cache entries during queries will actually decrease performance\n//     as it wastes resources trying to save data that won't be saved\n//\n// Best Practices:\n// - Use this pattern in transaction-driven contexts rather than query/render scenarios\n// - Consider controlled cache updates, e.g., by specific accounts (like oracles)\n// - Ideal for cases where cache updates happen every N blocks or on specific events\n// - Carefully evaluate if caching will actually improve performance in your use case\n//\n// Basic usage example:\n//\n//\tm := memo.New()\n//\n//\t// Cache expensive computation\n//\tresult := m.Memoize(\"key\", func() any {\n//\t    // expensive operation\n//\t    return \"computed-value\"\n//\t})\n//\n//\t// Subsequent calls with same key return cached result\n//\tresult = m.Memoize(\"key\", func() any {\n//\t    // function won't be called, cached value is returned\n//\t    return \"computed-value\"\n//\t})\n//\n// Example with validation:\n//\n//\ttype TimestampedValue struct {\n//\t    Value     string\n//\t    Timestamp time.Time\n//\t}\n//\n//\tm := memo.New()\n//\n//\t// Cache value with timestamp\n//\tresult := m.MemoizeWithValidator(\n//\t    \"key\",\n//\t    func() any {\n//\t        return TimestampedValue{\n//\t            Value:     \"data\",\n//\t            Timestamp: time.Now(),\n//\t        }\n//\t    },\n//\t    func(cached any) bool {\n//\t        // Validate that the cached value is not older than 1 hour\n//\t        if tv, ok := cached.(TimestampedValue); ok {\n//\t            return time.Since(tv.Timestamp) \u003c time.Hour\n//\t        }\n//\t        return false\n//\t    },\n//\t)\npackage memo\n\nimport (\n\t\"gno.land/p/demo/btree\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Record implements the btree.Record interface for our cache entries\ntype cacheEntry struct {\n\tkey   any\n\tvalue any\n}\n\n// Less implements btree.Record interface\nfunc (e cacheEntry) Less(than btree.Record) bool {\n\t// Convert the other record to cacheEntry\n\tother := than.(cacheEntry)\n\t// Compare string representations of keys for consistent ordering\n\treturn ufmt.Sprintf(\"%v\", e.key) \u003c ufmt.Sprintf(\"%v\", other.key)\n}\n\n// Memoizer is a structure to handle memoization of function results.\ntype Memoizer struct {\n\tcache *btree.BTree\n}\n\n// New creates a new Memoizer instance.\nfunc New() *Memoizer {\n\treturn \u0026Memoizer{\n\t\tcache: btree.New(),\n\t}\n}\n\n// Memoize ensures the result of the given function is cached for the specified key.\nfunc (m *Memoizer) Memoize(key any, fn func() any) any {\n\tentry := cacheEntry{key: key}\n\tif found := m.cache.Get(entry); found != nil {\n\t\treturn found.(cacheEntry).value\n\t}\n\n\tvalue := fn()\n\tm.cache.Insert(cacheEntry{key: key, value: value})\n\treturn value\n}\n\n// MemoizeWithValidator ensures the result is cached and valid according to the validator function.\nfunc (m *Memoizer) MemoizeWithValidator(key any, fn func() any, isValid func(any) bool) any {\n\tentry := cacheEntry{key: key}\n\tif found := m.cache.Get(entry); found != nil {\n\t\tcachedEntry := found.(cacheEntry)\n\t\tif isValid(cachedEntry.value) {\n\t\t\treturn cachedEntry.value\n\t\t}\n\t}\n\n\tvalue := fn()\n\tm.cache.Insert(cacheEntry{key: key, value: value})\n\treturn value\n}\n\n// Invalidate removes the cached value for the specified key.\nfunc (m *Memoizer) Invalidate(key any) {\n\tm.cache.Delete(cacheEntry{key: key})\n}\n\n// Clear clears all cached values.\nfunc (m *Memoizer) Clear() {\n\tm.cache.Clear(true)\n}\n\n// Size returns the number of items currently in the cache.\nfunc (m *Memoizer) Size() int {\n\treturn m.cache.Len()\n}\n"},{"name":"memo_test.gno","body":"package memo\n\nimport (\n\t\"testing\"\n\t\"time\"\n)\n\ntype timestampedValue struct {\n\tvalue     any\n\ttimestamp time.Time\n}\n\n// complexKey is used to test struct keys\ntype complexKey struct {\n\tID   int\n\tName string\n}\n\nfunc TestMemoize(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tkey       any\n\t\tvalue     any\n\t\tcallCount *int\n\t}{\n\t\t{\n\t\t\tname:      \"string key and value\",\n\t\t\tkey:       \"test-key\",\n\t\t\tvalue:     \"test-value\",\n\t\t\tcallCount: new(int),\n\t\t},\n\t\t{\n\t\t\tname:      \"int key and value\",\n\t\t\tkey:       42,\n\t\t\tvalue:     123,\n\t\t\tcallCount: new(int),\n\t\t},\n\t\t{\n\t\t\tname:      \"mixed types\",\n\t\t\tkey:       \"number\",\n\t\t\tvalue:     42,\n\t\t\tcallCount: new(int),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tm := New()\n\t\t\tif m.Size() != 0 {\n\t\t\t\tt.Errorf(\"Initial size = %d, want 0\", m.Size())\n\t\t\t}\n\n\t\t\tfn := func() any {\n\t\t\t\t*tt.callCount++\n\t\t\t\treturn tt.value\n\t\t\t}\n\n\t\t\t// First call should compute\n\t\t\tresult := m.Memoize(tt.key, fn)\n\t\t\tif result != tt.value {\n\t\t\t\tt.Errorf(\"Memoize() = %v, want %v\", result, tt.value)\n\t\t\t}\n\t\t\tif *tt.callCount != 1 {\n\t\t\t\tt.Errorf(\"Function called %d times, want 1\", *tt.callCount)\n\t\t\t}\n\t\t\tif m.Size() != 1 {\n\t\t\t\tt.Errorf(\"Size after first call = %d, want 1\", m.Size())\n\t\t\t}\n\n\t\t\t// Second call should use cache\n\t\t\tresult = m.Memoize(tt.key, fn)\n\t\t\tif result != tt.value {\n\t\t\t\tt.Errorf(\"Memoize() second call = %v, want %v\", result, tt.value)\n\t\t\t}\n\t\t\tif *tt.callCount != 1 {\n\t\t\t\tt.Errorf(\"Function called %d times, want 1\", *tt.callCount)\n\t\t\t}\n\t\t\tif m.Size() != 1 {\n\t\t\t\tt.Errorf(\"Size after second call = %d, want 1\", m.Size())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMemoizeWithValidator(t *testing.T) {\n\ttests := []struct {\n\t\tname            string\n\t\tkey             any\n\t\tvalue           any\n\t\tvalidDuration   time.Duration\n\t\twaitDuration    time.Duration\n\t\texpectedCalls   int\n\t\tshouldRecompute bool\n\t}{\n\t\t{\n\t\t\tname:            \"valid cache\",\n\t\t\tkey:             \"key1\",\n\t\t\tvalue:           \"value1\",\n\t\t\tvalidDuration:   time.Hour,\n\t\t\twaitDuration:    time.Millisecond,\n\t\t\texpectedCalls:   1,\n\t\t\tshouldRecompute: false,\n\t\t},\n\t\t{\n\t\t\tname:            \"expired cache\",\n\t\t\tkey:             \"key2\",\n\t\t\tvalue:           \"value2\",\n\t\t\tvalidDuration:   time.Millisecond,\n\t\t\twaitDuration:    time.Millisecond * 2,\n\t\t\texpectedCalls:   2,\n\t\t\tshouldRecompute: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tm := New()\n\t\t\tcallCount := 0\n\n\t\t\tfn := func() any {\n\t\t\t\tcallCount++\n\t\t\t\treturn timestampedValue{\n\t\t\t\t\tvalue:     tt.value,\n\t\t\t\t\ttimestamp: time.Now(),\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tisValid := func(cached any) bool {\n\t\t\t\tif tv, ok := cached.(timestampedValue); ok {\n\t\t\t\t\treturn time.Since(tv.timestamp) \u003c tt.validDuration\n\t\t\t\t}\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\t// First call\n\t\t\tresult := m.MemoizeWithValidator(tt.key, fn, isValid)\n\t\t\tif tv, ok := result.(timestampedValue); !ok || tv.value != tt.value {\n\t\t\t\tt.Errorf(\"MemoizeWithValidator() = %v, want value %v\", result, tt.value)\n\t\t\t}\n\n\t\t\t// Wait\n\t\t\ttesting.SkipHeights(10)\n\n\t\t\t// Second call\n\t\t\tresult = m.MemoizeWithValidator(tt.key, fn, isValid)\n\t\t\tif tv, ok := result.(timestampedValue); !ok || tv.value != tt.value {\n\t\t\t\tt.Errorf(\"MemoizeWithValidator() second call = %v, want value %v\", result, tt.value)\n\t\t\t}\n\n\t\t\tif callCount != tt.expectedCalls {\n\t\t\t\tt.Errorf(\"Function called %d times, want %d\", callCount, tt.expectedCalls)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestInvalidate(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tkey       any\n\t\tvalue     any\n\t\tcallCount *int\n\t}{\n\t\t{\n\t\t\tname:      \"invalidate existing key\",\n\t\t\tkey:       \"test-key\",\n\t\t\tvalue:     \"test-value\",\n\t\t\tcallCount: new(int),\n\t\t},\n\t\t{\n\t\t\tname:      \"invalidate non-existing key\",\n\t\t\tkey:       \"missing-key\",\n\t\t\tvalue:     \"test-value\",\n\t\t\tcallCount: new(int),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tm := New()\n\t\t\tfn := func() any {\n\t\t\t\t*tt.callCount++\n\t\t\t\treturn tt.value\n\t\t\t}\n\n\t\t\t// First call\n\t\t\tm.Memoize(tt.key, fn)\n\t\t\tif m.Size() != 1 {\n\t\t\t\tt.Errorf(\"Size after first call = %d, want 1\", m.Size())\n\t\t\t}\n\n\t\t\t// Invalidate\n\t\t\tm.Invalidate(tt.key)\n\t\t\tif m.Size() != 0 {\n\t\t\t\tt.Errorf(\"Size after invalidate = %d, want 0\", m.Size())\n\t\t\t}\n\n\t\t\t// Call again should recompute\n\t\t\tresult := m.Memoize(tt.key, fn)\n\t\t\tif result != tt.value {\n\t\t\t\tt.Errorf(\"Memoize() after invalidate = %v, want %v\", result, tt.value)\n\t\t\t}\n\t\t\tif *tt.callCount != 2 {\n\t\t\t\tt.Errorf(\"Function called %d times, want 2\", *tt.callCount)\n\t\t\t}\n\t\t\tif m.Size() != 1 {\n\t\t\t\tt.Errorf(\"Size after recompute = %d, want 1\", m.Size())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClear(t *testing.T) {\n\tm := New()\n\tcallCount := 0\n\n\tfn := func() any {\n\t\tcallCount++\n\t\treturn \"value\"\n\t}\n\n\t// Cache some values\n\tm.Memoize(\"key1\", fn)\n\tm.Memoize(\"key2\", fn)\n\n\tif callCount != 2 {\n\t\tt.Errorf(\"Initial calls = %d, want 2\", callCount)\n\t}\n\tif m.Size() != 2 {\n\t\tt.Errorf(\"Size after initial calls = %d, want 2\", m.Size())\n\t}\n\n\t// Clear cache\n\tm.Clear()\n\tif m.Size() != 0 {\n\t\tt.Errorf(\"Size after clear = %d, want 0\", m.Size())\n\t}\n\n\t// Recompute values\n\tm.Memoize(\"key1\", fn)\n\tm.Memoize(\"key2\", fn)\n\n\tif callCount != 4 {\n\t\tt.Errorf(\"Calls after clear = %d, want 4\", callCount)\n\t}\n\tif m.Size() != 2 {\n\t\tt.Errorf(\"Size after recompute = %d, want 2\", m.Size())\n\t}\n}\n\nfunc TestSize(t *testing.T) {\n\tm := New()\n\n\tif m.Size() != 0 {\n\t\tt.Errorf(\"Initial size = %d, want 0\", m.Size())\n\t}\n\n\tcallCount := 0\n\tfn := func() any {\n\t\tcallCount++\n\t\treturn \"value\"\n\t}\n\n\t// Add items\n\tm.Memoize(\"key1\", fn)\n\tif m.Size() != 1 {\n\t\tt.Errorf(\"Size after first insert = %d, want 1\", m.Size())\n\t}\n\n\tm.Memoize(\"key2\", fn)\n\tif m.Size() != 2 {\n\t\tt.Errorf(\"Size after second insert = %d, want 2\", m.Size())\n\t}\n\n\t// Duplicate key should not increase size\n\tm.Memoize(\"key1\", fn)\n\tif m.Size() != 2 {\n\t\tt.Errorf(\"Size after duplicate insert = %d, want 2\", m.Size())\n\t}\n\n\t// Remove item\n\tm.Invalidate(\"key1\")\n\tif m.Size() != 1 {\n\t\tt.Errorf(\"Size after invalidate = %d, want 1\", m.Size())\n\t}\n\n\t// Clear all\n\tm.Clear()\n\tif m.Size() != 0 {\n\t\tt.Errorf(\"Size after clear = %d, want 0\", m.Size())\n\t}\n}\n\nfunc TestMemoizeWithDifferentKeyTypes(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tkeys      []any    // Now an array of keys\n\t\tvalues    []string // Corresponding values\n\t\tcallCount *int\n\t}{\n\t\t{\n\t\t\tname:      \"integer keys\",\n\t\t\tkeys:      []any{42, 43},\n\t\t\tvalues:    []string{\"value-for-42\", \"value-for-43\"},\n\t\t\tcallCount: new(int),\n\t\t},\n\t\t{\n\t\t\tname:      \"float keys\",\n\t\t\tkeys:      []any{3.14, 2.718},\n\t\t\tvalues:    []string{\"value-for-pi\", \"value-for-e\"},\n\t\t\tcallCount: new(int),\n\t\t},\n\t\t{\n\t\t\tname:      \"bool keys\",\n\t\t\tkeys:      []any{true, false},\n\t\t\tvalues:    []string{\"value-for-true\", \"value-for-false\"},\n\t\t\tcallCount: new(int),\n\t\t},\n\t\t/*\n\t\t\t{\n\t\t\t\tname: \"struct keys\",\n\t\t\t\tkeys: []any{\n\t\t\t\t\tcomplexKey{ID: 1, Name: \"test1\"},\n\t\t\t\t\tcomplexKey{ID: 2, Name: \"test2\"},\n\t\t\t\t},\n\t\t\t\tvalues:    []string{\"value-for-struct1\", \"value-for-struct2\"},\n\t\t\t\tcallCount: new(int),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname:      \"nil and empty interface keys\",\n\t\t\t\tkeys:      []any{nil, any(nil)},\n\t\t\t\tvalues:    []string{\"value-for-nil\", \"value-for-empty-interface\"},\n\t\t\t\tcallCount: new(int),\n\t\t\t},\n\t\t*/\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tm := New()\n\n\t\t\t// Test both keys\n\t\t\tfor i, key := range tt.keys {\n\t\t\t\tvalue := tt.values[i]\n\t\t\t\tfn := func() any {\n\t\t\t\t\t*tt.callCount++\n\t\t\t\t\treturn value\n\t\t\t\t}\n\n\t\t\t\t// First call should compute\n\t\t\t\tresult := m.Memoize(key, fn)\n\t\t\t\tif result != value {\n\t\t\t\t\tt.Errorf(\"Memoize() for key %v = %v, want %v\", key, result, value)\n\t\t\t\t}\n\t\t\t\tif *tt.callCount != i+1 {\n\t\t\t\t\tt.Errorf(\"Function called %d times, want %d\", *tt.callCount, i+1)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Verify size includes both entries\n\t\t\tif m.Size() != 2 {\n\t\t\t\tt.Errorf(\"Size after both inserts = %d, want 2\", m.Size())\n\t\t\t}\n\n\t\t\t// Second call for each key should use cache\n\t\t\tfor i, key := range tt.keys {\n\t\t\t\tinitialCount := *tt.callCount\n\t\t\t\tresult := m.Memoize(key, func() any {\n\t\t\t\t\t*tt.callCount++\n\t\t\t\t\treturn \"should-not-be-called\"\n\t\t\t\t})\n\n\t\t\t\tif result != tt.values[i] {\n\t\t\t\t\tt.Errorf(\"Memoize() second call for key %v = %v, want %v\", key, result, tt.values[i])\n\t\t\t\t}\n\t\t\t\tif *tt.callCount != initialCount {\n\t\t\t\t\tt.Errorf(\"Cache miss for key %v\", key)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Test invalidate for each key\n\t\t\tfor i, key := range tt.keys {\n\t\t\t\tm.Invalidate(key)\n\t\t\t\tif m.Size() != 1-i {\n\t\t\t\t\tt.Errorf(\"Size after invalidate %d = %d, want %d\", i+1, m.Size(), 1-i)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMultipleKeyTypes(t *testing.T) {\n\tm := New()\n\tcallCount := 0\n\n\t// Insert different key types simultaneously (two of each type)\n\tkeys := []any{\n\t\t42, 43, // ints\n\t\t\"string-key1\", \"string-key2\", // strings\n\t\t3.14, 2.718, // floats\n\t\ttrue, false, // bools\n\t}\n\n\tfor i, key := range keys {\n\t\tvalue := i\n\t\tm.Memoize(key, func() any {\n\t\t\tcallCount++\n\t\t\treturn value\n\t\t})\n\t}\n\n\t// Verify size includes all entries\n\tif m.Size() != len(keys) {\n\t\tt.Errorf(\"Size = %d, want %d\", m.Size(), len(keys))\n\t}\n\n\t// Verify all values are cached correctly\n\tfor i, key := range keys {\n\t\tinitialCount := callCount\n\t\tresult := m.Memoize(key, func() any {\n\t\t\tcallCount++\n\t\t\treturn -1 // Should never be returned if cache works\n\t\t})\n\n\t\tif result != i {\n\t\t\tt.Errorf(\"Memoize(%v) = %v, want %v\", key, result, i)\n\t\t}\n\t\tif callCount != initialCount {\n\t\t\tt.Errorf(\"Cache miss for key %v\", key)\n\t\t}\n\t}\n\n\t// Test invalidation of pairs\n\tfor i := 0; i \u003c len(keys); i += 2 {\n\t\tm.Invalidate(keys[i])\n\t\tm.Invalidate(keys[i+1])\n\t\texpectedSize := len(keys) - (i + 2)\n\t\tif m.Size() != expectedSize {\n\t\t\tt.Errorf(\"Size after invalidating pair %d = %d, want %d\", i/2, m.Size(), expectedSize)\n\t\t}\n\t}\n\n\t// Clear and verify\n\tm.Clear()\n\tif m.Size() != 0 {\n\t\tt.Errorf(\"Size after clear = %d, want 0\", m.Size())\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"gWpMphhzZlwpELgztvXQHqaf+VTCkSGDNiTW0mHqKCo4hb4VN5uFgay2buUM7C/rRFZdQ9kF1C+c+R0T8vmK/A=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","package":{"name":"printfdebugging","path":"gno.land/p/moul/printfdebugging","files":[{"name":"color.gno","body":"package printfdebugging\n\n// consts copied from https://github.com/fatih/color/blob/main/color.go\n\n// Attribute defines a single SGR Code\ntype Attribute int\n\nconst Escape = \"\\x1b\"\n\n// Base attributes\nconst (\n\tReset Attribute = iota\n\tBold\n\tFaint\n\tItalic\n\tUnderline\n\tBlinkSlow\n\tBlinkRapid\n\tReverseVideo\n\tConcealed\n\tCrossedOut\n)\n\nconst (\n\tResetBold Attribute = iota + 22\n\tResetItalic\n\tResetUnderline\n\tResetBlinking\n\t_\n\tResetReversed\n\tResetConcealed\n\tResetCrossedOut\n)\n\n// Foreground text colors\nconst (\n\tFgBlack Attribute = iota + 30\n\tFgRed\n\tFgGreen\n\tFgYellow\n\tFgBlue\n\tFgMagenta\n\tFgCyan\n\tFgWhite\n)\n\n// Foreground Hi-Intensity text colors\nconst (\n\tFgHiBlack Attribute = iota + 90\n\tFgHiRed\n\tFgHiGreen\n\tFgHiYellow\n\tFgHiBlue\n\tFgHiMagenta\n\tFgHiCyan\n\tFgHiWhite\n)\n\n// Background text colors\nconst (\n\tBgBlack Attribute = iota + 40\n\tBgRed\n\tBgGreen\n\tBgYellow\n\tBgBlue\n\tBgMagenta\n\tBgCyan\n\tBgWhite\n)\n\n// Background Hi-Intensity text colors\nconst (\n\tBgHiBlack Attribute = iota + 100\n\tBgHiRed\n\tBgHiGreen\n\tBgHiYellow\n\tBgHiBlue\n\tBgHiMagenta\n\tBgHiCyan\n\tBgHiWhite\n)\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/printfdebugging\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"},{"name":"printfdebugging.gno","body":"// this package is a joke... or not.\npackage printfdebugging\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nfunc BigRedLine(args ...string) {\n\tprintln(ufmt.Sprintf(\"%s[%dm####################################%s[%dm %s\",\n\t\tEscape, int(BgRed), Escape, int(Reset),\n\t\tstrings.Join(args, \" \"),\n\t))\n}\n\nfunc Success() {\n\tprintln(\"   \\033[31mS\\033[33mU\\033[32mC\\033[36mC\\033[34mE\\033[35mS\\033[31mS\\033[0m   \")\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"9fgzPHmqCZuoi52JumNiD9DWuIL/Q8k0I2vWoqcQqTtjIIOeup+Rsw/RhSO2awpi3tEbwdQ/i7RxIbINJScFyA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","package":{"name":"lplist","path":"gno.land/p/moul/ulist/lplist","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/ulist/lplist\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"},{"name":"lplist.gno","body":"// Package lplist provides a layered proxy implementation for lists that allows transparent\n// migration of data between different schema versions.\n//\n// LayeredProxyList wraps an existing list (source) with a new list (target) and optionally\n// applies migrations to source data when it's accessed. This enables schema evolution without\n// requiring upfront migration of all data, making it ideal for large datasets or when\n// preserving original data is important.\n//\n// Key features:\n// - Lazy migration: Data is only transformed when accessed, not stored in migrated form\n// - Append-only source: Source data is treated as immutable to preserve original data\n// - Chaining: Multiple LayeredProxyLists can be stacked for multi-step migrations\n//\n// Example usage:\n//\n//\t// Define data types for different schema versions\n//\ttype UserV1 struct {\n//\t    Name string\n//\t    Age int\n//\t}\n//\n//\ttype UserV2 struct {\n//\t    FullName string\n//\t    Age int\n//\t    Active bool\n//\t}\n//\n//\t// Create source list with old schema\n//\tsourceList := ulist.New()\n//\tsourceList.Append(\n//\t    UserV1{Name: \"Alice\", Age: 30},\n//\t    UserV1{Name: \"Bob\", Age: 25},\n//\t)\n//\n//\t// Define migration function from V1 to V2\n//\tmigrateUserV1ToV2 := func(v any) any {\n//\t    user := v.(UserV1)\n//\t    return UserV2{\n//\t        FullName: user.Name,  // Name field renamed to FullName\n//\t        Age: user.Age,\n//\t        Active: true,         // New field with default value\n//\t    }\n//\t}\n//\n//\t// Create layered proxy with migration\n//\tproxy := NewLayeredProxyList(sourceList, migrateUserV1ToV2)\n//\n//\t// Add new data directly in V2 format\n//\tproxy.Append(UserV2{FullName: \"Charlie\", Age: 40, Active: false})\n//\n//\t// All access through proxy returns data in V2 format\n//\tfor i := 0; i \u003c proxy.Size(); i++ {\n//\t    user := proxy.Get(i).(UserV2)\n//\t    fmt.Printf(\"User: %s, Age: %d, Active: %t\\n\", user.FullName, user.Age, user.Active)\n//\t}\npackage lplist\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/moul/ulist/v0\"\n)\n\n// MigratorFn is a function type that lazily converts values from source to target\ntype MigratorFn func(any) any\n\n// LayeredProxyList represents a wrapper around an existing List that handles migration\ntype LayeredProxyList struct {\n\tsource       ulist.IList\n\ttarget       *ulist.List\n\tmigrator     MigratorFn\n\tsourceHeight int // Store initial source size to optimize lookups\n}\n\n// NewLayeredProxyList creates a new LayeredProxyList instance that wraps an existing List\nfunc NewLayeredProxyList(source ulist.IList, migrator MigratorFn) *LayeredProxyList {\n\tsourceHeight := source.TotalSize()\n\ttarget := ulist.New()\n\treturn \u0026LayeredProxyList{\n\t\tsource:       source,\n\t\ttarget:       target,\n\t\tmigrator:     migrator,\n\t\tsourceHeight: sourceHeight,\n\t}\n}\n\n// Get retrieves the value at the specified index\n// Uses sourceHeight to efficiently route requests\nfunc (l *LayeredProxyList) Get(index int) any {\n\tif index \u003c l.sourceHeight {\n\t\t// Direct access to source for indices below sourceHeight\n\t\tval := l.source.Get(index)\n\t\tif val == nil {\n\t\t\treturn nil\n\t\t}\n\t\t// Only apply migrator if it exists\n\t\tif l.migrator != nil {\n\t\t\treturn l.migrator(val)\n\t\t}\n\t\treturn val\n\t}\n\t// For indices \u003e= sourceHeight, adjust index to be relative to target list starting at 0\n\ttargetIndex := index - l.sourceHeight\n\treturn l.target.Get(targetIndex)\n}\n\n// Append adds one or more values to the target list\nfunc (l *LayeredProxyList) Append(values ...any) {\n\tl.target.Append(values...)\n}\n\n// Delete marks elements as deleted in the appropriate list\nfunc (l *LayeredProxyList) Delete(indices ...int) error {\n\tfor _, index := range indices {\n\t\tif index \u003c l.sourceHeight {\n\t\t\terr := l.source.Delete(index)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, index := range indices {\n\t\ttargetIndex := index - l.sourceHeight\n\t\terr := l.target.Delete(targetIndex)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// Size returns the total number of active elements\nfunc (l *LayeredProxyList) Size() int {\n\treturn l.source.Size() + l.target.Size()\n}\n\n// TotalSize returns the total number of elements in the list\nfunc (l *LayeredProxyList) TotalSize() int {\n\treturn l.sourceHeight + l.target.TotalSize()\n}\n\n// MustDelete deletes elements, panicking on error\nfunc (l *LayeredProxyList) MustDelete(indices ...int) {\n\tif err := l.Delete(indices...); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// MustGet retrieves a value, panicking if not found\nfunc (l *LayeredProxyList) MustGet(index int) any {\n\tval := l.Get(index)\n\tif val == nil {\n\t\tpanic(ulist.ErrDeleted)\n\t}\n\treturn val\n}\n\n// GetRange returns elements between start and end indices\nfunc (l *LayeredProxyList) GetRange(start, end int) []ulist.Entry {\n\tvar entries []ulist.Entry\n\tl.Iterator(start, end, func(index int, value any) bool {\n\t\tentries = append(entries, ulist.Entry{Index: index, Value: value})\n\t\treturn false\n\t})\n\treturn entries\n}\n\n// GetRangeByOffset returns elements starting from offset\nfunc (l *LayeredProxyList) GetRangeByOffset(offset int, count int) []ulist.Entry {\n\tvar entries []ulist.Entry\n\tl.IteratorByOffset(offset, count, func(index int, value any) bool {\n\t\tentries = append(entries, ulist.Entry{Index: index, Value: value})\n\t\treturn false\n\t})\n\treturn entries\n}\n\n// Iterator performs iteration between start and end indices\nfunc (l *LayeredProxyList) Iterator(start, end int, cb ulist.IterCbFn) bool {\n\t// For empty list or invalid range\n\tif start \u003c 0 \u0026\u0026 end \u003c 0 {\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\n\ttotalSize := l.TotalSize()\n\tif end \u003e= totalSize {\n\t\tend = totalSize - 1\n\t}\n\tif start \u003e= totalSize {\n\t\tstart = 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\nfunc (l *LayeredProxyList) IteratorByOffset(offset int, count int, cb ulist.IterCbFn) bool {\n\tif count == 0 {\n\t\treturn false\n\t}\n\n\t// Normalize offset\n\tif offset \u003c 0 {\n\t\toffset = 0\n\t}\n\ttotalSize := l.TotalSize()\n\tif offset \u003e= totalSize {\n\t\toffset = 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 = totalSize - 1\n\t} else {\n\t\tend = 0\n\t}\n\n\twrapperReturned := false\n\n\t// Calculate absolute value manually instead of using abs function\n\tremaining := count\n\tif remaining \u003c 0 {\n\t\tremaining = -remaining\n\t}\n\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\n\tret := l.Iterator(offset, end, wrapper)\n\tif wrapperReturned {\n\t\treturn false\n\t}\n\treturn ret\n}\n\n// Set updates the value at the specified index\nfunc (l *LayeredProxyList) Set(index int, value any) error {\n\tif index \u003c l.sourceHeight {\n\t\t// Cannot modify source list directly\n\t\treturn errors.New(\"cannot modify source list directly\")\n\t}\n\n\t// Adjust index to be relative to target list starting at 0\n\ttargetIndex := index - l.sourceHeight\n\treturn l.target.Set(targetIndex, value)\n}\n\n// MustSet updates the value at the specified index, panicking on error\nfunc (l *LayeredProxyList) MustSet(index int, value any) {\n\tif err := l.Set(index, value); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// GetByOffset returns elements starting from offset with count determining direction\nfunc (l *LayeredProxyList) GetByOffset(offset int, count int) []ulist.Entry {\n\tvar entries []ulist.Entry\n\tl.IteratorByOffset(offset, count, func(index int, value any) bool {\n\t\tentries = append(entries, ulist.Entry{Index: index, Value: value})\n\t\treturn false\n\t})\n\treturn entries\n}\n\n// Verify that LayeredProxyList implements IList\nvar _ ulist.IList = (*LayeredProxyList)(nil)\n"},{"name":"lplist_test.gno","body":"package lplist\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/moul/ulist/v0\"\n)\n\n// TestLayeredProxyListBasicOperations tests the basic operations of LayeredProxyList\nfunc TestLayeredProxyListBasicOperations(t *testing.T) {\n\t// Create source list with initial data\n\tsource := ulist.New()\n\tsource.Append(1, 2, 3)\n\n\t// Create proxy list with a simple multiplier migrator\n\tmigrator := func(v any) any {\n\t\treturn v.(int) * 2\n\t}\n\tproxy := NewLayeredProxyList(source, migrator)\n\n\t// Test initial state\n\tif got := proxy.Size(); got != 3 {\n\t\tt.Errorf(\"initial Size() = %v, want %v\", got, 3)\n\t}\n\tif got := proxy.TotalSize(); got != 3 {\n\t\tt.Errorf(\"initial TotalSize() = %v, want %v\", got, 3)\n\t}\n\n\t// Test Get with migration\n\ttests := []struct {\n\t\tindex int\n\t\twant  any\n\t}{\n\t\t{0, 2}, // 1 * 2\n\t\t{1, 4}, // 2 * 2\n\t\t{2, 6}, // 3 * 2\n\t}\n\n\tfor _, tt := range tests {\n\t\tif got := proxy.Get(tt.index); got != tt.want {\n\t\t\tt.Errorf(\"Get(%v) = %v, want %v\", tt.index, got, tt.want)\n\t\t}\n\t}\n\n\t// Test Append to target\n\tproxy.Append(7, 8)\n\tif got := proxy.Size(); got != 5 {\n\t\tt.Errorf(\"Size() after append = %v, want %v\", got, 5)\n\t}\n\n\t// Test Get from target (no migration)\n\tif got := proxy.Get(3); got != 7 {\n\t\tt.Errorf(\"Get(3) = %v, want %v\", got, 7)\n\t}\n}\n\n// TestLayeredProxyListDelete tests delete operations\nfunc TestLayeredProxyListDelete(t *testing.T) {\n\tsource := ulist.New()\n\tsource.Append(1, 2, 3)\n\tproxy := NewLayeredProxyList(source, nil)\n\tproxy.Append(4, 5)\n\n\t// Test deleting from source (should fail)\n\tif err := proxy.Delete(1); err == nil {\n\t\tt.Error(\"Delete from source should return error\")\n\t}\n\n\t// Test deleting from target (should succeed)\n\tif err := proxy.Delete(3); err != nil {\n\t\tt.Errorf(\"Delete from target failed: %s\", err.Error())\n\t}\n\n\t// After deletion, the value might be undefined rather than nil\n\t// Check that it's not equal to the original value\n\tif got := proxy.Get(3); got == 5 {\n\t\tt.Errorf(\"Get(3) after delete = %v, want it to be deleted\", got)\n\t}\n}\n\n// TestLayeredProxyListIteration tests iteration methods\nfunc TestLayeredProxyListIteration(t *testing.T) {\n\tsource := ulist.New()\n\tsource.Append(1, 2, 3)\n\tproxy := NewLayeredProxyList(source, nil)\n\tproxy.Append(4, 5)\n\n\t// Test GetRange\n\tentries := proxy.GetRange(0, 4)\n\tif len(entries) != 5 {\n\t\tt.Errorf(\"GetRange returned %v entries, want 5\", len(entries))\n\t}\n\n\t// Test reverse iteration\n\tentries = proxy.GetRange(4, 0)\n\tif len(entries) != 5 {\n\t\tt.Errorf(\"Reverse GetRange returned %v entries, want 5\", len(entries))\n\t}\n\n\t// Test IteratorByOffset with positive count\n\tvar values []any\n\tproxy.IteratorByOffset(1, 3, func(index int, value any) bool {\n\t\tvalues = append(values, value)\n\t\treturn false\n\t})\n\tif len(values) != 3 {\n\t\tt.Errorf(\"IteratorByOffset returned %v values, want 3\", len(values))\n\t}\n}\n\n// TestLayeredProxyListMustOperations tests must operations\nfunc TestLayeredProxyListMustOperations(t *testing.T) {\n\tsource := ulist.New()\n\tsource.Append(1, 2)\n\tproxy := NewLayeredProxyList(source, nil)\n\n\t// Test MustGet success\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tt.Errorf(\"MustGet panicked unexpectedly: %v\", r)\n\t\t}\n\t}()\n\tif got := proxy.MustGet(1); got != 2 {\n\t\tt.Errorf(\"MustGet(1) = %v, want 2\", got)\n\t}\n\n\t// Test MustGet panic\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Error(\"MustGet should have panicked\")\n\t\t}\n\t}()\n\tproxy.MustGet(99) // Should panic\n}\n\n// TestLayeredProxyListWithNilMigrator tests behavior without a migrator\nfunc TestLayeredProxyListWithNilMigrator(t *testing.T) {\n\tsource := ulist.New()\n\tsource.Append(1, 2)\n\tproxy := NewLayeredProxyList(source, nil)\n\n\tif got := proxy.Get(0); got != 1 {\n\t\tt.Errorf(\"Get(0) with nil migrator = %v, want 1\", got)\n\t}\n}\n\n// TestLayeredProxyListEmpty tests operations on empty lists\nfunc TestLayeredProxyListEmpty(t *testing.T) {\n\tsource := ulist.New()\n\tproxy := NewLayeredProxyList(source, nil)\n\n\tif got := proxy.Size(); got != 0 {\n\t\tt.Errorf(\"Size() of empty list = %v, want 0\", got)\n\t}\n\n\tif got := proxy.Get(0); got != nil {\n\t\tt.Errorf(\"Get(0) of empty list = %v, want nil\", got)\n\t}\n\n\tentries := proxy.GetRange(0, 10)\n\tif len(entries) != 0 {\n\t\tt.Errorf(\"GetRange on empty list returned %v entries, want 0\", len(entries))\n\t}\n}\n\n// TestLayeredProxyListChaining tests chaining of layered proxies with struct migrations\nfunc TestLayeredProxyListChaining(t *testing.T) {\n\t// Define struct types for different versions\n\ttype v1 struct {\n\t\tnamev1 string\n\t}\n\ttype v2 struct {\n\t\tnamev2 string\n\t}\n\ttype v3 struct {\n\t\tnamev3 string\n\t}\n\n\t// Create source list with v1 objects\n\tsource := ulist.New()\n\tsource.Append(v1{namev1: \"object1\"}, v1{namev1: \"object2\"})\n\n\t// Migration function from v1 to v2\n\tv1Tov2 := func(v any) any {\n\t\tobj := v.(v1)\n\t\treturn v2{namev2: obj.namev1 + \"_v2\"}\n\t}\n\n\t// Create first proxy with v1-\u003ev2 migration\n\tproxyV2 := NewLayeredProxyList(source, v1Tov2)\n\tproxyV2.Append(v2{namev2: \"direct_v2\"})\n\n\t// Migration function from v2 to v3\n\tv2Tov3 := func(v any) any {\n\t\tobj := v.(v2)\n\t\treturn v3{namev3: obj.namev2 + \"_v3\"}\n\t}\n\n\t// Create second proxy with v2-\u003ev3 migration, using the first proxy as source\n\tproxyV3 := NewLayeredProxyList(proxyV2, v2Tov3)\n\tproxyV3.Append(v3{namev3: \"direct_v3\"})\n\n\t// Verify sizes\n\tif got := proxyV3.Size(); got != 4 {\n\t\tt.Errorf(\"proxyV3.Size() = %v, want 4\", got)\n\t}\n\n\t// Test that all objects are correctly migrated when accessed through proxyV3\n\texpected := []struct {\n\t\tindex int\n\t\tname  string\n\t}{\n\t\t{0, \"object1_v2_v3\"}, // v1 -\u003e v2 -\u003e v3\n\t\t{1, \"object2_v2_v3\"}, // v1 -\u003e v2 -\u003e v3\n\t\t{2, \"direct_v2_v3\"},  // v2 -\u003e v3\n\t\t{3, \"direct_v3\"},     // v3 (no migration)\n\t}\n\n\tfor _, tt := range expected {\n\t\tobj := proxyV3.Get(tt.index).(v3)\n\t\tif obj.namev3 != tt.name {\n\t\t\tt.Errorf(\"proxyV3.Get(%d).namev3 = %v, want %v\", tt.index, obj.namev3, tt.name)\n\t\t}\n\t}\n\n\t// Verify getting items from middle layer (proxyV2)\n\tmiddleExpected := []struct {\n\t\tindex int\n\t\tname  string\n\t}{\n\t\t{0, \"object1_v2\"}, // v1 -\u003e v2\n\t\t{1, \"object2_v2\"}, // v1 -\u003e v2\n\t\t{2, \"direct_v2\"},  // v2 (no migration)\n\t}\n\n\tfor _, tt := range middleExpected {\n\t\tobj := proxyV2.Get(tt.index).(v2)\n\t\tif obj.namev2 != tt.name {\n\t\t\tt.Errorf(\"proxyV2.Get(%d).namev2 = %v, want %v\", tt.index, obj.namev2, tt.name)\n\t\t}\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"Ypf9tvWFY+jESlQJGC4sPu7js9Fcg24GVrl6iRep73JIJMWtse2FmGKK3WIykhq6Z7SZM9Be5Pr4CTPAaDUVsA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","package":{"name":"web25","path":"gno.land/p/moul/web25","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/web25\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"},{"name":"web25.gno","body":"// Pacakge web25 provides an opinionated way to register an external web2\n// frontend to provide a \"better\" web2.5 experience.\npackage web25\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/moul/realmpath/v0\"\n)\n\ntype Config struct {\n\tCID  string\n\tURL  string\n\tText string\n}\n\nfunc (c *Config) SetRemoteFrontendByURL(url string) {\n\tc.CID = \"\"\n\tc.URL = url\n}\n\nfunc (c *Config) SetRemoteFrontendByCID(cid string) {\n\tc.CID = cid\n\tc.URL = \"\"\n}\n\nfunc (c Config) GetLink() string {\n\tif c.CID != \"\" {\n\t\treturn \"https://ipfs.io/ipfs/\" + c.CID\n\t}\n\treturn c.URL\n}\n\nconst DefaultText = \"Click [here]({link}) to visit the full rendering experience.\\n\"\n\n// Render displays a frontend link at the top of your realm's Render function in\n// a concistent way to help gno visitors to have a consistent experience.\n//\n// if query is not nil, then it will check if it's not disable by ?no-web25, so\n// that you can call the render function from an external point of view.\nfunc (c Config) Render(path string) string {\n\tif realmpath.Parse(path).Query.Get(\"no-web25\") == \"1\" {\n\t\treturn \"\"\n\t}\n\ttext := c.Text\n\tif text == \"\" {\n\t\ttext = DefaultText\n\t}\n\ttext = strings.ReplaceAll(text, \"{link}\", c.GetLink())\n\treturn text\n}\n"},{"name":"web25_test.gno","body":"package web25\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"1wmNG4kfW6v0Xcj+bI4lueHvFDO0d4m95t4p44QAd5QsosqgpwC3AL4KL6Frizvg2dXZzxw+zRi9fgPqBASgZg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","package":{"name":"xmath","path":"gno.land/p/moul/xmath","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/xmath\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"},{"name":"xmath.gen.gno","body":"// Code generated by generator.go; DO NOT EDIT.\npackage xmath\n\n// Int8 helpers\nfunc MaxInt8(a, b int8) int8 {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc MinInt8(a, b int8) int8 {\n\tif a \u003c b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc ClampInt8(value, min, max int8) int8 {\n\tif value \u003c min {\n\t\treturn min\n\t}\n\tif value \u003e max {\n\t\treturn max\n\t}\n\treturn value\n}\n\nfunc AbsInt8(x int8) int8 {\n\tif x \u003c 0 {\n\t\treturn -x\n\t}\n\treturn x\n}\n\nfunc SignInt8(x int8) int8 {\n\tif x \u003c 0 {\n\t\treturn -1\n\t}\n\tif x \u003e 0 {\n\t\treturn 1\n\t}\n\treturn 0\n}\n\n// Int16 helpers\nfunc MaxInt16(a, b int16) int16 {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc MinInt16(a, b int16) int16 {\n\tif a \u003c b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc ClampInt16(value, min, max int16) int16 {\n\tif value \u003c min {\n\t\treturn min\n\t}\n\tif value \u003e max {\n\t\treturn max\n\t}\n\treturn value\n}\n\nfunc AbsInt16(x int16) int16 {\n\tif x \u003c 0 {\n\t\treturn -x\n\t}\n\treturn x\n}\n\nfunc SignInt16(x int16) int16 {\n\tif x \u003c 0 {\n\t\treturn -1\n\t}\n\tif x \u003e 0 {\n\t\treturn 1\n\t}\n\treturn 0\n}\n\n// Int32 helpers\nfunc MaxInt32(a, b int32) int32 {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc MinInt32(a, b int32) int32 {\n\tif a \u003c b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc ClampInt32(value, min, max int32) int32 {\n\tif value \u003c min {\n\t\treturn min\n\t}\n\tif value \u003e max {\n\t\treturn max\n\t}\n\treturn value\n}\n\nfunc AbsInt32(x int32) int32 {\n\tif x \u003c 0 {\n\t\treturn -x\n\t}\n\treturn x\n}\n\nfunc SignInt32(x int32) int32 {\n\tif x \u003c 0 {\n\t\treturn -1\n\t}\n\tif x \u003e 0 {\n\t\treturn 1\n\t}\n\treturn 0\n}\n\n// Int64 helpers\nfunc MaxInt64(a, b int64) int64 {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc MinInt64(a, b int64) int64 {\n\tif a \u003c b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc ClampInt64(value, min, max int64) int64 {\n\tif value \u003c min {\n\t\treturn min\n\t}\n\tif value \u003e max {\n\t\treturn max\n\t}\n\treturn value\n}\n\nfunc AbsInt64(x int64) int64 {\n\tif x \u003c 0 {\n\t\treturn -x\n\t}\n\treturn x\n}\n\nfunc SignInt64(x int64) int64 {\n\tif x \u003c 0 {\n\t\treturn -1\n\t}\n\tif x \u003e 0 {\n\t\treturn 1\n\t}\n\treturn 0\n}\n\n// Int helpers\nfunc MaxInt(a, b int) int {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc MinInt(a, b int) int {\n\tif a \u003c b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc ClampInt(value, min, max int) int {\n\tif value \u003c min {\n\t\treturn min\n\t}\n\tif value \u003e max {\n\t\treturn max\n\t}\n\treturn value\n}\n\nfunc AbsInt(x int) int {\n\tif x \u003c 0 {\n\t\treturn -x\n\t}\n\treturn x\n}\n\nfunc SignInt(x int) int {\n\tif x \u003c 0 {\n\t\treturn -1\n\t}\n\tif x \u003e 0 {\n\t\treturn 1\n\t}\n\treturn 0\n}\n\n// Uint8 helpers\nfunc MaxUint8(a, b uint8) uint8 {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc MinUint8(a, b uint8) uint8 {\n\tif a \u003c b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc ClampUint8(value, min, max uint8) uint8 {\n\tif value \u003c min {\n\t\treturn min\n\t}\n\tif value \u003e max {\n\t\treturn max\n\t}\n\treturn value\n}\n\n// Uint16 helpers\nfunc MaxUint16(a, b uint16) uint16 {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc MinUint16(a, b uint16) uint16 {\n\tif a \u003c b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc ClampUint16(value, min, max uint16) uint16 {\n\tif value \u003c min {\n\t\treturn min\n\t}\n\tif value \u003e max {\n\t\treturn max\n\t}\n\treturn value\n}\n\n// Uint32 helpers\nfunc MaxUint32(a, b uint32) uint32 {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc MinUint32(a, b uint32) uint32 {\n\tif a \u003c b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc ClampUint32(value, min, max uint32) uint32 {\n\tif value \u003c min {\n\t\treturn min\n\t}\n\tif value \u003e max {\n\t\treturn max\n\t}\n\treturn value\n}\n\n// Uint64 helpers\nfunc MaxUint64(a, b uint64) uint64 {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc MinUint64(a, b uint64) uint64 {\n\tif a \u003c b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc ClampUint64(value, min, max uint64) uint64 {\n\tif value \u003c min {\n\t\treturn min\n\t}\n\tif value \u003e max {\n\t\treturn max\n\t}\n\treturn value\n}\n\n// Uint helpers\nfunc MaxUint(a, b uint) uint {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc MinUint(a, b uint) uint {\n\tif a \u003c b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc ClampUint(value, min, max uint) uint {\n\tif value \u003c min {\n\t\treturn min\n\t}\n\tif value \u003e max {\n\t\treturn max\n\t}\n\treturn value\n}\n\n// Float32 helpers\nfunc MaxFloat32(a, b float32) float32 {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc MinFloat32(a, b float32) float32 {\n\tif a \u003c b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc ClampFloat32(value, min, max float32) float32 {\n\tif value \u003c min {\n\t\treturn min\n\t}\n\tif value \u003e max {\n\t\treturn max\n\t}\n\treturn value\n}\n\nfunc AbsFloat32(x float32) float32 {\n\tif x \u003c 0 {\n\t\treturn -x\n\t}\n\treturn x\n}\n\nfunc SignFloat32(x float32) float32 {\n\tif x \u003c 0 {\n\t\treturn -1\n\t}\n\tif x \u003e 0 {\n\t\treturn 1\n\t}\n\treturn 0\n}\n\n// Float64 helpers\nfunc MaxFloat64(a, b float64) float64 {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc MinFloat64(a, b float64) float64 {\n\tif a \u003c b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc ClampFloat64(value, min, max float64) float64 {\n\tif value \u003c min {\n\t\treturn min\n\t}\n\tif value \u003e max {\n\t\treturn max\n\t}\n\treturn value\n}\n\nfunc AbsFloat64(x float64) float64 {\n\tif x \u003c 0 {\n\t\treturn -x\n\t}\n\treturn x\n}\n\nfunc SignFloat64(x float64) float64 {\n\tif x \u003c 0 {\n\t\treturn -1\n\t}\n\tif x \u003e 0 {\n\t\treturn 1\n\t}\n\treturn 0\n}\n"},{"name":"xmath.gen_test.gno","body":"package xmath\n\nimport \"testing\"\n\nfunc TestInt8Helpers(t *testing.T) {\n\t// Test MaxInt8\n\tif MaxInt8(1, 2) != 2 {\n\t\tt.Error(\"MaxInt8(1, 2) should be 2\")\n\t}\n\tif MaxInt8(-1, -2) != -1 {\n\t\tt.Error(\"MaxInt8(-1, -2) should be -1\")\n\t}\n\n\t// Test MinInt8\n\tif MinInt8(1, 2) != 1 {\n\t\tt.Error(\"MinInt8(1, 2) should be 1\")\n\t}\n\tif MinInt8(-1, -2) != -2 {\n\t\tt.Error(\"MinInt8(-1, -2) should be -2\")\n\t}\n\n\t// Test ClampInt8\n\tif ClampInt8(5, 1, 3) != 3 {\n\t\tt.Error(\"ClampInt8(5, 1, 3) should be 3\")\n\t}\n\tif ClampInt8(0, 1, 3) != 1 {\n\t\tt.Error(\"ClampInt8(0, 1, 3) should be 1\")\n\t}\n\tif ClampInt8(2, 1, 3) != 2 {\n\t\tt.Error(\"ClampInt8(2, 1, 3) should be 2\")\n\t}\n\n\t// Test AbsInt8\n\tif AbsInt8(-5) != 5 {\n\t\tt.Error(\"AbsInt8(-5) should be 5\")\n\t}\n\tif AbsInt8(5) != 5 {\n\t\tt.Error(\"AbsInt8(5) should be 5\")\n\t}\n\n\t// Test SignInt8\n\tif SignInt8(-5) != -1 {\n\t\tt.Error(\"SignInt8(-5) should be -1\")\n\t}\n\tif SignInt8(5) != 1 {\n\t\tt.Error(\"SignInt8(5) should be 1\")\n\t}\n\tif SignInt8(0) != 0 {\n\t\tt.Error(\"SignInt8(0) should be 0\")\n\t}\n\n}\n\nfunc TestInt16Helpers(t *testing.T) {\n\t// Test MaxInt16\n\tif MaxInt16(1, 2) != 2 {\n\t\tt.Error(\"MaxInt16(1, 2) should be 2\")\n\t}\n\tif MaxInt16(-1, -2) != -1 {\n\t\tt.Error(\"MaxInt16(-1, -2) should be -1\")\n\t}\n\n\t// Test MinInt16\n\tif MinInt16(1, 2) != 1 {\n\t\tt.Error(\"MinInt16(1, 2) should be 1\")\n\t}\n\tif MinInt16(-1, -2) != -2 {\n\t\tt.Error(\"MinInt16(-1, -2) should be -2\")\n\t}\n\n\t// Test ClampInt16\n\tif ClampInt16(5, 1, 3) != 3 {\n\t\tt.Error(\"ClampInt16(5, 1, 3) should be 3\")\n\t}\n\tif ClampInt16(0, 1, 3) != 1 {\n\t\tt.Error(\"ClampInt16(0, 1, 3) should be 1\")\n\t}\n\tif ClampInt16(2, 1, 3) != 2 {\n\t\tt.Error(\"ClampInt16(2, 1, 3) should be 2\")\n\t}\n\n\t// Test AbsInt16\n\tif AbsInt16(-5) != 5 {\n\t\tt.Error(\"AbsInt16(-5) should be 5\")\n\t}\n\tif AbsInt16(5) != 5 {\n\t\tt.Error(\"AbsInt16(5) should be 5\")\n\t}\n\n\t// Test SignInt16\n\tif SignInt16(-5) != -1 {\n\t\tt.Error(\"SignInt16(-5) should be -1\")\n\t}\n\tif SignInt16(5) != 1 {\n\t\tt.Error(\"SignInt16(5) should be 1\")\n\t}\n\tif SignInt16(0) != 0 {\n\t\tt.Error(\"SignInt16(0) should be 0\")\n\t}\n\n}\n\nfunc TestInt32Helpers(t *testing.T) {\n\t// Test MaxInt32\n\tif MaxInt32(1, 2) != 2 {\n\t\tt.Error(\"MaxInt32(1, 2) should be 2\")\n\t}\n\tif MaxInt32(-1, -2) != -1 {\n\t\tt.Error(\"MaxInt32(-1, -2) should be -1\")\n\t}\n\n\t// Test MinInt32\n\tif MinInt32(1, 2) != 1 {\n\t\tt.Error(\"MinInt32(1, 2) should be 1\")\n\t}\n\tif MinInt32(-1, -2) != -2 {\n\t\tt.Error(\"MinInt32(-1, -2) should be -2\")\n\t}\n\n\t// Test ClampInt32\n\tif ClampInt32(5, 1, 3) != 3 {\n\t\tt.Error(\"ClampInt32(5, 1, 3) should be 3\")\n\t}\n\tif ClampInt32(0, 1, 3) != 1 {\n\t\tt.Error(\"ClampInt32(0, 1, 3) should be 1\")\n\t}\n\tif ClampInt32(2, 1, 3) != 2 {\n\t\tt.Error(\"ClampInt32(2, 1, 3) should be 2\")\n\t}\n\n\t// Test AbsInt32\n\tif AbsInt32(-5) != 5 {\n\t\tt.Error(\"AbsInt32(-5) should be 5\")\n\t}\n\tif AbsInt32(5) != 5 {\n\t\tt.Error(\"AbsInt32(5) should be 5\")\n\t}\n\n\t// Test SignInt32\n\tif SignInt32(-5) != -1 {\n\t\tt.Error(\"SignInt32(-5) should be -1\")\n\t}\n\tif SignInt32(5) != 1 {\n\t\tt.Error(\"SignInt32(5) should be 1\")\n\t}\n\tif SignInt32(0) != 0 {\n\t\tt.Error(\"SignInt32(0) should be 0\")\n\t}\n\n}\n\nfunc TestInt64Helpers(t *testing.T) {\n\t// Test MaxInt64\n\tif MaxInt64(1, 2) != 2 {\n\t\tt.Error(\"MaxInt64(1, 2) should be 2\")\n\t}\n\tif MaxInt64(-1, -2) != -1 {\n\t\tt.Error(\"MaxInt64(-1, -2) should be -1\")\n\t}\n\n\t// Test MinInt64\n\tif MinInt64(1, 2) != 1 {\n\t\tt.Error(\"MinInt64(1, 2) should be 1\")\n\t}\n\tif MinInt64(-1, -2) != -2 {\n\t\tt.Error(\"MinInt64(-1, -2) should be -2\")\n\t}\n\n\t// Test ClampInt64\n\tif ClampInt64(5, 1, 3) != 3 {\n\t\tt.Error(\"ClampInt64(5, 1, 3) should be 3\")\n\t}\n\tif ClampInt64(0, 1, 3) != 1 {\n\t\tt.Error(\"ClampInt64(0, 1, 3) should be 1\")\n\t}\n\tif ClampInt64(2, 1, 3) != 2 {\n\t\tt.Error(\"ClampInt64(2, 1, 3) should be 2\")\n\t}\n\n\t// Test AbsInt64\n\tif AbsInt64(-5) != 5 {\n\t\tt.Error(\"AbsInt64(-5) should be 5\")\n\t}\n\tif AbsInt64(5) != 5 {\n\t\tt.Error(\"AbsInt64(5) should be 5\")\n\t}\n\n\t// Test SignInt64\n\tif SignInt64(-5) != -1 {\n\t\tt.Error(\"SignInt64(-5) should be -1\")\n\t}\n\tif SignInt64(5) != 1 {\n\t\tt.Error(\"SignInt64(5) should be 1\")\n\t}\n\tif SignInt64(0) != 0 {\n\t\tt.Error(\"SignInt64(0) should be 0\")\n\t}\n\n}\n\nfunc TestIntHelpers(t *testing.T) {\n\t// Test MaxInt\n\tif MaxInt(1, 2) != 2 {\n\t\tt.Error(\"MaxInt(1, 2) should be 2\")\n\t}\n\tif MaxInt(-1, -2) != -1 {\n\t\tt.Error(\"MaxInt(-1, -2) should be -1\")\n\t}\n\n\t// Test MinInt\n\tif MinInt(1, 2) != 1 {\n\t\tt.Error(\"MinInt(1, 2) should be 1\")\n\t}\n\tif MinInt(-1, -2) != -2 {\n\t\tt.Error(\"MinInt(-1, -2) should be -2\")\n\t}\n\n\t// Test ClampInt\n\tif ClampInt(5, 1, 3) != 3 {\n\t\tt.Error(\"ClampInt(5, 1, 3) should be 3\")\n\t}\n\tif ClampInt(0, 1, 3) != 1 {\n\t\tt.Error(\"ClampInt(0, 1, 3) should be 1\")\n\t}\n\tif ClampInt(2, 1, 3) != 2 {\n\t\tt.Error(\"ClampInt(2, 1, 3) should be 2\")\n\t}\n\n\t// Test AbsInt\n\tif AbsInt(-5) != 5 {\n\t\tt.Error(\"AbsInt(-5) should be 5\")\n\t}\n\tif AbsInt(5) != 5 {\n\t\tt.Error(\"AbsInt(5) should be 5\")\n\t}\n\n\t// Test SignInt\n\tif SignInt(-5) != -1 {\n\t\tt.Error(\"SignInt(-5) should be -1\")\n\t}\n\tif SignInt(5) != 1 {\n\t\tt.Error(\"SignInt(5) should be 1\")\n\t}\n\tif SignInt(0) != 0 {\n\t\tt.Error(\"SignInt(0) should be 0\")\n\t}\n\n}\n\nfunc TestUint8Helpers(t *testing.T) {\n\t// Test MaxUint8\n\tif MaxUint8(1, 2) != 2 {\n\t\tt.Error(\"MaxUint8(1, 2) should be 2\")\n\t}\n\n\t// Test MinUint8\n\tif MinUint8(1, 2) != 1 {\n\t\tt.Error(\"MinUint8(1, 2) should be 1\")\n\t}\n\n\t// Test ClampUint8\n\tif ClampUint8(5, 1, 3) != 3 {\n\t\tt.Error(\"ClampUint8(5, 1, 3) should be 3\")\n\t}\n\tif ClampUint8(0, 1, 3) != 1 {\n\t\tt.Error(\"ClampUint8(0, 1, 3) should be 1\")\n\t}\n\tif ClampUint8(2, 1, 3) != 2 {\n\t\tt.Error(\"ClampUint8(2, 1, 3) should be 2\")\n\t}\n\n}\n\nfunc TestUint16Helpers(t *testing.T) {\n\t// Test MaxUint16\n\tif MaxUint16(1, 2) != 2 {\n\t\tt.Error(\"MaxUint16(1, 2) should be 2\")\n\t}\n\n\t// Test MinUint16\n\tif MinUint16(1, 2) != 1 {\n\t\tt.Error(\"MinUint16(1, 2) should be 1\")\n\t}\n\n\t// Test ClampUint16\n\tif ClampUint16(5, 1, 3) != 3 {\n\t\tt.Error(\"ClampUint16(5, 1, 3) should be 3\")\n\t}\n\tif ClampUint16(0, 1, 3) != 1 {\n\t\tt.Error(\"ClampUint16(0, 1, 3) should be 1\")\n\t}\n\tif ClampUint16(2, 1, 3) != 2 {\n\t\tt.Error(\"ClampUint16(2, 1, 3) should be 2\")\n\t}\n\n}\n\nfunc TestUint32Helpers(t *testing.T) {\n\t// Test MaxUint32\n\tif MaxUint32(1, 2) != 2 {\n\t\tt.Error(\"MaxUint32(1, 2) should be 2\")\n\t}\n\n\t// Test MinUint32\n\tif MinUint32(1, 2) != 1 {\n\t\tt.Error(\"MinUint32(1, 2) should be 1\")\n\t}\n\n\t// Test ClampUint32\n\tif ClampUint32(5, 1, 3) != 3 {\n\t\tt.Error(\"ClampUint32(5, 1, 3) should be 3\")\n\t}\n\tif ClampUint32(0, 1, 3) != 1 {\n\t\tt.Error(\"ClampUint32(0, 1, 3) should be 1\")\n\t}\n\tif ClampUint32(2, 1, 3) != 2 {\n\t\tt.Error(\"ClampUint32(2, 1, 3) should be 2\")\n\t}\n\n}\n\nfunc TestUint64Helpers(t *testing.T) {\n\t// Test MaxUint64\n\tif MaxUint64(1, 2) != 2 {\n\t\tt.Error(\"MaxUint64(1, 2) should be 2\")\n\t}\n\n\t// Test MinUint64\n\tif MinUint64(1, 2) != 1 {\n\t\tt.Error(\"MinUint64(1, 2) should be 1\")\n\t}\n\n\t// Test ClampUint64\n\tif ClampUint64(5, 1, 3) != 3 {\n\t\tt.Error(\"ClampUint64(5, 1, 3) should be 3\")\n\t}\n\tif ClampUint64(0, 1, 3) != 1 {\n\t\tt.Error(\"ClampUint64(0, 1, 3) should be 1\")\n\t}\n\tif ClampUint64(2, 1, 3) != 2 {\n\t\tt.Error(\"ClampUint64(2, 1, 3) should be 2\")\n\t}\n\n}\n\nfunc TestUintHelpers(t *testing.T) {\n\t// Test MaxUint\n\tif MaxUint(1, 2) != 2 {\n\t\tt.Error(\"MaxUint(1, 2) should be 2\")\n\t}\n\n\t// Test MinUint\n\tif MinUint(1, 2) != 1 {\n\t\tt.Error(\"MinUint(1, 2) should be 1\")\n\t}\n\n\t// Test ClampUint\n\tif ClampUint(5, 1, 3) != 3 {\n\t\tt.Error(\"ClampUint(5, 1, 3) should be 3\")\n\t}\n\tif ClampUint(0, 1, 3) != 1 {\n\t\tt.Error(\"ClampUint(0, 1, 3) should be 1\")\n\t}\n\tif ClampUint(2, 1, 3) != 2 {\n\t\tt.Error(\"ClampUint(2, 1, 3) should be 2\")\n\t}\n\n}\n\nfunc TestFloat32Helpers(t *testing.T) {\n\t// Test MaxFloat32\n\tif MaxFloat32(1, 2) != 2 {\n\t\tt.Error(\"MaxFloat32(1, 2) should be 2\")\n\t}\n\tif MaxFloat32(-1, -2) != -1 {\n\t\tt.Error(\"MaxFloat32(-1, -2) should be -1\")\n\t}\n\n\t// Test MinFloat32\n\tif MinFloat32(1, 2) != 1 {\n\t\tt.Error(\"MinFloat32(1, 2) should be 1\")\n\t}\n\tif MinFloat32(-1, -2) != -2 {\n\t\tt.Error(\"MinFloat32(-1, -2) should be -2\")\n\t}\n\n\t// Test ClampFloat32\n\tif ClampFloat32(5, 1, 3) != 3 {\n\t\tt.Error(\"ClampFloat32(5, 1, 3) should be 3\")\n\t}\n\tif ClampFloat32(0, 1, 3) != 1 {\n\t\tt.Error(\"ClampFloat32(0, 1, 3) should be 1\")\n\t}\n\tif ClampFloat32(2, 1, 3) != 2 {\n\t\tt.Error(\"ClampFloat32(2, 1, 3) should be 2\")\n\t}\n\n\t// Test AbsFloat32\n\tif AbsFloat32(-5) != 5 {\n\t\tt.Error(\"AbsFloat32(-5) should be 5\")\n\t}\n\tif AbsFloat32(5) != 5 {\n\t\tt.Error(\"AbsFloat32(5) should be 5\")\n\t}\n\n\t// Test SignFloat32\n\tif SignFloat32(-5) != -1 {\n\t\tt.Error(\"SignFloat32(-5) should be -1\")\n\t}\n\tif SignFloat32(5) != 1 {\n\t\tt.Error(\"SignFloat32(5) should be 1\")\n\t}\n\tif SignFloat32(0.0) != 0 {\n\t\tt.Error(\"SignFloat32(0.0) should be 0\")\n\t}\n\n}\n\nfunc TestFloat64Helpers(t *testing.T) {\n\t// Test MaxFloat64\n\tif MaxFloat64(1, 2) != 2 {\n\t\tt.Error(\"MaxFloat64(1, 2) should be 2\")\n\t}\n\tif MaxFloat64(-1, -2) != -1 {\n\t\tt.Error(\"MaxFloat64(-1, -2) should be -1\")\n\t}\n\n\t// Test MinFloat64\n\tif MinFloat64(1, 2) != 1 {\n\t\tt.Error(\"MinFloat64(1, 2) should be 1\")\n\t}\n\tif MinFloat64(-1, -2) != -2 {\n\t\tt.Error(\"MinFloat64(-1, -2) should be -2\")\n\t}\n\n\t// Test ClampFloat64\n\tif ClampFloat64(5, 1, 3) != 3 {\n\t\tt.Error(\"ClampFloat64(5, 1, 3) should be 3\")\n\t}\n\tif ClampFloat64(0, 1, 3) != 1 {\n\t\tt.Error(\"ClampFloat64(0, 1, 3) should be 1\")\n\t}\n\tif ClampFloat64(2, 1, 3) != 2 {\n\t\tt.Error(\"ClampFloat64(2, 1, 3) should be 2\")\n\t}\n\n\t// Test AbsFloat64\n\tif AbsFloat64(-5) != 5 {\n\t\tt.Error(\"AbsFloat64(-5) should be 5\")\n\t}\n\tif AbsFloat64(5) != 5 {\n\t\tt.Error(\"AbsFloat64(5) should be 5\")\n\t}\n\n\t// Test SignFloat64\n\tif SignFloat64(-5) != -1 {\n\t\tt.Error(\"SignFloat64(-5) should be -1\")\n\t}\n\tif SignFloat64(5) != 1 {\n\t\tt.Error(\"SignFloat64(5) should be 1\")\n\t}\n\tif SignFloat64(0.0) != 0 {\n\t\tt.Error(\"SignFloat64(0.0) should be 0\")\n\t}\n\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"0AUARJqAaBEZCas1rHoT6WfMZXKtApCTQx5w7J4+rww3rrR6Xp6gb/WxvNvsV3tWRRfbm6xqc7prycReS44s0Q=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"brainfuck","path":"gno.land/p/n2p5/brainfuck","files":[{"name":"brainfuck.gno","body":"// Package brainfuck implements a minimalist Brainfuck language interpreter.\npackage brainfuck\n\n// Run executes a byte slice encoded Brainfuck program with the provided input and\n// returns the output. If no input is desired, use an empty string \"\". Memory is\n// dynamically allocated and grows rightward as needed. The pointer cannot go below\n// zero (does not wrap from the beginning to end). When input is exhausted, the ','\n// command writes 0 to the current cell. Program exits early if unmatched brackets\n// are encountered.\nfunc Run(code, input []byte) []byte {\n\tmemory := []byte{} // dynamic memory tape\n\toutput := []byte{} // collected output bytes\n\tpointer := 0       // memory cell pointer\n\tcursor := 0        // code instruction pointer\n\ti := 0             // input index\n\n\tfor cursor \u003c len(code) {\n\t\tswitch code[cursor] {\n\t\tcase '\u003e': // Move pointer right\n\t\t\tpointer++\n\t\t\tif pointer \u003c= 0 { // Guard against potential overflow\n\t\t\t\tpointer = 0\n\t\t\t}\n\t\t\tensureMemory(\u0026memory, pointer)\n\t\tcase '\u003c': // Move pointer left\n\t\t\tpointer--\n\t\t\tif pointer \u003c= 0 { // Guard against below zero\n\t\t\t\tpointer = 0\n\t\t\t}\n\t\tcase '+': // Increment current cell (wraps: 255+1=0)\n\t\t\tensureMemory(\u0026memory, pointer)\n\t\t\tmemory[pointer]++\n\t\tcase '-': // Decrement current cell (wraps: 0-1=255)\n\t\t\tensureMemory(\u0026memory, pointer)\n\t\t\tmemory[pointer]--\n\t\tcase '.': // Output current cell\n\t\t\tensureMemory(\u0026memory, pointer)\n\t\t\toutput = append(output, memory[pointer])\n\t\tcase ',': // Read one byte from input\n\t\t\tensureMemory(\u0026memory, pointer)\n\t\t\tif i \u003c len(input) {\n\t\t\t\tmemory[pointer] = input[i]\n\t\t\t\ti++\n\t\t\t} else {\n\t\t\t\tmemory[pointer] = 0 // Write 0 when input is exhausted\n\t\t\t}\n\t\tcase '[': // Jump forward to matching ] if current cell is 0\n\t\t\tensureMemory(\u0026memory, pointer)\n\t\t\tif memory[pointer] == 0 {\n\t\t\t\tbrackets := 1 // Track nesting level\n\t\t\t\tfor brackets \u003e 0 {\n\t\t\t\t\tcursor++\n\t\t\t\t\tif cursor \u003e= len(code) {\n\t\t\t\t\t\treturn output // Exit if unmatched bracket\n\t\t\t\t\t}\n\t\t\t\t\tif code[cursor] == '[' {\n\t\t\t\t\t\tbrackets++\n\t\t\t\t\t} else if code[cursor] == ']' {\n\t\t\t\t\t\tbrackets--\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tcase ']': // Jump back to matching [ if current cell is not 0\n\t\t\tensureMemory(\u0026memory, pointer)\n\t\t\tif memory[pointer] != 0 {\n\t\t\t\tbrackets := 1 // Track nesting level\n\t\t\t\tfor brackets \u003e 0 {\n\t\t\t\t\tcursor--\n\t\t\t\t\tif cursor \u003c 0 {\n\t\t\t\t\t\treturn output // Exit if unmatched bracket\n\t\t\t\t\t}\n\t\t\t\t\tif code[cursor] == ']' {\n\t\t\t\t\t\tbrackets++\n\t\t\t\t\t} else if code[cursor] == '[' {\n\t\t\t\t\t\tbrackets--\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tcursor++\n\t\tif cursor \u003c 0 { // Guard cursor against potential overflow\n\t\t\tcursor = 0\n\t\t}\n\t\tif i \u003c 0 { // Guard input index against potential overflow\n\t\t\ti = 0\n\t\t}\n\t}\n\treturn output\n}\n\n// ensureMemory grows the memory slice if needed to accommodate the specified\n// pointer position. New cells are initialized to zero.\nfunc ensureMemory(memory *[]byte, pointer int) {\n\tfor pointer \u003e= len(*memory) {\n\t\t*memory = append(*memory, 0)\n\t}\n}\n"},{"name":"brainfuck_test.gno","body":"package brainfuck\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n)\n\nfunc TestBasicOperations(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tcode     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"Increment and Output\",\n\t\t\tcode:     \"+++.\",\n\t\t\tinput:    \"\",\n\t\t\texpected: \"\\x03\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Decrement and Output\",\n\t\t\tcode:     \"+++--.\",\n\t\t\tinput:    \"\",\n\t\t\texpected: \"\\x01\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Pointer Movement\",\n\t\t\tcode:     \"\u003e+++\u003e++\u003e+\u003c\u003c\u003c.\u003e.\u003e.\u003e.\",\n\t\t\tinput:    \"\",\n\t\t\texpected: \"\\x00\\x03\\x02\\x01\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Pointer at Zero Boundary\",\n\t\t\tcode:     \"\u003c\u003c\u003c+++.\",\n\t\t\tinput:    \"\",\n\t\t\texpected: \"\\x03\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\toutput := Run([]byte(tc.code), []byte(tc.input))\n\t\t\tif !bytes.Equal(output, []byte(tc.expected)) {\n\t\t\t\tt.Errorf(\"Expected %v, got %v\", []byte(tc.expected), output)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLoops(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tcode     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"Simple Loop\",\n\t\t\tcode:     \"+++[\u003e+\u003c-]\u003e.\",\n\t\t\tinput:    \"\",\n\t\t\texpected: \"\\x03\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Nested Loops\",\n\t\t\tcode:     \"++[\u003e++[\u003e++\u003c-]\u003c-]\u003e\u003e.\",\n\t\t\tinput:    \"\",\n\t\t\texpected: \"\\x08\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Skip Loop if Zero\",\n\t\t\tcode:     \"[\u003e+++\u003c-]\u003e.\",\n\t\t\tinput:    \"\",\n\t\t\texpected: \"\\x00\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Unmatched Opening Bracket\",\n\t\t\tcode:     \"[.\",\n\t\t\tinput:    \"\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Unmatched Closing Bracket\",\n\t\t\tcode:     \"].\",\n\t\t\tinput:    \"\",\n\t\t\texpected: \"\\x00\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\toutput := Run([]byte(tc.code), []byte(tc.input))\n\t\t\tif !bytes.Equal(output, []byte(tc.expected)) {\n\t\t\t\tt.Errorf(\"Expected %v, got %v\", []byte(tc.expected), output)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestInput(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tcode     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"Read Single Input\",\n\t\t\tcode:     \",.\",\n\t\t\tinput:    \"A\",\n\t\t\texpected: \"A\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Read Multiple Inputs\",\n\t\t\tcode:     \",\u003e,\u003e,.\",\n\t\t\tinput:    \"ABC\",\n\t\t\texpected: \"C\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Input Modification\",\n\t\t\tcode:     \",+.\",\n\t\t\tinput:    \"A\",\n\t\t\texpected: \"B\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Input Exhaustion\",\n\t\t\tcode:     \",.,.,.,..\",\n\t\t\tinput:    \"AB\",\n\t\t\texpected: \"AB\\x00\\x00\\x00\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Output Input Stream\",\n\t\t\tcode:     \",[.,]\",\n\t\t\tinput:    \"hello, world!\\n\",\n\t\t\texpected: \"hello, world!\\n\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\toutput := Run([]byte(tc.code), []byte(tc.input))\n\t\t\tif !bytes.Equal(output, []byte(tc.expected)) {\n\t\t\t\tt.Errorf(\"Expected %v, got %v\", []byte(tc.expected), output)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBsort(t *testing.T) {\n\t// [bsort.b -- bubble sort\n\t// (c) 2016 Daniel B. Cristofani\n\t// http://brainfuck.org/]\n\t// [This program sorts the bytes of its input by bubble sort.]\n\tcode := `\n\t\t\u003e\u003e,[\u003e\u003e,]\u003c\u003c[\n\t\t[\u003c\u003c]\u003e\u003e\u003e\u003e[\n\t\t\u003c\u003c[\u003e+\u003c\u003c+\u003e-]\n\t\t\u003e\u003e[\u003e+\u003c\u003c\u003c\u003c[-\u003e]\u003e[\u003c]\u003e\u003e-]\n\t\t\u003c\u003c\u003c[[-]\u003e\u003e[\u003e+\u003c-]\u003e\u003e[\u003c\u003c\u003c+\u003e\u003e\u003e-]]\n\t\t\u003e\u003e[[\u003c+\u003e-]\u003e\u003e]\u003c\n\t\t]\u003c\u003c[\u003e\u003e+\u003c\u003c-]\u003c\u003c\n\t\t]\u003e\u003e\u003e\u003e[.\u003e\u003e]`\n\tinput := \"ckduwcomaz\"\n\texpected := \"accdkmouwz\"\n\n\toutput := Run([]byte(code), []byte(input))\n\tif string(output) != expected {\n\t\tt.Errorf(\"Hello World test failed. Expected '%s', got '%s'\", expected, string(output))\n\t}\n}\n\nfunc TestHelloWorld(t *testing.T) {\n\t// Classic \"Hello World!\" program from wikipedia\n\tcode := `\n+++++ +++++             initialize counter (cell #0) to 10\n[                       use loop to set 70/100/30/10\n    \u003e +++++ ++              add  7 to cell #1\n    \u003e +++++ +++++           add 10 to cell #2\n    \u003e +++                   add  3 to cell #3\n    \u003e +                     add  1 to cell #4\n\u003c\u003c\u003c\u003c -                  decrement counter (cell #0)\n]\n\u003e ++ .                  print 'H'\n\u003e + .                   print 'e'\n+++++ ++ .              print 'l'\n.                       print 'l'\n+++ .                   print 'o'\n\u003e ++ .                  print ' '\n\u003c\u003c +++++ +++++ +++++ .  print 'W'\n\u003e .                     print 'o'\n+++ .                   print 'r'\n----- - .               print 'l'\n----- --- .             print 'd'\n\u003e + .                   print '!'\n\u003e .                     print 'eol'`\n\texpected := \"Hello World!\\n\"\n\n\toutput := Run([]byte(code), []byte{})\n\tif string(output) != expected {\n\t\tt.Errorf(\"Hello World test failed. Expected '%s', got '%s'\", expected, string(output))\n\t}\n}\n\nfunc TestEnsureMemory(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tinitialMem  []byte\n\t\tpointer     int\n\t\texpectedLen int\n\t}{\n\t\t{\n\t\t\tname:        \"Already Sufficient\",\n\t\t\tinitialMem:  []byte{0, 0, 0},\n\t\t\tpointer:     2,\n\t\t\texpectedLen: 3,\n\t\t},\n\t\t{\n\t\t\tname:        \"Extend By One\",\n\t\t\tinitialMem:  []byte{0, 0, 0},\n\t\t\tpointer:     3,\n\t\t\texpectedLen: 4,\n\t\t},\n\t\t{\n\t\t\tname:        \"Extend By Many\",\n\t\t\tinitialMem:  []byte{},\n\t\t\tpointer:     10,\n\t\t\texpectedLen: 11,\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tmemory := tc.initialMem\n\t\t\tensureMemory(\u0026memory, tc.pointer)\n\n\t\t\tif len(memory) != tc.expectedLen {\n\t\t\t\tt.Errorf(\"Expected memory length %d, got %d\", tc.expectedLen, len(memory))\n\t\t\t}\n\n\t\t\t// Check that we can access the pointer without panic\n\t\t\t_ = memory[tc.pointer]\n\t\t})\n\t}\n}\n\n// Test various complex programs to ensure the interpreter works correctly\nfunc TestComplexPrograms(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tcode     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"Multiply Two Numbers\",\n\t\t\tcode:     \",\u003e,\u003c[\u003e[-\u003e+\u003e+\u003c\u003c]\u003e\u003e[-\u003c\u003c+\u003e\u003e]\u003c\u003c\u003c-]\u003e\u003e.\",\n\t\t\tinput:    \"\\x03\\x04\", // 3 * 4 = 12\n\t\t\texpected: \"\\x0c\",     // 12 in ASCII\n\t\t},\n\t\t{\n\t\t\tname:     \"Cat Program\",\n\t\t\tcode:     \",[.,]\",\n\t\t\tinput:    \"Hello!\",\n\t\t\texpected: \"Hello!\",\n\t\t},\n\t\t{\n\t\t\tname:     \"ASCII to Decimal\",\n\t\t\tcode:     \",\u003e++++++[\u003c--------\u003e-]\u003c.\", // ASCII value minus 48\n\t\t\tinput:    \"5\",                       // ASCII 53\n\t\t\texpected: \"\\x05\",                    // 5 in decimal\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\toutput := Run([]byte(tc.code), []byte(tc.input))\n\t\t\tif !bytes.Equal(output, []byte(tc.expected)) {\n\t\t\t\tt.Errorf(\"Expected %v, got %v\", []byte(tc.expected), output)\n\t\t\t}\n\t\t})\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/n2p5/brainfuck\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"A/DgjGZjHwuR0mZ5ixr3wc3MkxltINfZqL6e4VXOpf0xqEtXqT9xYStKiBSnS6cziCu35hLTvNG2+PVfoFhLPg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"chonk","path":"gno.land/p/n2p5/chonk","files":[{"name":"chonk.gno","body":"// Package chonk provides a simple way to store arbitrarily large strings\n// in a linked list across transactions for efficient storage and retrieval.\n// A Chonk support three operations: Add, Flush, and Scanner.\n// - Add appends a string to the Chonk.\n// - Flush clears the Chonk.\n// - Scanner is used to iterate over the chunks in the Chonk.\npackage chonk\n\n// Chonk is a linked list string storage and\n// retrieval system for fine bois.\ntype Chonk struct {\n\tfirst *chunk\n\tlast  *chunk\n}\n\n// chunk is a linked list node for Chonk\ntype chunk struct {\n\ttext string\n\tnext *chunk\n}\n\n// New creates a reference to a new Chonk\nfunc New() *Chonk {\n\treturn \u0026Chonk{}\n}\n\n// Add appends a string to the Chonk. If the Chonk is empty,\n// the string will be the first and last chunk. Otherwise,\n// the string will be appended to the end of the Chonk.\nfunc (c *Chonk) Add(text string) {\n\tnext := \u0026chunk{text: text}\n\tif c.first == nil {\n\t\tc.first = next\n\t\tc.last = next\n\t\treturn\n\t}\n\tc.last.next = next\n\tc.last = next\n}\n\n// Flush clears the Chonk by setting the first and last\n// chunks to nil. This will allow the garbage collector to\n// free the memory used by the Chonk.\nfunc (c *Chonk) Flush() {\n\tc.first = nil\n\tc.last = nil\n}\n\n// Scanner returns a new Scanner for the Chonk. The Scanner\n// is used to iterate over the chunks in the Chonk.\nfunc (c *Chonk) Scanner() *Scanner {\n\treturn \u0026Scanner{\n\t\tnext: c.first,\n\t}\n}\n\n// Scanner is a simple string scanner for Chonk. It is used\n// to iterate over the chunks in a Chonk from first to last.\ntype Scanner struct {\n\tcurrent *chunk\n\tnext    *chunk\n}\n\n// Scan advances the scanner to the next chunk. It returns\n// true if there is a next chunk, and false if there is not.\nfunc (s *Scanner) Scan() bool {\n\tif s.next != nil {\n\t\ts.current = s.next\n\t\ts.next = s.next.next\n\t\treturn true\n\t}\n\treturn false\n}\n\n// Text returns the current chunk. It is only valid to call\n// this method after a call to Scan returns true. Expected usage:\n//\n//\t\tscanner := chonk.Scanner()\n//\t\t\tfor scanner.Scan() {\n//\t    \t\tfmt.Println(scanner.Text())\n//\t\t\t}\nfunc (s *Scanner) Text() string {\n\treturn s.current.text\n}\n"},{"name":"chonk_test.gno","body":"package chonk\n\nimport (\n\t\"testing\"\n)\n\nfunc TestChonk(t *testing.T) {\n\tt.Parallel()\n\tc := New()\n\ttestTable := []struct {\n\t\tname   string\n\t\tchunks []string\n\t}{\n\t\t{\n\t\t\tname:   \"empty\",\n\t\t\tchunks: []string{},\n\t\t},\n\t\t{\n\t\t\tname:   \"single chunk\",\n\t\t\tchunks: []string{\"a\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"multiple chunks\",\n\t\t\tchunks: []string{\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\", \"i\", \"j\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"multiline chunks\",\n\t\t\tchunks: []string{\"1a\\nb\\nc\\n\\n\", \"d\\ne\\nf\", \"g\\nh\\ni\", \"j\\nk\\nl\\n\\n\\n\\n\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"empty\",\n\t\t\tchunks: []string{},\n\t\t},\n\t}\n\ttestChonk := func(t *testing.T, c *Chonk, chunks []string) {\n\t\tfor _, chunk := range chunks {\n\t\t\tc.Add(chunk)\n\t\t}\n\t\tscanner := c.Scanner()\n\t\ti := 0\n\t\tfor scanner.Scan() {\n\t\t\tif scanner.Text() != chunks[i] {\n\t\t\t\tt.Errorf(\"expected %s, got %s\", chunks[i], scanner.Text())\n\t\t\t}\n\t\t\ti++\n\t\t}\n\t}\n\tfor _, test := range testTable {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\ttestChonk(t, c, test.chunks)\n\t\t\tc.Flush()\n\t\t})\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/n2p5/chonk\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"nW1OtX8EtbBA6QPZg1CJoqZwznGipzTKYIyn4veW+bBAfZNrpmnrmLszOf3iHqSUR/2lEBSOsHT1S/2854sUxQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"needle","path":"gno.land/p/n2p5/haystack/needle","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/n2p5/haystack/needle\"\ngno = \"0.9\"\n"},{"name":"needle.gno","body":"package needle\n\nimport (\n\t\"bytes\"\n\t\"crypto/sha256\"\n\t\"errors\"\n)\n\nconst (\n\t// HashLength is the length in bytes of the hash prefix in any message\n\tHashLength = 32\n\t// PayloadLength is the length of the remaining bytes of the message.\n\tPayloadLength = 160\n\t// NeedleLength is the number of bytes required for a valid needle.\n\tNeedleLength = HashLength + PayloadLength\n)\n\n// Needle is a container for a 160 byte payload\n// and a 32 byte sha256 hash of the payload.\ntype Needle struct {\n\thash    [HashLength]byte\n\tpayload [PayloadLength]byte\n}\n\nvar (\n\t// ErrorInvalidHash is an error for in invalid hash\n\tErrorInvalidHash = errors.New(\"invalid hash\")\n\t// ErrorByteSliceLength is an error for an invalid byte slice length passed in to New or FromBytes\n\tErrorByteSliceLength = errors.New(\"invalid byte slice length\")\n)\n\n// New creates a Needle used for submitting a payload to a Haystack sever. It takes a Payload\n// byte slice that is 160 bytes in length and returns a reference to a\n// Needle and an error. The purpose of this function is to make it\n// easy to create a new Needle from a payload. This function handles creating a sha256\n// hash of the payload, which is used by the Needle to submit to a haystack server.\nfunc New(p []byte) (*Needle, error) {\n\tif len(p) != PayloadLength {\n\t\treturn nil, ErrorByteSliceLength\n\t}\n\tvar n Needle\n\tsum := sha256.Sum256(p)\n\tcopy(n.hash[:], sum[:])\n\tcopy(n.payload[:], p)\n\treturn \u0026n, nil\n}\n\n// FromBytes is intended convert raw bytes (from UDP or storage) into a Needle.\n// It takes a byte slice and expects it to be exactly the length of NeedleLength.\n// The byte slice should consist of the first 32 bytes being the sha256 hash of the\n// payload and the payload bytes. This function verifies the length of the byte slice,\n// copies the bytes into a private [192]byte array, and validates the Needle. It returns\n// a reference to a Needle and an error.\nfunc FromBytes(b []byte) (*Needle, error) {\n\tif len(b) != NeedleLength {\n\t\treturn nil, ErrorByteSliceLength\n\t}\n\tvar n Needle\n\tcopy(n.hash[:], b[:HashLength])\n\tcopy(n.payload[:], b[HashLength:])\n\tif err := n.validate(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn \u0026n, nil\n}\n\n// Hash returns a copy of the bytes of the sha256 256 hash of the Needle payload.\nfunc (n *Needle) Hash() []byte {\n\treturn n.Bytes()[:HashLength]\n}\n\n// Payload returns a byte slice of the Needle payload\nfunc (n *Needle) Payload() []byte {\n\treturn n.Bytes()[HashLength:]\n}\n\n// Bytes returns a byte slice of the entire 192 byte hash + payload\nfunc (n *Needle) Bytes() []byte {\n\tb := make([]byte, NeedleLength)\n\tcopy(b, n.hash[:])\n\tcopy(b[HashLength:], n.payload[:])\n\treturn b\n}\n\n// validate checks that a Needle has a valid hash, it returns either nil or an error.\nfunc (n *Needle) validate() error {\n\tif hash := sha256.Sum256(n.Payload()); !bytes.Equal(n.Hash(), hash[:]) {\n\t\treturn ErrorInvalidHash\n\t}\n\treturn nil\n}\n"},{"name":"needle_test.gno","body":"package needle\n\nimport (\n\t\"bytes\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"testing\"\n)\n\nfunc TestNeedle(t *testing.T) {\n\tt.Parallel()\n\tt.Run(\"Bytes\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tp, _ := hex.DecodeString(\"40e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1\")\n\t\tn, _ := New(p)\n\t\tb := n.Bytes()\n\t\tb[0], b[1], b[2], b[3] = 0, 0, 0, 0\n\t\tif bytes.Equal(n.Bytes(), b) {\n\t\t\tt.Error(\"mutating Bytes() changed needle bytes\")\n\t\t}\n\t})\n\tt.Run(\"Payload\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tp, _ := hex.DecodeString(\"40e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1\")\n\t\tn, _ := New(p)\n\t\tpayload := n.Payload()\n\t\tif !bytes.Equal(p, payload) {\n\t\t\tt.Error(\"payload imported by New does not match needle.Payload()\")\n\t\t}\n\t\tpayload[0] = 0\n\t\tpl := n.Payload()\n\t\tif bytes.Equal(pl, payload) {\n\t\t\tt.Error(\"mutating Payload() changed needle payload\")\n\t\t}\n\t})\n\tt.Run(\"Hash\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tp, _ := hex.DecodeString(\"40e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1\")\n\t\tn, _ := New(p)\n\t\thash := n.Hash()\n\t\th := sha256.Sum256(p)\n\t\tif !bytes.Equal(h[:], hash) {\n\t\t\tt.Error(\"exported hash is invalid\")\n\t\t}\n\t\thash[0] = 0\n\t\th2 := n.Hash()\n\t\tif bytes.Equal(h2, hash) {\n\t\t\tt.Error(\"mutating Hash() changed needle hash\")\n\t\t}\n\t})\n}\n\nfunc TestNew(t *testing.T) {\n\tt.Parallel()\n\n\tp, _ := hex.DecodeString(\"40e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1\")\n\texpected, _ := hex.DecodeString(\"f1b462c84a0c51dad44293951f0b084a8871b3700ac1b9fc7a53a20bc0ba0fed40e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1\")\n\n\ttestTable := []struct {\n\t\tpayload     []byte\n\t\texpected    []byte\n\t\thasError    bool\n\t\tdescription string\n\t}{\n\t\t{\n\t\t\tpayload:     p,\n\t\t\texpected:    expected,\n\t\t\thasError:    false,\n\t\t\tdescription: \"expected payload\",\n\t\t},\n\t\t{\n\t\t\tpayload:     p[:PayloadLength-1],\n\t\t\texpected:    nil,\n\t\t\thasError:    true,\n\t\t\tdescription: \"payload invalid length (too small)\",\n\t\t},\n\t\t{\n\t\t\tpayload:     append(p, byte(1)),\n\t\t\texpected:    nil,\n\t\t\thasError:    true,\n\t\t\tdescription: \"payload invalid length (too large)\",\n\t\t},\n\t}\n\n\tfor _, test := range testTable {\n\t\tn, err := New(test.payload)\n\t\tif err != nil {\n\t\t\tif !test.hasError {\n\t\t\t\tt.Errorf(\"test: %v had error: %v\", test.description, err)\n\t\t\t}\n\t\t} else if !bytes.Equal(n.Bytes(), test.expected) {\n\t\t\tt.Errorf(\"%v, bytes not equal\\n%x\\n%x\", test.description, n.Bytes(), test.expected)\n\t\t}\n\t}\n}\n\nfunc TestFromBytes(t *testing.T) {\n\tt.Parallel()\n\n\tvalidRaw, _ := hex.DecodeString(\"f1b462c84a0c51dad44293951f0b084a8871b3700ac1b9fc7a53a20bc0ba0fed40e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1\")\n\tvalidExpected, _ := hex.DecodeString(\"f1b462c84a0c51dad44293951f0b084a8871b3700ac1b9fc7a53a20bc0ba0fed40e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1\")\n\tinvalidHash, _ := hex.DecodeString(\"182e0ca0d2fb1da76da6caf36a9d0d2838655632e85891216dc8b545d8f1410940e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1\")\n\n\ttestTable := []struct {\n\t\trawBytes    []byte\n\t\texpected    []byte\n\t\thasError    bool\n\t\tdescription string\n\t}{\n\t\t{\n\t\t\trawBytes:    validRaw,\n\t\t\texpected:    validExpected,\n\t\t\thasError:    false,\n\t\t\tdescription: \"valid raw bytes\",\n\t\t},\n\t\t{\n\t\t\trawBytes:    make([]byte, 0),\n\t\t\texpected:    nil,\n\t\t\thasError:    true,\n\t\t\tdescription: \"empty bytes\",\n\t\t},\n\t\t{\n\t\t\trawBytes:    make([]byte, NeedleLength-1),\n\t\t\texpected:    nil,\n\t\t\thasError:    true,\n\t\t\tdescription: \"too few bytes, one less than expected\",\n\t\t},\n\t\t{\n\t\t\trawBytes:    make([]byte, 0),\n\t\t\texpected:    nil,\n\t\t\thasError:    true,\n\t\t\tdescription: \"too few bytes, no bytes\",\n\t\t},\n\t\t{\n\t\t\trawBytes:    make([]byte, NeedleLength+1),\n\t\t\texpected:    nil,\n\t\t\thasError:    true,\n\t\t\tdescription: \"too many bytes\",\n\t\t},\n\t\t{\n\t\t\trawBytes:    invalidHash,\n\t\t\texpected:    nil,\n\t\t\thasError:    true,\n\t\t\tdescription: \"invalid hash\",\n\t\t},\n\t}\n\tfor _, test := range testTable {\n\t\tn, err := FromBytes(test.rawBytes)\n\t\tif err != nil {\n\t\t\tif !test.hasError {\n\t\t\t\tt.Errorf(\"test: %v had error: %v\", test.description, err)\n\t\t\t}\n\t\t} else if !bytes.Equal(n.Bytes(), test.expected) {\n\t\t\tt.Errorf(\"%v, bytes not equal\\n%x\\n%x\", test.description, n.Bytes(), test.expected)\n\t\t}\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"NIzsjMagMyqFspbjM+JOT/rTNGGSUHruAFO17Juvsbc7aERTvlOZK53qYszVVJHes0eAiI/cguFessiri2X0qA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"haystack","path":"gno.land/p/n2p5/haystack","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/n2p5/haystack\"\ngno = \"0.9\"\n"},{"name":"haystack.gno","body":"package haystack\n\nimport (\n\t\"encoding/hex\"\n\t\"errors\"\n\n\t\"gno.land/p/n2p5/haystack/needle\"\n\t\"gno.land/p/nt/avl/v0\"\n)\n\nvar (\n\t// ErrorNeedleNotFound is returned when a needle is not found in the haystack.\n\tErrorNeedleNotFound = errors.New(\"needle not found\")\n\t// ErrorNeedleLength is returned when a needle is not the correct length.\n\tErrorNeedleLength = errors.New(\"invalid needle length\")\n\t// ErrorHashLength is returned when a needle hash is not the correct length.\n\tErrorHashLength = errors.New(\"invalid hash length\")\n\t// ErrorDuplicateNeedle is returned when a needle already exists in the haystack.\n\tErrorDuplicateNeedle = errors.New(\"needle already exists\")\n\t// ErrorHashMismatch is returned when a needle hash does not match the needle. This should\n\t// never happen and indicates a critical internal storage error.\n\tErrorHashMismatch = errors.New(\"storage error: hash mismatch\")\n\t// ErrorValueInvalidType is returned when a needle value is not a byte slice. This should\n\t// never happen and indicates a critical internal storage error.\n\tErrorValueInvalidType = errors.New(\"storage error: invalid value type, expected []byte\")\n)\n\nconst (\n\t// EncodedHashLength is the length of the hex-encoded needle hash.\n\tEncodedHashLength = needle.HashLength * 2\n\t// EncodedPayloadLength is the length of the hex-encoded needle payload.\n\tEncodedPayloadLength = needle.PayloadLength * 2\n\t// EncodedNeedleLength is the length of the hex-encoded needle.\n\tEncodedNeedleLength = EncodedHashLength + EncodedPayloadLength\n)\n\n// Haystack is a permissionless, append-only, content-addressed key-value store for fix\n// length messages known as needles. A needle is a 192 byte byte slice with a 32 byte\n// hash (sha256) and a 160 byte payload.\ntype Haystack struct{ internal *avl.Tree }\n\n// New creates a new instance of a Haystack key-value store.\nfunc New() *Haystack {\n\treturn \u0026Haystack{\n\t\tinternal: avl.NewTree(),\n\t}\n}\n\n// Add takes a fixed-length hex-encoded needle bytes and adds it to the haystack key-value\n// store. The key is the first 32 bytes of the needle hash (64 bytes hex-encoded) of the\n// sha256 sum of the payload. The value is the 160 byte byte slice of the needle payload.\n// An error is returned if the needle is found to be invalid.\nfunc (h *Haystack) Add(needleHex string) error {\n\tif len(needleHex) != EncodedNeedleLength {\n\t\treturn ErrorNeedleLength\n\t}\n\tb, err := hex.DecodeString(needleHex)\n\tif err != nil {\n\t\treturn err\n\t}\n\tn, err := needle.FromBytes(b)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif h.internal.Has(needleHex[:EncodedHashLength]) {\n\t\treturn ErrorDuplicateNeedle\n\t}\n\th.internal.Set(needleHex[:EncodedHashLength], n.Payload())\n\treturn nil\n}\n\n// Get takes a hex-encoded needle hash and returns the complete hex-encoded needle bytes\n// and an error. Errors covers errors that span from the needle not being found, internal\n// storage error inconsistencies, and invalid value types.\nfunc (h *Haystack) Get(hash string) (string, error) {\n\tif len(hash) != EncodedHashLength {\n\t\treturn \"\", ErrorHashLength\n\t}\n\tif _, err := hex.DecodeString(hash); err != nil {\n\t\treturn \"\", err\n\t}\n\tv := h.internal.Get(hash)\n\tif v == nil {\n\t\treturn \"\", ErrorNeedleNotFound\n\t}\n\tb, ok := v.([]byte)\n\tif !ok {\n\t\treturn \"\", ErrorValueInvalidType\n\t}\n\tn, err := needle.New(b)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tneedleHash := hex.EncodeToString(n.Hash())\n\tif needleHash != hash {\n\t\treturn \"\", ErrorHashMismatch\n\t}\n\treturn hex.EncodeToString(n.Bytes()), nil\n}\n"},{"name":"haystack_test.gno","body":"package haystack\n\nimport (\n\t\"encoding/hex\"\n\t\"testing\"\n\n\t\"gno.land/p/n2p5/haystack/needle\"\n)\n\nfunc TestHaystack(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"New\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\th := New()\n\t\tif h == nil {\n\t\t\tt.Error(\"New returned nil\")\n\t\t}\n\t})\n\n\tt.Run(\"Add\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\th := New()\n\t\tn, _ := needle.New(make([]byte, needle.PayloadLength))\n\t\tvalidNeedleHex := hex.EncodeToString(n.Bytes())\n\n\t\ttestTable := []struct {\n\t\t\tneedleHex string\n\t\t\terr       error\n\t\t}{\n\t\t\t{validNeedleHex, nil},\n\t\t\t{validNeedleHex, ErrorDuplicateNeedle},\n\t\t\t{\"bad\" + validNeedleHex[3:], needle.ErrorInvalidHash},\n\t\t\t{\"XXX\" + validNeedleHex[3:], hex.InvalidByteError('X')},\n\t\t\t{validNeedleHex[:len(validNeedleHex)-2], ErrorNeedleLength},\n\t\t\t{validNeedleHex + \"00\", ErrorNeedleLength},\n\t\t\t{\"000\", ErrorNeedleLength},\n\t\t}\n\t\tfor _, tt := range testTable {\n\t\t\terr := h.Add(tt.needleHex)\n\t\t\tif err != tt.err {\n\t\t\t\tt.Error(tt.needleHex, err.Error(), \"!=\", tt.err.Error())\n\t\t\t}\n\t\t}\n\t})\n\n\tt.Run(\"Get\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\th := New()\n\n\t\t// genNeedleHex returns a hex-encoded needle and its hash for a given index.\n\t\tgenNeedleHex := func(i int) (string, string) {\n\t\t\tb := make([]byte, needle.PayloadLength)\n\t\t\tb[0] = byte(i)\n\t\t\tn, _ := needle.New(b)\n\t\t\treturn hex.EncodeToString(n.Bytes()), hex.EncodeToString(n.Hash())\n\t\t}\n\n\t\t// Add a valid needle to the haystack.\n\t\tvalidNeedleHex, validHash := genNeedleHex(0)\n\t\th.Add(validNeedleHex)\n\n\t\t// Add a needle and break the value type.\n\t\t_, brokenHashValueType := genNeedleHex(1)\n\t\th.internal.Set(brokenHashValueType, 0)\n\n\t\t// Add a needle with invalid hash.\n\t\t_, invalidHash := genNeedleHex(2)\n\t\th.internal.Set(invalidHash, make([]byte, needle.PayloadLength))\n\n\t\ttestTable := []struct {\n\t\t\thash     string\n\t\t\texpected string\n\t\t\terr      error\n\t\t}{\n\t\t\t{validHash, validNeedleHex, nil},\n\t\t\t{validHash[:len(validHash)-2], \"\", ErrorHashLength},\n\t\t\t{validHash + \"00\", \"\", ErrorHashLength},\n\t\t\t{\"XXX\" + validHash[3:], \"\", hex.InvalidByteError('X')},\n\t\t\t{\"bad\" + validHash[3:], \"\", ErrorNeedleNotFound},\n\t\t\t{brokenHashValueType, \"\", ErrorValueInvalidType},\n\t\t\t{invalidHash, \"\", ErrorHashMismatch},\n\t\t}\n\t\tfor _, tt := range testTable {\n\t\t\tactual, err := h.Get(tt.hash)\n\t\t\tif err != tt.err {\n\t\t\t\tt.Error(tt.hash, err.Error(), \"!=\", tt.err.Error())\n\t\t\t}\n\t\t\tif actual != tt.expected {\n\t\t\t\tt.Error(tt.hash, actual, \"!=\", tt.expected)\n\t\t\t}\n\t\t}\n\t})\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"apSuC/aSw00r5HkTjZZkuhEHZyjK0QgA5x/TrnmKeaB//ol/m2yXWZWjL7R7CB9vA7Tqv5aJaVzhfnXl0/9Jwg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"loci","path":"gno.land/p/n2p5/loci","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/n2p5/loci\"\ngno = \"0.9\"\n"},{"name":"loci.gno","body":"// loci is a single purpose datastore keyed by the caller's address. It has two\n// functions: Set and Get. loci is plural for locus, which is a central or core\n// place where something is found or from which it originates. In this case,\n// it's a simple key-value store where an address (the key) can store exactly\n// one value (in the form of a byte slice). Only the caller can set the value\n// for their address, but anyone can retrieve the value for any address.\npackage loci\n\nimport (\n\t\"gno.land/p/nt/avl/v0\"\n)\n\n// LociStore is a simple key-value store that uses\n// an AVL tree to store the data.\ntype LociStore struct {\n\tinternal *avl.Tree\n}\n\n// New creates a reference to a new LociStore.\nfunc New() *LociStore {\n\treturn \u0026LociStore{\n\t\tinternal: avl.NewTree(),\n\t}\n}\n\n// Set stores a byte slice in the AVL tree using `rlm.Previous().Address()`\n// as the key. rlm must be the caller's own captured cur (asserted via\n// rlm.IsCurrent()); the previous walk used to be unsafe.PreviousRealm()\n// which is forgeable from non-crossing wrappers — an attacker realm\n// could overwrite another realm's slot. With IsCurrent + rlm.Previous(),\n// the slot key is unforgeable.\nfunc (s *LociStore) Set(_ int, rlm realm, value []byte) {\n\tif !rlm.IsCurrent() {\n\t\tpanic(\"loci: rlm does not match the current crossing frame\")\n\t}\n\tkey := string(rlm.Previous().Address())\n\ts.internal.Set(key, value)\n}\n\n// Get retrieves a byte slice from the AVL tree using the provided address.\n// The return values are the byte slice value and a boolean indicating\n// whether the value exists.\nfunc (s *LociStore) Get(addr address) []byte {\n\tvalue := s.internal.Get(string(addr))\n\tif value == nil {\n\t\treturn nil\n\t}\n\treturn value.([]byte)\n}\n"},{"name":"loci_test.gno","body":"package loci\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n)\n\nfunc TestLociStore(cur realm, t *testing.T) {\n\tcaller := testutils.TestAddress(\"caller\")\n\n\tt.Run(\"TestSet\", func(cur realm, t *testing.T) {\n\t\ttesting.SetRealm(testing.NewUserRealm(caller))\n\t\tstore := New()\n\t\t// Ensure that the value is nil before setting it.\n\t\tif r1 := store.Get(caller); r1 != nil {\n\t\t\tt.Errorf(\"expected value to be nil, got '%s'\", r1)\n\t\t}\n\t\tfunc(cur realm) { store.Set(0, cur, []byte(\"hello\")) }(cross(cur))\n\t\tif r2 := store.Get(caller); string(r2) != \"hello\" {\n\t\t\tt.Errorf(\"expected value to be 'hello', got '%s'\", r2)\n\t\t}\n\t\tfunc(cur realm) { store.Set(0, cur, []byte(\"world\")) }(cross(cur))\n\t\tif r3 := store.Get(caller); string(r3) != \"world\" {\n\t\t\tt.Errorf(\"expected value to be 'world', got '%s'\", r3)\n\t\t}\n\t})\n\tt.Run(\"TestGet\", func(cur realm, t *testing.T) {\n\t\ttesting.SetRealm(testing.NewUserRealm(caller))\n\t\tstore := New()\n\t\tfunc(cur realm) { store.Set(0, cur, []byte(\"hello\")) }(cross(cur))\n\t\tif r0 := store.Get(testutils.TestAddress(\"nil_user\")); r0 != nil {\n\t\t\tt.Errorf(\"expected value to be nil, got '%s'\", r0)\n\t\t}\n\t\tif r1 := store.Get(caller); string(r1) != \"hello\" {\n\t\t\tt.Errorf(\"expected value to be 'hello', got '%s'\", r1)\n\t\t}\n\t})\n}\n"},{"name":"z_0_filetest.gno","body":"// PKGPATH: gno.land/r/test\npackage test\n\nimport (\n\t\"gno.land/p/n2p5/loci\"\n)\n\nvar store *loci.LociStore\n\nfunc init() {\n\tstore = loci.New()\n}\n\nfunc main(cur realm) {\n\tstore.Set(0, cur, []byte(\"hello, world\"))\n\tprintln(string(store.Get(cur.Previous().Address())))\n}\n\n// Output:\n// hello, world\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"c3xv6tjFMefEimXR/6XtR9d0k3vIMxt1IKhQ2JGTSbg/oP61WIqM4PCrxwD1KsVmLLNfBqECUva40UJS69EfnA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"mgroup","path":"gno.land/p/n2p5/mgroup","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/n2p5/mgroup\"\ngno = \"0.9\"\n"},{"name":"mgroup.gno","body":"// Package mgroup is a simple managed group managing ownership and membership\n// for authorization in gno realms. The ManagedGroup struct is used to manage\n// the owner, backup owners, and members of a group. The owner is the primary\n// owner of the group and can add and remove backup owners and members. Backup\n// owners can claim ownership of the group. This is meant to provide backup\n// accounts for the owner in case the owner account is lost or compromised.\n// Members are used to authorize actions across realms.\npackage mgroup\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/ownable/v0\"\n)\n\nvar (\n\tErrCannotRemoveOwner = errors.New(\"mgroup: cannot remove owner\")\n\tErrNotBackupOwner    = errors.New(\"mgroup: not a backup owner\")\n\tErrNotMember         = errors.New(\"mgroup: not a member\")\n\tErrInvalidAddress    = errors.New(\"mgroup: address is invalid\")\n)\n\ntype ManagedGroup struct {\n\towner        *ownable.Ownable\n\tbackupOwners *avl.Tree\n\tmembers      *avl.Tree\n}\n\n// New creates a new ManagedGroup with the owner set to the provided address.\n// The owner is automatically added as a backup owner and member of the group.\nfunc New(ownerAddress address) *ManagedGroup {\n\tg := \u0026ManagedGroup{\n\t\towner:        ownable.NewWithAddress(ownerAddress),\n\t\tbackupOwners: avl.NewTree(),\n\t\tmembers:      avl.NewTree(),\n\t}\n\terr := g.addBackupOwner(ownerAddress)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\terr = g.addMember(ownerAddress)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn g\n}\n\n// AddBackupOwner adds a backup owner to the group by address. rlm must\n// be the caller's own captured cur (asserted via rlm.IsCurrent());\n// rlm.Previous().Address() must equal the owner.\nfunc (g *ManagedGroup) AddBackupOwner(_ int, rlm realm, addr address) error {\n\tif !rlm.IsCurrent() {\n\t\treturn ownable.ErrUnauthorized\n\t}\n\tif !g.owner.OwnedBy(rlm.Previous().Address()) {\n\t\treturn ownable.ErrUnauthorized\n\t}\n\treturn g.addBackupOwner(addr)\n}\n\nfunc (g *ManagedGroup) addBackupOwner(addr address) error {\n\tif !addr.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\tg.backupOwners.Set(addr.String(), struct{}{})\n\treturn nil\n}\n\n// RemoveBackupOwner removes a backup owner from the group by address.\n// The owner cannot be removed. rlm must be the caller's own captured\n// cur; rlm.Previous().Address() must equal the owner.\nfunc (g *ManagedGroup) RemoveBackupOwner(_ int, rlm realm, addr address) error {\n\tif !rlm.IsCurrent() {\n\t\treturn ownable.ErrUnauthorized\n\t}\n\tif !g.owner.OwnedBy(rlm.Previous().Address()) {\n\t\treturn ownable.ErrUnauthorized\n\t}\n\tif !addr.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\tif addr == g.Owner() {\n\t\treturn ErrCannotRemoveOwner\n\t}\n\tg.backupOwners.Remove(addr.String())\n\treturn nil\n}\n\n// ClaimOwnership allows a backup owner to claim ownership of the group.\n// rlm must be the caller's own captured cur; the claimant is\n// rlm.Previous().Address(), which must already be a backup owner. The\n// claimant is automatically added as a member of the group.\nfunc (g *ManagedGroup) ClaimOwnership(_ int, rlm realm) error {\n\tif !rlm.IsCurrent() {\n\t\treturn ownable.ErrUnauthorized\n\t}\n\tcaller := rlm.Previous().Address()\n\t// already owner, skip\n\tif caller == g.Owner() {\n\t\treturn nil\n\t}\n\tif !g.IsBackupOwner(caller) {\n\t\treturn ErrNotMember\n\t}\n\tg.owner = ownable.NewWithAddress(caller)\n\treturn g.addMember(caller)\n}\n\n// AddMember adds a member to the group by address. rlm must be the\n// caller's own captured cur; rlm.Previous().Address() must equal the\n// owner.\nfunc (g *ManagedGroup) AddMember(_ int, rlm realm, addr address) error {\n\tif !rlm.IsCurrent() {\n\t\treturn ownable.ErrUnauthorized\n\t}\n\tif !g.owner.OwnedBy(rlm.Previous().Address()) {\n\t\treturn ownable.ErrUnauthorized\n\t}\n\treturn g.addMember(addr)\n}\n\nfunc (g *ManagedGroup) addMember(addr address) error {\n\tif !addr.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\tg.members.Set(addr.String(), struct{}{})\n\treturn nil\n}\n\n// RemoveMember removes a member from the group by address. The owner\n// cannot be removed. rlm must be the caller's own captured cur;\n// rlm.Previous().Address() must equal the owner.\nfunc (g *ManagedGroup) RemoveMember(_ int, rlm realm, addr address) error {\n\tif !rlm.IsCurrent() {\n\t\treturn ownable.ErrUnauthorized\n\t}\n\tif !g.owner.OwnedBy(rlm.Previous().Address()) {\n\t\treturn ownable.ErrUnauthorized\n\t}\n\tif !addr.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\tif addr == g.Owner() {\n\t\treturn ErrCannotRemoveOwner\n\t}\n\tg.members.Remove(addr.String())\n\treturn nil\n}\n\n// MemberCount returns the number of members in the group.\nfunc (g *ManagedGroup) MemberCount() int {\n\treturn g.members.Size()\n}\n\n// BackupOwnerCount returns the number of backup owners in the group.\nfunc (g *ManagedGroup) BackupOwnerCount() int {\n\treturn g.backupOwners.Size()\n}\n\n// IsMember checks if an address is a member of the group.\nfunc (g *ManagedGroup) IsMember(addr address) bool {\n\treturn g.members.Has(addr.String())\n}\n\n// IsBackupOwner checks if an address is a backup owner in the group.\nfunc (g *ManagedGroup) IsBackupOwner(addr address) bool {\n\treturn g.backupOwners.Has(addr.String())\n}\n\n// Owner returns the owner of the group.\nfunc (g *ManagedGroup) Owner() address {\n\treturn g.owner.Owner()\n}\n\n// BackupOwners returns a slice of all backup owners in the group, using the underlying\n// avl.Tree to iterate over the backup owners. If you have a large group, you may\n// want to use BackupOwnersWithOffset to iterate over backup owners in chunks.\nfunc (g *ManagedGroup) BackupOwners() []string {\n\treturn g.BackupOwnersWithOffset(0, g.BackupOwnerCount())\n}\n\n// Members returns a slice of all members in the group, using the underlying\n// avl.Tree to iterate over the members. If you have a large group, you may\n// want to use MembersWithOffset to iterate over members in chunks.\nfunc (g *ManagedGroup) Members() []string {\n\treturn g.MembersWithOffset(0, g.MemberCount())\n}\n\n// BackupOwnersWithOffset returns a slice of backup owners in the group, using the underlying\n// avl.Tree to iterate over the backup owners. The offset and count parameters allow you\n// to iterate over backup owners in chunks to support patterns such as pagination.\nfunc (g *ManagedGroup) BackupOwnersWithOffset(offset, count int) []string {\n\treturn sliceWithOffset(g.backupOwners, offset, count)\n}\n\n// MembersWithOffset returns a slice of members in the group, using the underlying\n// avl.Tree to iterate over the members. The offset and count parameters allow you\n// to iterate over members in chunks to support patterns such as pagination.\nfunc (g *ManagedGroup) MembersWithOffset(offset, count int) []string {\n\treturn sliceWithOffset(g.members, offset, count)\n}\n\n// sliceWithOffset is a helper function to iterate over an avl.Tree with an offset and count.\nfunc sliceWithOffset(t *avl.Tree, offset, count int) []string {\n\tvar result []string\n\tt.IterateByOffset(offset, count, func(k string, _ any) bool {\n\t\tif k == \"\" {\n\t\t\treturn true\n\t\t}\n\t\tresult = append(result, k)\n\t\treturn false\n\t})\n\treturn result\n}\n"},{"name":"mgroup_test.gno","body":"package mgroup\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/ownable/v0\"\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nfunc TestManagedGroup(cur realm, t *testing.T) {\n\tu1 := testutils.TestAddress(\"u1\")\n\tu2 := testutils.TestAddress(\"u2\")\n\tu3 := testutils.TestAddress(\"u3\")\n\tu4 := testutils.TestAddress(\"u4\")\n\n\tt.Run(\"AddBackupOwner\", func(cur realm, t *testing.T) {\n\t\tg := New(u1)\n\n\t\ttesting.SetRealm(testing.NewUserRealm(u1))\n\t\tfunc(cur realm) {\n\t\t\tif err := g.AddBackupOwner(0, cur, u2); err != nil {\n\t\t\t\tt.Errorf(\"expected nil, got %v\", err.Error())\n\t\t\t}\n\t\t\t// ensure invalid address is caught\n\t\t\tvar badAddr address\n\t\t\tif err := g.AddBackupOwner(0, cur, badAddr); err != ErrInvalidAddress {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", ErrInvalidAddress.Error(), err.Error())\n\t\t\t}\n\t\t}(cross(cur))\n\n\t\ttesting.SetRealm(testing.NewUserRealm(u3))\n\t\tfunc(cur realm) {\n\t\t\t// an address that is not the owner should not be allowed to add a backup owner\n\t\t\tif err := g.AddBackupOwner(0, cur, u4); err != ownable.ErrUnauthorized {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", ownable.ErrUnauthorized, err.Error())\n\t\t\t}\n\t\t}(cross(cur))\n\n\t})\n\tt.Run(\"RemoveBackupOwner\", func(cur realm, t *testing.T) {\n\t\tg := New(u1)\n\n\t\ttesting.SetRealm(testing.NewUserRealm(u1))\n\t\tfunc(cur realm) {\n\t\t\tg.AddBackupOwner(0, cur, u2)\n\t\t\tif err := g.RemoveBackupOwner(0, cur, u2); err != nil {\n\t\t\t\tt.Errorf(\"expected nil, got %v\", err.Error())\n\t\t\t}\n\t\t\t// running this twice should not error.\n\t\t\tif err := g.RemoveBackupOwner(0, cur, u2); err != nil {\n\t\t\t\tt.Errorf(\"expected nil, got %v\", err.Error())\n\t\t\t}\n\t\t\tvar badAddr address\n\t\t\tif err := g.RemoveBackupOwner(0, cur, badAddr); err != ErrInvalidAddress {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", ErrInvalidAddress.Error(), err.Error())\n\t\t\t}\n\t\t\tif err := g.RemoveBackupOwner(0, cur, u1); err != ErrCannotRemoveOwner {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", ErrCannotRemoveOwner.Error(), err.Error())\n\t\t\t}\n\t\t}(cross(cur))\n\n\t\ttesting.SetRealm(testing.NewUserRealm(u3))\n\t\tfunc(cur realm) {\n\t\t\tg.AddBackupOwner(0, cur, u2)\n\t\t\tif err := g.RemoveBackupOwner(0, cur, u2); err != ownable.ErrUnauthorized {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", ownable.ErrUnauthorized, err.Error())\n\t\t\t}\n\t\t}(cross(cur))\n\t})\n\n\tt.Run(\"ClaimOwnership\", func(cur realm, t *testing.T) {\n\t\tg := New(u1)\n\n\t\ttesting.SetRealm(testing.NewUserRealm(u1))\n\t\tfunc(cur realm) {\n\t\t\tg.AddBackupOwner(0, cur, u2)\n\t\t}(cross(cur))\n\t\ttesting.SetRealm(testing.NewUserRealm(u2))\n\t\tfunc(cur realm) {\n\t\t\tif err := g.ClaimOwnership(0, cur); err != nil {\n\t\t\t\tt.Errorf(\"expected nil, got %v\", err.Error())\n\t\t\t}\n\t\t\tif g.Owner() != u2 {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", u2, g.Owner())\n\t\t\t}\n\t\t\tif !g.IsMember(u2) {\n\t\t\t\tt.Errorf(\"expected %v to be a member\", u2)\n\t\t\t}\n\t\t\tif err := g.ClaimOwnership(0, cur); err != nil {\n\t\t\t\tt.Errorf(\"expected nil, got %v\", err.Error())\n\t\t\t}\n\t\t}(cross(cur))\n\t\ttesting.SetRealm(testing.NewUserRealm(u3))\n\t\tfunc(cur realm) {\n\t\t\tif err := g.ClaimOwnership(0, cur); err != ErrNotMember {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", ErrNotMember.Error(), err.Error())\n\t\t\t}\n\t\t}(cross(cur))\n\t})\n\n\tt.Run(\"AddMember\", func(cur realm, t *testing.T) {\n\t\tg := New(u1)\n\t\ttesting.SetRealm(testing.NewUserRealm(u1))\n\t\tfunc(cur realm) {\n\t\t\tif err := g.AddMember(0, cur, u2); err != nil {\n\t\t\t\tt.Errorf(\"expected nil, got %v\", err.Error())\n\t\t\t}\n\t\t\tif !g.IsMember(u2) {\n\t\t\t\tt.Errorf(\"expected %v to be a member\", u2)\n\t\t\t}\n\t\t\tvar badAddr address\n\t\t\tif err := g.AddMember(0, cur, badAddr); err != ErrInvalidAddress {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", ErrInvalidAddress, err.Error())\n\t\t\t}\n\t\t}(cross(cur))\n\t\ttesting.SetRealm(testing.NewUserRealm(u3))\n\t\tfunc(cur realm) {\n\t\t\tif err := g.AddMember(0, cur, u4); err != ownable.ErrUnauthorized {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", ownable.ErrUnauthorized, err.Error())\n\t\t\t}\n\t\t}(cross(cur))\n\t})\n\n\tt.Run(\"RemoveMember\", func(cur realm, t *testing.T) {\n\t\tg := New(u1)\n\n\t\ttesting.SetRealm(testing.NewUserRealm(u1))\n\t\tfunc(cur realm) {\n\t\t\tg.AddMember(0, cur, u2)\n\t\t\tif err := g.RemoveMember(0, cur, u2); err != nil {\n\t\t\t\tt.Errorf(\"expected nil, got %v\", err.Error())\n\t\t\t}\n\t\t\tif g.IsMember(u2) {\n\t\t\t\tt.Errorf(\"expected %v to not be a member\", u2)\n\t\t\t}\n\t\t\t// running this twice should not error\n\t\t\tif err := g.RemoveMember(0, cur, u2); err != nil {\n\t\t\t\tt.Errorf(\"expected nil, got %v\", err.Error())\n\t\t\t}\n\t\t\tvar badAddr address\n\t\t\tif err := g.RemoveMember(0, cur, badAddr); err != ErrInvalidAddress {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", ErrInvalidAddress.Error(), err.Error())\n\t\t\t}\n\t\t\tif err := g.RemoveMember(0, cur, u1); err != ErrCannotRemoveOwner {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", ErrCannotRemoveOwner.Error(), err.Error())\n\t\t\t}\n\t\t}(cross(cur))\n\n\t\ttesting.SetRealm(testing.NewUserRealm(u2))\n\t\tfunc(cur realm) {\n\t\t\tif err := g.RemoveMember(0, cur, u3); err != ownable.ErrUnauthorized {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", ownable.ErrUnauthorized.Error(), err.Error())\n\t\t\t}\n\t\t}(cross(cur))\n\t})\n\n\tt.Run(\"MemberCount\", func(cur realm, t *testing.T) {\n\t\tg := New(u1)\n\t\ttesting.SetRealm(testing.NewUserRealm(u1))\n\t\tfunc(cur realm) {\n\t\t\tuassert.Equal(t, 1, g.MemberCount())\n\t\t\tg.AddMember(0, cur, u2)\n\t\t\tuassert.Equal(t, 2, g.MemberCount())\n\t\t\tg.AddMember(0, cur, u3)\n\t\t\tuassert.Equal(t, 3, g.MemberCount())\n\t\t\tg.RemoveMember(0, cur, u2)\n\t\t\tuassert.Equal(t, 2, g.MemberCount())\n\t\t}(cross(cur))\n\t})\n\n\tt.Run(\"BackupOwnerCount\", func(cur realm, t *testing.T) {\n\t\tg := New(u1)\n\t\ttesting.SetRealm(testing.NewUserRealm(u1))\n\t\tfunc(cur realm) {\n\t\t\tuassert.Equal(t, 1, g.BackupOwnerCount())\n\t\t\tg.AddBackupOwner(0, cur, u2)\n\t\t\tuassert.Equal(t, 2, g.BackupOwnerCount())\n\t\t\tg.AddBackupOwner(0, cur, u3)\n\t\t\tuassert.Equal(t, 3, g.BackupOwnerCount())\n\t\t\tg.RemoveBackupOwner(0, cur, u2)\n\t\t\tuassert.Equal(t, 2, g.BackupOwnerCount())\n\t\t}(cross(cur))\n\t})\n\n\tt.Run(\"IsMember\", func(cur realm, t *testing.T) {\n\t\tg := New(u1)\n\t\ttesting.SetRealm(testing.NewUserRealm(u1))\n\t\tfunc(cur realm) {\n\t\t\tif !g.IsMember(u1) {\n\t\t\t\tt.Errorf(\"expected %v to be a member\", u1)\n\t\t\t}\n\t\t\tif g.IsMember(u2) {\n\t\t\t\tt.Errorf(\"expected %v to not be a member\", u2)\n\t\t\t}\n\t\t\tg.AddMember(0, cur, u2)\n\t\t\tif !g.IsMember(u2) {\n\t\t\t\tt.Errorf(\"expected %v to be a member\", u2)\n\t\t\t}\n\t\t}(cross(cur))\n\t})\n\tt.Run(\"IsBackupOwner\", func(cur realm, t *testing.T) {\n\t\tg := New(u1)\n\t\ttesting.SetRealm(testing.NewUserRealm(u1))\n\t\tfunc(cur realm) {\n\t\t\tif !g.IsBackupOwner(u1) {\n\t\t\t\tt.Errorf(\"expected %v to be a backup owner\", u1)\n\t\t\t}\n\t\t\tif g.IsBackupOwner(u2) {\n\t\t\t\tt.Errorf(\"expected %v to not be a backup owner\", u2)\n\t\t\t}\n\t\t\tg.AddBackupOwner(0, cur, u2)\n\t\t\tif !g.IsBackupOwner(u2) {\n\t\t\t\tt.Errorf(\"expected %v to be a backup owner\", u2)\n\t\t\t}\n\t\t}(cross(cur))\n\t})\n\n\tt.Run(\"Owner\", func(cur realm, t *testing.T) {\n\t\tg := New(u1)\n\t\ttesting.SetRealm(testing.NewUserRealm(u1))\n\t\tfunc(cur realm) {\n\t\t\tif g.Owner() != u1 {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", u1, g.Owner())\n\t\t\t}\n\t\t\tg.AddBackupOwner(0, cur, u2)\n\t\t\tif g.Owner() != u1 {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", u1, g.Owner())\n\t\t\t}\n\t\t}(cross(cur))\n\t\ttesting.SetRealm(testing.NewUserRealm(u2))\n\t\tfunc(cur realm) {\n\t\t\tg.ClaimOwnership(0, cur)\n\t\t\tif g.Owner() != u2 {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", u2, g.Owner())\n\t\t\t}\n\t\t}(cross(cur))\n\t})\n\tt.Run(\"BackupOwners\", func(cur realm, t *testing.T) {\n\t\tg := New(u1)\n\t\ttesting.SetRealm(testing.NewUserRealm(u1))\n\t\tfunc(cur realm) {\n\t\t\tg.AddBackupOwner(0, cur, u2)\n\t\t\tg.AddBackupOwner(0, cur, u3)\n\t\t\towners := g.BackupOwners()\n\t\t\tif len(owners) != 3 {\n\t\t\t\tt.Errorf(\"expected 3, got %v\", len(owners))\n\t\t\t}\n\t\t\t// BackupOwners iterates AVL by address-string key. TestAddress\n\t\t\t// does not preserve u1\u003cu2\u003cu3 order, so assert set membership.\n\t\t\tset := map[string]bool{owners[0]: true, owners[1]: true, owners[2]: true}\n\t\t\tfor _, addr := range []address{u1, u2, u3} {\n\t\t\t\tif !set[addr.String()] {\n\t\t\t\t\tt.Errorf(\"expected %v in backup owners\", addr)\n\t\t\t\t}\n\t\t\t}\n\t\t}(cross(cur))\n\t})\n\tt.Run(\"Members\", func(cur realm, t *testing.T) {\n\t\tg := New(u1)\n\t\ttesting.SetRealm(testing.NewUserRealm(u1))\n\t\tfunc(cur realm) {\n\t\t\tg.AddMember(0, cur, u2)\n\t\t\tg.AddMember(0, cur, u3)\n\t\t\tmembers := g.Members()\n\t\t\tif len(members) != 3 {\n\t\t\t\tt.Errorf(\"expected 3, got %v\", len(members))\n\t\t\t}\n\t\t\tset := map[string]bool{members[0]: true, members[1]: true, members[2]: true}\n\t\t\tfor _, addr := range []address{u1, u2, u3} {\n\t\t\t\tif !set[addr.String()] {\n\t\t\t\t\tt.Errorf(\"expected %v in members\", addr)\n\t\t\t\t}\n\t\t\t}\n\t\t}(cross(cur))\n\t})\n}\n\nfunc TestSliceWithOffset(cur realm, t *testing.T) {\n\ttestTable := []struct {\n\t\tname          string\n\t\tslice         []string\n\t\toffset        int\n\t\tcount         int\n\t\texpected      []string\n\t\texpectedCount int\n\t}{\n\t\t{\n\t\t\tname:          \"empty\",\n\t\t\tslice:         []string{},\n\t\t\toffset:        0,\n\t\t\tcount:         0,\n\t\t\texpected:      []string{},\n\t\t\texpectedCount: 0,\n\t\t},\n\t\t{\n\t\t\tname:          \"single\",\n\t\t\tslice:         []string{\"a\"},\n\t\t\toffset:        0,\n\t\t\tcount:         1,\n\t\t\texpected:      []string{\"a\"},\n\t\t\texpectedCount: 1,\n\t\t},\n\t\t{\n\t\t\tname:          \"single offset\",\n\t\t\tslice:         []string{\"a\"},\n\t\t\toffset:        1,\n\t\t\tcount:         1,\n\t\t\texpected:      []string{},\n\t\t\texpectedCount: 0,\n\t\t},\n\t\t{\n\t\t\tname:          \"multiple\",\n\t\t\tslice:         []string{\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\", \"i\", \"j\"},\n\t\t\toffset:        0,\n\t\t\tcount:         10,\n\t\t\texpected:      []string{\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\", \"i\", \"j\"},\n\t\t\texpectedCount: 10,\n\t\t},\n\t\t{\n\t\t\tname:          \"multiple offset\",\n\t\t\tslice:         []string{\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\", \"i\", \"j\"},\n\t\t\toffset:        5,\n\t\t\tcount:         5,\n\t\t\texpected:      []string{\"f\", \"g\", \"h\", \"i\", \"j\"},\n\t\t\texpectedCount: 5,\n\t\t},\n\t\t{\n\t\t\tname:          \"multiple offset end\",\n\t\t\tslice:         []string{\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\", \"i\", \"j\"},\n\t\t\toffset:        10,\n\t\t\tcount:         5,\n\t\t\texpected:      []string{},\n\t\t\texpectedCount: 0,\n\t\t},\n\t\t{\n\t\t\tname:          \"multiple offset past end\",\n\t\t\tslice:         []string{\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\", \"i\", \"j\"},\n\t\t\toffset:        11,\n\t\t\tcount:         5,\n\t\t\texpected:      []string{},\n\t\t\texpectedCount: 0,\n\t\t},\n\t\t{\n\t\t\tname:          \"multiple offset count past end\",\n\t\t\tslice:         []string{\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\", \"i\", \"j\"},\n\t\t\toffset:        5,\n\t\t\tcount:         20,\n\t\t\texpected:      []string{\"f\", \"g\", \"h\", \"i\", \"j\"},\n\t\t\texpectedCount: 5,\n\t\t},\n\t}\n\tfor _, test := range testTable {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\ttree := avl.NewTree()\n\t\t\tfor _, s := range test.slice {\n\t\t\t\ttree.Set(s, struct{}{})\n\t\t\t}\n\t\t\tslice := sliceWithOffset(tree, test.offset, test.count)\n\t\t\tif len(slice) != test.expectedCount {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", test.expectedCount, len(slice))\n\t\t\t}\n\t\t})\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"EZgEc6AmidYSh48aeTvvnp7QLLrhTp4V/xcjQa58bqksDFU2zwSrOm0Kk4okT3oE4n4nFc6w2FuU0XKsQqsNCA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"rolist","path":"gno.land/p/nt/avl/rolist/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/avl/rolist/v0\"\ngno = \"0.9\"\n"},{"name":"rolist.gno","body":"// Package rolist provides a read-only wrapper for list.List with safe value transformation.\n//\n// It is useful when you want to expose a read-only view of a list 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    Name     string\n//\t    Balance  int\n//\t    Internal string // sensitive field\n//\t}\n//\n//\t// Create and populate the original list\n//\tvar privateList list.List\n//\tprivateList.Append(\u0026User{\n//\t    Name:     \"Alice\",\n//\t    Balance:  100,\n//\t    Internal: \"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    u := v.(*User)\n//\t    return \u0026User{\n//\t        Name:     u.Name,\n//\t        Balance:  u.Balance,\n//\t        Internal: \"\", // omit sensitive data\n//\t    }\n//\t}\n//\n//\t// Create a read-only view of the list\n//\tpublicList := rolist.Wrap(privateList, makeEntrySafeFn)\n//\n//\t// Safely access the data\n//\tvalue := publicList.Get(0)\n//\tuser := value.(*User)\n//\t// user.Name == \"Alice\"\n//\t// user.Balance == 100\n//\t// user.Internal == \"\" (sensitive data is filtered)\npackage rolist\n\nimport (\n\t\"gno.land/p/nt/avl/list/v0\"\n\t\"gno.land/p/nt/avl/rotree/v0\"\n)\n\n// IReadOnlyList defines the read-only operations available on a list.\ntype IReadOnlyList interface {\n\tLen() int\n\tGet(index int) any\n\tSlice(startIndex, endIndex int) []any\n\tForEach(fn func(index int, value any) bool)\n}\n\n// ReadOnlyList wraps a list.List and provides read-only access.\ntype ReadOnlyList struct {\n\tlist            *list.List\n\tmakeEntrySafeFn func(any) any\n}\n\n// Verify interface implementations\nvar (\n\t_ IReadOnlyList = (*ReadOnlyList)(nil)\n\t_ IReadOnlyList = (interface{ list.IList })(nil) // is subset of list.IList\n)\n\n// Wrap creates a new ReadOnlyList from an existing list.List and a safety transformation function.\n// If makeEntrySafeFn is nil, values will be returned as-is without transformation.\nfunc Wrap(list *list.List, makeEntrySafeFn func(any) any) *ReadOnlyList {\n\treturn \u0026ReadOnlyList{\n\t\tlist:            list,\n\t\tmakeEntrySafeFn: makeEntrySafeFn,\n\t}\n}\n\n// getSafeValue applies the makeEntrySafeFn if it exists, otherwise returns the original value\nfunc (rol *ReadOnlyList) getSafeValue(value any) any {\n\tif rol.makeEntrySafeFn == nil {\n\t\treturn value\n\t}\n\treturn rol.makeEntrySafeFn(value)\n}\n\n// Len returns the number of elements in the list.\nfunc (rol *ReadOnlyList) Len() int {\n\treturn rol.list.Len()\n}\n\n// Get returns the value at the specified index, converted to a safe format.\n// Returns nil if index is out of bounds.\nfunc (rol *ReadOnlyList) Get(index int) any {\n\tvalue := rol.list.Get(index)\n\tif value == nil {\n\t\treturn nil\n\t}\n\treturn rol.getSafeValue(value)\n}\n\n// Slice returns a slice of values from startIndex (inclusive) to endIndex (exclusive),\n// with all values converted to a safe format.\nfunc (rol *ReadOnlyList) Slice(startIndex, endIndex int) []any {\n\tvalues := rol.list.Slice(startIndex, endIndex)\n\tif values == nil {\n\t\treturn nil\n\t}\n\n\tresult := make([]any, len(values))\n\tfor i, v := range values {\n\t\tresult[i] = rol.getSafeValue(v)\n\t}\n\treturn result\n}\n\n// ForEach iterates through all elements in the list, providing safe versions of the values.\nfunc (rol *ReadOnlyList) ForEach(fn func(index int, value any) bool) {\n\trol.list.ForEach(func(index int, value any) bool {\n\t\treturn fn(index, rol.getSafeValue(value))\n\t})\n}\n\n// Tree returns a read-only pointer to the underlying AVL tree.\nfunc (rol *ReadOnlyList) Tree() *rotree.ReadOnlyTree {\n\treturn rol.list.Tree()\n}\n"},{"name":"rolist_test.gno","body":"package rolist\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/avl/list/v0\"\n)\n\nfunc TestExample(t *testing.T) {\n\t// User represents our internal data structure\n\ttype User struct {\n\t\tID       string\n\t\tName     string\n\t\tBalance  int\n\t\tInternal string // sensitive internal data\n\t}\n\n\t// Create and populate the original list\n\tl := \u0026list.List{}\n\tl.Append(\n\t\t\u0026User{\n\t\t\tID:       \"1\",\n\t\t\tName:     \"Alice\",\n\t\t\tBalance:  100,\n\t\t\tInternal: \"sensitive_data_1\",\n\t\t},\n\t\t\u0026User{\n\t\t\tID:       \"2\",\n\t\t\tName:     \"Bob\",\n\t\t\tBalance:  200,\n\t\t\tInternal: \"sensitive_data_2\",\n\t\t},\n\t)\n\n\t// Define a makeEntrySafeFn that:\n\t// 1. Creates a defensive copy of the User struct\n\t// 2. Omits sensitive internal data\n\tmakeEntrySafeFn := func(v any) any {\n\t\toriginalUser := v.(*User)\n\t\treturn \u0026User{\n\t\t\tID:       originalUser.ID,\n\t\t\tName:     originalUser.Name,\n\t\t\tBalance:  originalUser.Balance,\n\t\t\tInternal: \"\", // Omit sensitive data\n\t\t}\n\t}\n\n\t// Create a read-only view of the list\n\troList := Wrap(l, makeEntrySafeFn)\n\n\t// Test retrieving and verifying a user\n\tt.Run(\"Get User\", func(t *testing.T) {\n\t\t// Get user from read-only list\n\t\tvalue := roList.Get(0)\n\t\tif value == nil {\n\t\t\tt.Fatal(\"User at index 0 not found\")\n\t\t}\n\n\t\tuser := value.(*User)\n\n\t\t// Verify user data is correct\n\t\tif user.Name != \"Alice\" || user.Balance != 100 {\n\t\t\tt.Errorf(\"Unexpected user data: got name=%s balance=%d\", user.Name, user.Balance)\n\t\t}\n\n\t\t// Verify sensitive data is not exposed\n\t\tif user.Internal != \"\" {\n\t\t\tt.Error(\"Sensitive data should not be exposed\")\n\t\t}\n\n\t\t// Verify it's a different instance than the original\n\t\toriginalUser := l.Get(0).(*User)\n\t\tif user == originalUser {\n\t\t\tt.Error(\"Read-only list should return a copy, not the original pointer\")\n\t\t}\n\t})\n\n\t// Test slice functionality\n\tt.Run(\"Slice Users\", func(t *testing.T) {\n\t\tusers := roList.Slice(0, 2)\n\t\tif len(users) != 2 {\n\t\t\tt.Fatalf(\"Expected 2 users, got %d\", len(users))\n\t\t}\n\n\t\tfor _, v := range users {\n\t\t\tuser := v.(*User)\n\t\t\tif user.Internal != \"\" {\n\t\t\t\tt.Error(\"Sensitive data exposed in slice\")\n\t\t\t}\n\t\t}\n\t})\n\n\t// Test ForEach functionality\n\tt.Run(\"ForEach Users\", func(t *testing.T) {\n\t\tcount := 0\n\t\troList.ForEach(func(index int, value any) bool {\n\t\t\tuser := value.(*User)\n\t\t\tif user.Internal != \"\" {\n\t\t\t\tt.Error(\"Sensitive data exposed during iteration\")\n\t\t\t}\n\t\t\tcount++\n\t\t\treturn false\n\t\t})\n\n\t\tif count != 2 {\n\t\t\tt.Errorf(\"Expected 2 users, got %d\", count)\n\t\t}\n\t})\n}\n\nfunc TestNilMakeEntrySafeFn(t *testing.T) {\n\t// Create a list with some test data\n\tl := \u0026list.List{}\n\toriginalValue := []int{1, 2, 3}\n\tl.Append(originalValue)\n\n\t// Create a ReadOnlyList with nil makeEntrySafeFn\n\troList := Wrap(l, nil)\n\n\t// Test that we get back the original value\n\tvalue := roList.Get(0)\n\tif value == nil {\n\t\tt.Fatal(\"Value not found\")\n\t}\n\n\t// Verify it's the exact same slice (not a copy)\n\tretrievedSlice := value.([]int)\n\tif \u0026retrievedSlice[0] != \u0026originalValue[0] {\n\t\tt.Error(\"Expected to get back the original slice reference\")\n\t}\n}\n\nfunc TestReadOnlyList(t *testing.T) {\n\t// Example of a makeEntrySafeFn that appends \"_readonly\" to demonstrate transformation\n\tmakeEntrySafeFn := func(value any) any {\n\t\treturn value.(string) + \"_readonly\"\n\t}\n\n\tl := \u0026list.List{}\n\tl.Append(\"value1\", \"value2\", \"value3\")\n\n\troList := Wrap(l, makeEntrySafeFn)\n\n\ttests := []struct {\n\t\tname     string\n\t\tindex    int\n\t\texpected any\n\t}{\n\t\t{\"ExistingIndex0\", 0, \"value1_readonly\"},\n\t\t{\"ExistingIndex1\", 1, \"value2_readonly\"},\n\t\t{\"NonExistingIndex\", 3, nil},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvalue := roList.Get(tt.index)\n\t\t\tif value != tt.expected {\n\t\t\t\tt.Errorf(\"For index %d, expected %v, got %v\", tt.index, tt.expected, value)\n\t\t\t}\n\t\t})\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"Mt5g7kVY5JN4GJ7QT5xY5DPofiDnjHAQZhjjZBQFcKUnSif2S8qbe88hMmm2t6J5qNPqanqpc7WXWV7aDIn9Sg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"rolist","path":"gno.land/p/nt/bptree/rolist/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/bptree/rolist/v0\"\ngno = \"0.9\"\n"},{"name":"rolist.gno","body":"// Package rolist provides a read-only wrapper for list.List with safe value transformation.\npackage rolist\n\nimport (\n\t\"gno.land/p/nt/bptree/list/v0\"\n\t\"gno.land/p/nt/bptree/rotree/v0\"\n)\n\n// IReadOnlyList defines the read-only operations available on a list.\ntype IReadOnlyList interface {\n\tLen() int\n\tGet(index int) (any, bool)\n\tSlice(startIndex, endIndex int) []any\n\tForEach(fn func(index int, value any) bool)\n}\n\n// ReadOnlyList wraps a list.List and provides read-only access.\ntype ReadOnlyList struct {\n\tlist            *list.List\n\tmakeEntrySafeFn func(any) any\n}\n\n// Verify interface implementations\nvar (\n\t_ IReadOnlyList = (*ReadOnlyList)(nil)\n\t_ IReadOnlyList = (interface{ list.IList })(nil) // is subset of list.IList\n)\n\n// Wrap creates a new ReadOnlyList from an existing list.List and a safety transformation function.\n// If makeEntrySafeFn is nil, values will be returned as-is without transformation.\nfunc Wrap(list *list.List, makeEntrySafeFn func(any) any) *ReadOnlyList {\n\treturn \u0026ReadOnlyList{\n\t\tlist:            list,\n\t\tmakeEntrySafeFn: makeEntrySafeFn,\n\t}\n}\n\n// getSafeValue applies the makeEntrySafeFn if it exists, otherwise returns the original value\nfunc (rol *ReadOnlyList) getSafeValue(value any) any {\n\tif rol.makeEntrySafeFn == nil {\n\t\treturn value\n\t}\n\treturn rol.makeEntrySafeFn(value)\n}\n\n// Len returns the number of elements in the list.\nfunc (rol *ReadOnlyList) Len() int {\n\treturn rol.list.Len()\n}\n\n// Get returns the value at the specified index, converted to a safe format.\n// Returns (nil, false) if index is out of bounds.\nfunc (rol *ReadOnlyList) Get(index int) (any, bool) {\n\tvalue, ok := rol.list.Get(index)\n\tif !ok {\n\t\treturn nil, false\n\t}\n\treturn rol.getSafeValue(value), true\n}\n\n// Slice returns a slice of values from startIndex (inclusive) to endIndex (exclusive),\n// with all values converted to a safe format.\nfunc (rol *ReadOnlyList) Slice(startIndex, endIndex int) []any {\n\tvalues := rol.list.Slice(startIndex, endIndex)\n\tif values == nil {\n\t\treturn nil\n\t}\n\n\tresult := make([]any, len(values))\n\tfor i, v := range values {\n\t\tresult[i] = rol.getSafeValue(v)\n\t}\n\treturn result\n}\n\n// ForEach iterates through all elements in the list, providing safe versions of the values.\nfunc (rol *ReadOnlyList) ForEach(fn func(index int, value any) bool) {\n\trol.list.ForEach(func(index int, value any) bool {\n\t\treturn fn(index, rol.getSafeValue(value))\n\t})\n}\n\n// Tree returns a read-only pointer to the underlying B+ tree.\nfunc (rol *ReadOnlyList) Tree() *rotree.ReadOnlyTree {\n\treturn rol.list.Tree()\n}\n"},{"name":"rolist_test.gno","body":"package rolist\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/bptree/list/v0\"\n)\n\nfunc TestExample(t *testing.T) {\n\t// User represents our internal data structure\n\ttype User struct {\n\t\tID       string\n\t\tName     string\n\t\tBalance  int\n\t\tInternal string // sensitive internal data\n\t}\n\n\t// Create and populate the original list\n\tl := \u0026list.List{}\n\tl.Append(\n\t\t\u0026User{\n\t\t\tID:       \"1\",\n\t\t\tName:     \"Alice\",\n\t\t\tBalance:  100,\n\t\t\tInternal: \"sensitive_data_1\",\n\t\t},\n\t\t\u0026User{\n\t\t\tID:       \"2\",\n\t\t\tName:     \"Bob\",\n\t\t\tBalance:  200,\n\t\t\tInternal: \"sensitive_data_2\",\n\t\t},\n\t)\n\n\t// Define a makeEntrySafeFn that:\n\t// 1. Creates a defensive copy of the User struct\n\t// 2. Omits sensitive internal data\n\tmakeEntrySafeFn := func(v any) any {\n\t\toriginalUser := v.(*User)\n\t\treturn \u0026User{\n\t\t\tID:       originalUser.ID,\n\t\t\tName:     originalUser.Name,\n\t\t\tBalance:  originalUser.Balance,\n\t\t\tInternal: \"\", // Omit sensitive data\n\t\t}\n\t}\n\n\t// Create a read-only view of the list\n\troList := Wrap(l, makeEntrySafeFn)\n\n\t// Test retrieving and verifying a user\n\tt.Run(\"Get User\", func(t *testing.T) {\n\t\t// Get user from read-only list\n\t\tvalue, ok := roList.Get(0)\n\t\tif !ok {\n\t\t\tt.Fatal(\"User at index 0 not found\")\n\t\t}\n\n\t\tuser := value.(*User)\n\n\t\t// Verify user data is correct\n\t\tif user.Name != \"Alice\" || user.Balance != 100 {\n\t\t\tt.Errorf(\"Unexpected user data: got name=%s balance=%d\", user.Name, user.Balance)\n\t\t}\n\n\t\t// Verify sensitive data is not exposed\n\t\tif user.Internal != \"\" {\n\t\t\tt.Error(\"Sensitive data should not be exposed\")\n\t\t}\n\n\t\t// Verify it's a different instance than the original\n\t\torigVal, _ := l.Get(0)\n\t\toriginalUser := origVal.(*User)\n\t\tif user == originalUser {\n\t\t\tt.Error(\"Read-only list should return a copy, not the original pointer\")\n\t\t}\n\t})\n\n\t// Test slice functionality\n\tt.Run(\"Slice Users\", func(t *testing.T) {\n\t\tusers := roList.Slice(0, 2)\n\t\tif len(users) != 2 {\n\t\t\tt.Fatalf(\"Expected 2 users, got %d\", len(users))\n\t\t}\n\n\t\tfor _, v := range users {\n\t\t\tuser := v.(*User)\n\t\t\tif user.Internal != \"\" {\n\t\t\t\tt.Error(\"Sensitive data exposed in slice\")\n\t\t\t}\n\t\t}\n\t})\n\n\t// Test ForEach functionality\n\tt.Run(\"ForEach Users\", func(t *testing.T) {\n\t\tcount := 0\n\t\troList.ForEach(func(index int, value any) bool {\n\t\t\tuser := value.(*User)\n\t\t\tif user.Internal != \"\" {\n\t\t\t\tt.Error(\"Sensitive data exposed during iteration\")\n\t\t\t}\n\t\t\tcount++\n\t\t\treturn false\n\t\t})\n\n\t\tif count != 2 {\n\t\t\tt.Errorf(\"Expected 2 users, got %d\", count)\n\t\t}\n\t})\n}\n\nfunc TestNilMakeEntrySafeFn(t *testing.T) {\n\t// Create a list with some test data\n\tl := \u0026list.List{}\n\toriginalValue := []int{1, 2, 3}\n\tl.Append(originalValue)\n\n\t// Create a ReadOnlyList with nil makeEntrySafeFn\n\troList := Wrap(l, nil)\n\n\t// Test that we get back the original value\n\tvalue, ok := roList.Get(0)\n\tif !ok {\n\t\tt.Fatal(\"Value not found\")\n\t}\n\n\t// Verify it's the exact same slice (not a copy)\n\tretrievedSlice := value.([]int)\n\tif \u0026retrievedSlice[0] != \u0026originalValue[0] {\n\t\tt.Error(\"Expected to get back the original slice reference\")\n\t}\n}\n\nfunc TestReadOnlyList(t *testing.T) {\n\t// Example of a makeEntrySafeFn that appends \"_readonly\" to demonstrate transformation\n\tmakeEntrySafeFn := func(value any) any {\n\t\treturn value.(string) + \"_readonly\"\n\t}\n\n\tl := \u0026list.List{}\n\tl.Append(\"value1\", \"value2\", \"value3\")\n\n\troList := Wrap(l, makeEntrySafeFn)\n\n\ttests := []struct {\n\t\tname     string\n\t\tindex    int\n\t\texpected any\n\t\tok       bool\n\t}{\n\t\t{\"ExistingIndex0\", 0, \"value1_readonly\", true},\n\t\t{\"ExistingIndex1\", 1, \"value2_readonly\", true},\n\t\t{\"NonExistingIndex\", 3, nil, false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvalue, ok := roList.Get(tt.index)\n\t\t\tif ok != tt.ok || value != tt.expected {\n\t\t\t\tt.Errorf(\"For index %d, expected (%v, %v), got (%v, %v)\", tt.index, tt.expected, tt.ok, value, ok)\n\t\t\t}\n\t\t})\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"IW1s41JiXTrhkpJx98yDGwbaUB1eKfdwypkhScJ9Y71kM8O06caRjwJwkD1t8UfHyZ4EHwGkpwTVCXn6XDiHEA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"grc1155","path":"gno.land/p/nt/grc1155/v0","files":[{"name":"README.md","body":"# GRC-1155 Spec: Multi Token Standard\n\nGRC1155 is a specification for managing multiple tokens based on Gnoland. The name and design is based on Ethereum's ERC1155 standard.\n\n## See also:\n\n[ERC-1155 Spec][erc-1155]\n\n[erc-1155]: https://eips.ethereum.org/EIPS/eip-1155\n"},{"name":"basic_grc1155_token.gno","body":"package grc1155\n\nimport (\n\t\"chain/runtime/unsafe\"\n\t\"math/overflow\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\ntype basicGRC1155Token struct {\n\turi               string\n\tbalances          avl.Tree // \"TokenId:Address\" -\u003e int64\n\toperatorApprovals avl.Tree // \"OwnerAddress:OperatorAddress\" -\u003e bool\n}\n\nvar _ IGRC1155 = (*basicGRC1155Token)(nil)\n\n// Returns new basic GRC1155 token\nfunc NewBasicGRC1155Token(uri string) *basicGRC1155Token {\n\treturn \u0026basicGRC1155Token{\n\t\turi:               uri,\n\t\tbalances:          avl.Tree{},\n\t\toperatorApprovals: avl.Tree{},\n\t}\n}\n\nfunc (s *basicGRC1155Token) Uri() string { return s.uri }\n\n// BalanceOf returns the input address's balance of the token type requested\nfunc (s *basicGRC1155Token) BalanceOf(addr address, tid TokenID) (int64, error) {\n\tif !isValidAddress(addr) {\n\t\treturn 0, ErrInvalidAddress\n\t}\n\n\tkey := string(tid) + \":\" + addr.String()\n\tbalance := s.balances.Get(key)\n\tif balance == nil {\n\t\treturn 0, nil\n\t}\n\n\treturn balance.(int64), nil\n}\n\n// BalanceOfBatch returns the balance of multiple account/token pairs\nfunc (s *basicGRC1155Token) BalanceOfBatch(owners []address, batch []TokenID) ([]int64, error) {\n\tif len(owners) != len(batch) {\n\t\treturn nil, ErrMismatchLength\n\t}\n\n\tbalanceOfBatch := make([]int64, len(owners))\n\n\tfor i := 0; i \u003c len(owners); i++ {\n\t\tbalanceOfBatch[i], _ = s.BalanceOf(owners[i], batch[i])\n\t}\n\n\treturn balanceOfBatch, nil\n}\n\n// SetApprovalForAll can approve the operator to operate on all tokens\nfunc (s *basicGRC1155Token) SetApprovalForAll(operator address, approved bool) error {\n\tif !isValidAddress(operator) {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tcaller := unsafe.OriginCaller()\n\treturn s.setApprovalForAll(caller, operator, approved)\n}\n\n// IsApprovedForAll returns true if operator is the owner or is approved for all by the owner.\n// Otherwise, returns false\nfunc (s *basicGRC1155Token) IsApprovedForAll(owner, operator address) bool {\n\tif operator == owner {\n\t\treturn true\n\t}\n\tkey := owner.String() + \":\" + operator.String()\n\tapproved := s.operatorApprovals.Get(key)\n\tif approved == nil {\n\t\treturn false\n\t}\n\n\tab, ok := approved.(bool)\n\treturn ok \u0026\u0026 ab\n}\n\n// Safely transfers `tokenId` token from `from` to `to`, checking that\n// contract recipients are aware of the GRC1155 protocol to prevent\n// tokens from being forever locked.\nfunc (s *basicGRC1155Token) SafeTransferFrom(from, to address, tid TokenID, amount int64) error {\n\tcaller := unsafe.OriginCaller()\n\tif !s.IsApprovedForAll(caller, from) {\n\t\treturn ErrCallerIsNotOwnerOrApproved\n\t}\n\n\terr := s.safeBatchTransferFrom(from, to, []TokenID{tid}, []int64{amount})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !s.doSafeTransferAcceptanceCheck(caller, from, to, tid, amount) {\n\t\treturn ErrTransferToRejectedOrNonGRC1155Receiver\n\t}\n\n\temit(\u0026TransferSingleEvent{caller, from, to, tid, amount})\n\n\treturn nil\n}\n\n// Safely transfers a `batch` of tokens from `from` to `to`, checking that\n// contract recipients are aware of the GRC1155 protocol to prevent\n// tokens from being forever locked.\nfunc (s *basicGRC1155Token) SafeBatchTransferFrom(from, to address, batch []TokenID, amounts []int64) error {\n\tcaller := unsafe.OriginCaller()\n\tif !s.IsApprovedForAll(caller, from) {\n\t\treturn ErrCallerIsNotOwnerOrApproved\n\t}\n\n\terr := s.safeBatchTransferFrom(from, to, batch, amounts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !s.doSafeBatchTransferAcceptanceCheck(caller, from, to, batch, amounts) {\n\t\treturn ErrTransferToRejectedOrNonGRC1155Receiver\n\t}\n\n\temit(\u0026TransferBatchEvent{caller, from, to, batch, amounts})\n\n\treturn nil\n}\n\n// Creates `amount` tokens of token type `id`, and assigns them to `to`. Also checks that\n// contract recipients are using GRC1155 protocol.\nfunc (s *basicGRC1155Token) SafeMint(to address, tid TokenID, amount int64) error {\n\tcaller := unsafe.OriginCaller()\n\n\terr := s.mintBatch(to, []TokenID{tid}, []int64{amount})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !s.doSafeTransferAcceptanceCheck(caller, zeroAddress, to, tid, amount) {\n\t\treturn ErrTransferToRejectedOrNonGRC1155Receiver\n\t}\n\n\temit(\u0026TransferSingleEvent{caller, zeroAddress, to, tid, amount})\n\n\treturn nil\n}\n\n// Batch version of `SafeMint()`. Also checks that\n// contract recipients are using GRC1155 protocol.\nfunc (s *basicGRC1155Token) SafeBatchMint(to address, batch []TokenID, amounts []int64) error {\n\tcaller := unsafe.OriginCaller()\n\n\terr := s.mintBatch(to, batch, amounts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !s.doSafeBatchTransferAcceptanceCheck(caller, zeroAddress, to, batch, amounts) {\n\t\treturn ErrTransferToRejectedOrNonGRC1155Receiver\n\t}\n\n\temit(\u0026TransferBatchEvent{caller, zeroAddress, to, batch, amounts})\n\n\treturn nil\n}\n\n// Destroys `amount` tokens of token type `id` from `from`.\nfunc (s *basicGRC1155Token) Burn(from address, tid TokenID, amount int64) error {\n\tcaller := unsafe.OriginCaller()\n\n\terr := s.burnBatch(from, []TokenID{tid}, []int64{amount})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\temit(\u0026TransferSingleEvent{caller, from, zeroAddress, tid, amount})\n\n\treturn nil\n}\n\n// Batch version of `Burn()`\nfunc (s *basicGRC1155Token) BatchBurn(from address, batch []TokenID, amounts []int64) error {\n\tcaller := unsafe.OriginCaller()\n\n\terr := s.burnBatch(from, batch, amounts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\temit(\u0026TransferBatchEvent{caller, from, zeroAddress, batch, amounts})\n\n\treturn nil\n}\n\n/* Helper methods */\n\n// Helper for SetApprovalForAll(): approve `operator` to operate on all of `owner` tokens\nfunc (s *basicGRC1155Token) setApprovalForAll(owner, operator address, approved bool) error {\n\tif owner == operator {\n\t\treturn nil\n\t}\n\n\tkey := owner.String() + \":\" + operator.String()\n\tif approved {\n\t\ts.operatorApprovals.Set(key, approved)\n\t} else {\n\t\ts.operatorApprovals.Remove(key)\n\t}\n\n\temit(\u0026ApprovalForAllEvent{owner, operator, approved})\n\n\treturn nil\n}\n\n// Helper for SafeTransferFrom() and SafeBatchTransferFrom()\nfunc (s *basicGRC1155Token) safeBatchTransferFrom(from, to address, batch []TokenID, amounts []int64) error {\n\tif len(batch) != len(amounts) {\n\t\treturn ErrMismatchLength\n\t}\n\tif !isValidAddress(from) || !isValidAddress(to) {\n\t\treturn ErrInvalidAddress\n\t}\n\tif from == to {\n\t\treturn ErrCannotTransferToSelf\n\t}\n\tfor _, amount := range amounts {\n\t\tif amount \u003c 0 {\n\t\t\treturn ErrInvalidAmount\n\t\t}\n\t}\n\n\tcaller := unsafe.OriginCaller()\n\ts.beforeTokenTransfer(caller, from, to, batch, amounts)\n\n\tfor i := 0; i \u003c len(batch); i++ {\n\t\ttid := batch[i]\n\t\tamount := amounts[i]\n\t\tfromBalance, err := s.BalanceOf(from, tid)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif fromBalance \u003c amount {\n\t\t\treturn ErrInsufficientBalance\n\t\t}\n\t\ttoBalance, err := s.BalanceOf(to, tid)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfromBalance = overflow.Sub64p(fromBalance, amount)\n\t\ttoBalance = overflow.Add64p(toBalance, amount)\n\t\tfromBalanceKey := string(tid) + \":\" + from.String()\n\t\ttoBalanceKey := string(tid) + \":\" + to.String()\n\t\ts.balances.Set(fromBalanceKey, fromBalance)\n\t\ts.balances.Set(toBalanceKey, toBalance)\n\t}\n\n\ts.afterTokenTransfer(caller, from, to, batch, amounts)\n\n\treturn nil\n}\n\n// Helper for SafeMint() and SafeBatchMint()\nfunc (s *basicGRC1155Token) mintBatch(to address, batch []TokenID, amounts []int64) error {\n\tif len(batch) != len(amounts) {\n\t\treturn ErrMismatchLength\n\t}\n\tif !isValidAddress(to) {\n\t\treturn ErrInvalidAddress\n\t}\n\tfor _, amount := range amounts {\n\t\tif amount \u003c 0 {\n\t\t\treturn ErrInvalidAmount\n\t\t}\n\t}\n\n\tcaller := unsafe.OriginCaller()\n\ts.beforeTokenTransfer(caller, zeroAddress, to, batch, amounts)\n\n\tfor i := 0; i \u003c len(batch); i++ {\n\t\ttid := batch[i]\n\t\tamount := amounts[i]\n\t\ttoBalance, err := s.BalanceOf(to, tid)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ttoBalance = overflow.Add64p(toBalance, amount)\n\t\ttoBalanceKey := string(tid) + \":\" + to.String()\n\t\ts.balances.Set(toBalanceKey, toBalance)\n\t}\n\n\ts.afterTokenTransfer(caller, zeroAddress, to, batch, amounts)\n\n\treturn nil\n}\n\n// Helper for Burn() and BurnBatch()\nfunc (s *basicGRC1155Token) burnBatch(from address, batch []TokenID, amounts []int64) error {\n\tif len(batch) != len(amounts) {\n\t\treturn ErrMismatchLength\n\t}\n\tif !isValidAddress(from) {\n\t\treturn ErrInvalidAddress\n\t}\n\tfor _, amount := range amounts {\n\t\tif amount \u003c 0 {\n\t\t\treturn ErrInvalidAmount\n\t\t}\n\t}\n\n\tcaller := unsafe.OriginCaller()\n\ts.beforeTokenTransfer(caller, from, zeroAddress, batch, amounts)\n\n\tfor i := 0; i \u003c len(batch); i++ {\n\t\ttid := batch[i]\n\t\tamount := amounts[i]\n\t\tfromBalance, err := s.BalanceOf(from, tid)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif fromBalance \u003c amount {\n\t\t\treturn ErrBurnAmountExceedsBalance\n\t\t}\n\t\tfromBalance = overflow.Sub64p(fromBalance, amount)\n\t\tfromBalanceKey := string(tid) + \":\" + from.String()\n\t\ts.balances.Set(fromBalanceKey, fromBalance)\n\t}\n\n\ts.afterTokenTransfer(caller, from, zeroAddress, batch, amounts)\n\n\treturn nil\n}\n\nfunc (s *basicGRC1155Token) setUri(newUri string) {\n\ts.uri = newUri\n\temit(\u0026UpdateURIEvent{newUri})\n}\n\nfunc (s *basicGRC1155Token) beforeTokenTransfer(operator, from, to address, batch []TokenID, amounts []int64) {\n\t// TODO: Implementation\n}\n\nfunc (s *basicGRC1155Token) afterTokenTransfer(operator, from, to address, batch []TokenID, amounts []int64) {\n\t// TODO: Implementation\n}\n\nfunc (s *basicGRC1155Token) doSafeTransferAcceptanceCheck(operator, from, to address, tid TokenID, amount int64) bool {\n\t// TODO: Implementation\n\treturn true\n}\n\nfunc (s *basicGRC1155Token) doSafeBatchTransferAcceptanceCheck(operator, from, to address, batch []TokenID, amounts []int64) bool {\n\t// TODO: Implementation\n\treturn true\n}\n\nfunc (s *basicGRC1155Token) RenderHome() (str string) {\n\tstr += ufmt.Sprintf(\"# URI:%s\\n\", s.uri)\n\n\treturn\n}\n\nfunc (mt *basicGRC1155Token) Getter() MultiTokenGetter {\n\treturn func() IGRC1155 {\n\t\treturn mt\n\t}\n}\n"},{"name":"basic_grc1155_token_test.gno","body":"package grc1155\n\nimport (\n\t\"chain/runtime/unsafe\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nconst dummyURI = \"ipfs://xyz\"\n\nfunc TestNewBasicGRC1155Token(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n}\n\nfunc TestUri(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\tuassert.Equal(t, dummyURI, dummy.Uri())\n}\n\nfunc TestBalanceOf(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\ttid1 := TokenID(\"1\")\n\ttid2 := TokenID(\"2\")\n\n\t_, err := dummy.BalanceOf(zeroAddress, tid1)\n\tuassert.Error(t, err, \"should result in error\")\n\n\tbalanceAddr1OfToken1, err := dummy.BalanceOf(addr1, tid1)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, int64(0), balanceAddr1OfToken1)\n\n\tdummy.mintBatch(addr1, []TokenID{tid1, tid2}, []int64{10, 100})\n\tdummy.mintBatch(addr2, []TokenID{tid1}, []int64{20})\n\n\tbalanceAddr1OfToken1, err = dummy.BalanceOf(addr1, tid1)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tbalanceAddr1OfToken2, err := dummy.BalanceOf(addr1, tid2)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tbalanceAddr2OfToken1, err := dummy.BalanceOf(addr2, tid1)\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tuassert.Equal(t, int64(10), balanceAddr1OfToken1)\n\tuassert.Equal(t, int64(100), balanceAddr1OfToken2)\n\tuassert.Equal(t, int64(20), balanceAddr2OfToken1)\n}\n\nfunc TestBalanceOfBatch(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\ttid1 := TokenID(\"1\")\n\ttid2 := TokenID(\"2\")\n\n\tbalanceBatch, err := dummy.BalanceOfBatch([]address{addr1, addr2}, []TokenID{tid1, tid2})\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, int64(0), balanceBatch[0])\n\tuassert.Equal(t, int64(0), balanceBatch[1])\n\n\tdummy.mintBatch(addr1, []TokenID{tid1}, []int64{10})\n\tdummy.mintBatch(addr2, []TokenID{tid2}, []int64{20})\n\n\tbalanceBatch, err = dummy.BalanceOfBatch([]address{addr1, addr2}, []TokenID{tid1, tid2})\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, int64(10), balanceBatch[0])\n\tuassert.Equal(t, int64(20), balanceBatch[1])\n}\n\nfunc TestIsApprovedForAll(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\tisApprovedForAll := dummy.IsApprovedForAll(addr1, addr2)\n\tuassert.False(t, isApprovedForAll)\n}\n\nfunc TestSetApprovalForAll(t *testing.T) {\n\t//alice := testutils.TestAddress(\"alice\")\n\t//testing.SetOriginCaller(alice)\n\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tcaller := unsafe.OriginCaller()\n\taddr := address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\tisApprovedForAll := dummy.IsApprovedForAll(caller, addr)\n\tuassert.False(t, isApprovedForAll)\n\n\terr := dummy.SetApprovalForAll(addr, true)\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tisApprovedForAll = dummy.IsApprovedForAll(caller, addr)\n\tuassert.True(t, isApprovedForAll)\n\n\terr = dummy.SetApprovalForAll(addr, false)\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tisApprovedForAll = dummy.IsApprovedForAll(caller, addr)\n\tuassert.False(t, isApprovedForAll)\n}\n\nfunc TestSafeTransferFrom(t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\ttesting.SetOriginCaller(alice)\n\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tcaller := unsafe.OriginCaller()\n\taddr := address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\ttid := TokenID(\"1\")\n\n\tdummy.mintBatch(caller, []TokenID{tid}, []int64{100})\n\n\terr := dummy.SafeTransferFrom(caller, zeroAddress, tid, 10)\n\tuassert.Error(t, err, \"should result in error\")\n\n\terr = dummy.SafeTransferFrom(caller, addr, tid, 160)\n\tuassert.Error(t, err, \"should result in error\")\n\n\terr = dummy.SafeTransferFrom(caller, addr, tid, 60)\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\t// Check balance of caller after transfer\n\tbalanceOfCaller, err := dummy.BalanceOf(caller, tid)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, int64(40), balanceOfCaller)\n\n\t// Check balance of addr after transfer\n\tbalanceOfAddr, err := dummy.BalanceOf(addr, tid)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, int64(60), balanceOfAddr)\n}\n\nfunc TestSafeBatchTransferFrom(t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\ttesting.SetOriginCaller(alice)\n\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tcaller := unsafe.OriginCaller()\n\taddr := address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\ttid1 := TokenID(\"1\")\n\ttid2 := TokenID(\"2\")\n\n\tdummy.mintBatch(caller, []TokenID{tid1, tid2}, []int64{10, 100})\n\n\terr := dummy.SafeBatchTransferFrom(caller, zeroAddress, []TokenID{tid1, tid2}, []int64{4, 60})\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.SafeBatchTransferFrom(caller, addr, []TokenID{tid1, tid2}, []int64{40, 60})\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.SafeBatchTransferFrom(caller, addr, []TokenID{tid1}, []int64{40, 60})\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.SafeBatchTransferFrom(caller, addr, []TokenID{tid1, tid2}, []int64{4, 60})\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tbalanceBatch, err := dummy.BalanceOfBatch([]address{caller, addr, caller, addr}, []TokenID{tid1, tid1, tid2, tid2})\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\t// Check token1's balance of caller after batch transfer\n\tuassert.Equal(t, int64(6), balanceBatch[0])\n\n\t// Check token1's balance of addr after batch transfer\n\tuassert.Equal(t, int64(4), balanceBatch[1])\n\n\t// Check token2's balance of caller after batch transfer\n\tuassert.Equal(t, int64(40), balanceBatch[2])\n\n\t// Check token2's balance of addr after batch transfer\n\tuassert.Equal(t, int64(60), balanceBatch[3])\n}\n\nfunc TestSafeMint(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\ttid1 := TokenID(\"1\")\n\ttid2 := TokenID(\"2\")\n\n\terr := dummy.SafeMint(zeroAddress, tid1, 100)\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.SafeMint(addr1, tid1, 100)\n\tuassert.NoError(t, err, \"should not result in error\")\n\terr = dummy.SafeMint(addr1, tid2, 200)\n\tuassert.NoError(t, err, \"should not result in error\")\n\terr = dummy.SafeMint(addr2, tid1, 50)\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tbalanceBatch, err := dummy.BalanceOfBatch([]address{addr1, addr2, addr1}, []TokenID{tid1, tid1, tid2})\n\tuassert.NoError(t, err, \"should not result in error\")\n\t// Check token1's balance of addr1 after mint\n\tuassert.Equal(t, int64(100), balanceBatch[0])\n\t// Check token1's balance of addr2 after mint\n\tuassert.Equal(t, int64(50), balanceBatch[1])\n\t// Check token2's balance of addr1 after mint\n\tuassert.Equal(t, int64(200), balanceBatch[2])\n}\n\nfunc TestSafeBatchMint(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\ttid1 := TokenID(\"1\")\n\ttid2 := TokenID(\"2\")\n\n\terr := dummy.SafeBatchMint(zeroAddress, []TokenID{tid1, tid2}, []int64{100, 200})\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.SafeBatchMint(addr1, []TokenID{tid1, tid2}, []int64{100, 200})\n\tuassert.NoError(t, err, \"should not result in error\")\n\terr = dummy.SafeBatchMint(addr2, []TokenID{tid1, tid2}, []int64{300, 400})\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tbalanceBatch, err := dummy.BalanceOfBatch([]address{addr1, addr2, addr1, addr2}, []TokenID{tid1, tid1, tid2, tid2})\n\tuassert.NoError(t, err, \"should not result in error\")\n\t// Check token1's balance of addr1 after batch mint\n\tuassert.Equal(t, int64(100), balanceBatch[0])\n\t// Check token1's balance of addr2 after batch mint\n\tuassert.Equal(t, int64(300), balanceBatch[1])\n\t// Check token2's balance of addr1 after batch mint\n\tuassert.Equal(t, int64(200), balanceBatch[2])\n\t// Check token2's balance of addr2 after batch mint\n\tuassert.Equal(t, int64(400), balanceBatch[3])\n}\n\nfunc TestBurn(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr := address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\ttid1 := TokenID(\"1\")\n\ttid2 := TokenID(\"2\")\n\n\tdummy.mintBatch(addr, []TokenID{tid1, tid2}, []int64{100, 200})\n\terr := dummy.Burn(zeroAddress, tid1, int64(60))\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.Burn(addr, tid1, int64(160))\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.Burn(addr, tid1, int64(60))\n\tuassert.NoError(t, err, \"should not result in error\")\n\terr = dummy.Burn(addr, tid2, int64(60))\n\tuassert.NoError(t, err, \"should not result in error\")\n\tbalanceBatch, err := dummy.BalanceOfBatch([]address{addr, addr}, []TokenID{tid1, tid2})\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\t// Check token1's balance of addr after burn\n\tuassert.Equal(t, int64(40), balanceBatch[0])\n\t// Check token2's balance of addr after burn\n\tuassert.Equal(t, int64(140), balanceBatch[1])\n}\n\nfunc TestBatchBurn(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr := address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\ttid1 := TokenID(\"1\")\n\ttid2 := TokenID(\"2\")\n\n\tdummy.mintBatch(addr, []TokenID{tid1, tid2}, []int64{100, 200})\n\terr := dummy.BatchBurn(zeroAddress, []TokenID{tid1, tid2}, []int64{60, 60})\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.BatchBurn(addr, []TokenID{tid1, tid2}, []int64{160, 60})\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.BatchBurn(addr, []TokenID{tid1, tid2}, []int64{60, 60})\n\tuassert.NoError(t, err, \"should not result in error\")\n\tbalanceBatch, err := dummy.BalanceOfBatch([]address{addr, addr}, []TokenID{tid1, tid2})\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\t// Check token1's balance of addr after batch burn\n\tuassert.Equal(t, int64(40), balanceBatch[0])\n\t// Check token2's balance of addr after batch burn\n\tuassert.Equal(t, int64(140), balanceBatch[1])\n}\n"},{"name":"errors.gno","body":"package grc1155\n\nimport \"errors\"\n\nvar (\n\tErrInvalidAddress                         = errors.New(\"invalid address\")\n\tErrMismatchLength                         = errors.New(\"accounts and ids length mismatch\")\n\tErrCannotTransferToSelf                   = errors.New(\"cannot send transfer to self\")\n\tErrTransferToRejectedOrNonGRC1155Receiver = errors.New(\"transfer to rejected or non GRC1155Receiver implementer\")\n\tErrCallerIsNotOwnerOrApproved             = errors.New(\"caller is not token owner or approved\")\n\tErrInsufficientBalance                    = errors.New(\"insufficient balance for transfer\")\n\tErrBurnAmountExceedsBalance               = errors.New(\"burn amount exceeds balance\")\n\tErrInvalidAmount                          = errors.New(\"invalid amount\")\n)\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/grc1155/v0\"\ngno = \"0.9\"\n"},{"name":"igrc1155.gno","body":"package grc1155\n\ntype IGRC1155 interface {\n\tSafeTransferFrom(from, to address, tid TokenID, amount int64) error\n\tSafeBatchTransferFrom(from, to address, batch []TokenID, amounts []int64) error\n\tBalanceOf(owner address, tid TokenID) (int64, error)\n\tBalanceOfBatch(owners []address, batch []TokenID) ([]int64, error)\n\tSetApprovalForAll(operator address, approved bool) error\n\tIsApprovedForAll(owner, operator address) bool\n}\n\ntype TokenID string\n\ntype TransferSingleEvent struct {\n\tOperator address\n\tFrom     address\n\tTo       address\n\tTokenID  TokenID\n\tAmount   int64\n}\n\ntype TransferBatchEvent struct {\n\tOperator address\n\tFrom     address\n\tTo       address\n\tBatch    []TokenID\n\tAmounts  []int64\n}\n\ntype ApprovalForAllEvent struct {\n\tOwner    address\n\tOperator address\n\tApproved bool\n}\n\ntype UpdateURIEvent struct {\n\tURI string\n}\n\ntype MultiTokenGetter func() IGRC1155\n"},{"name":"util.gno","body":"package grc1155\n\nconst zeroAddress address = \"\"\n\nfunc isValidAddress(addr address) bool {\n\tif !addr.IsValid() {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc emit(event any) {\n\t// TODO: setup a pubsub system here?\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"IDyUP5iLFBnOVPQmqI5nAk+AcYS9kFM0h2JFYHOliIkQqoSjmOlfwDWIWmJjCXBLk4wEyenIUrJLZjjuoWoDyg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"grc777","path":"gno.land/p/nt/grc777/v0","files":[{"name":"dummy_test.gno","body":"package grc777\n\nimport (\n\t\"testing\"\n)\n\ntype dummyImpl struct{}\n\n// FIXME: this should fail.\nvar _ IGRC777 = (*dummyImpl)(nil)\n\nfunc TestInterface(t *testing.T) {\n\tvar _ IGRC777 = \u0026dummyImpl{}\n}\n\nfunc (impl *dummyImpl) GetName() string                     { panic(\"not implemented\") }\nfunc (impl *dummyImpl) GetSymbol() string                   { panic(\"not implemented\") }\nfunc (impl *dummyImpl) GetDecimals() int                    { panic(\"not implemented\") }\nfunc (impl *dummyImpl) Granularity() (granularity int64)    { panic(\"not implemented\") }\nfunc (impl *dummyImpl) TotalSupply() (supply int64)         { panic(\"not implemented\") }\nfunc (impl *dummyImpl) BalanceOf(address_XXX address) int64 { panic(\"not implemented\") }\nfunc (impl *dummyImpl) Burn(amount int64, data []byte)      { panic(\"not implemented\") }\nfunc (impl *dummyImpl) AuthorizeOperator(operator address)  { panic(\"not implemented\") }\nfunc (impl *dummyImpl) RevokeOperator(operators address)    { panic(\"not implemented\") }\nfunc (impl *dummyImpl) DefaultOperators() []address         { panic(\"not implemented\") }\nfunc (impl *dummyImpl) Send(recipient address, amount int64, data []byte) {\n\tpanic(\"not implemented\")\n}\n\nfunc (impl *dummyImpl) IsOperatorFor(operator, tokenHolder address) bool {\n\tpanic(\"not implemented\")\n}\n\nfunc (impl *dummyImpl) OperatorSend(sender, recipient address, amount int64, data, operatorData []byte) {\n\tpanic(\"not implemented\")\n}\n\nfunc (impl *dummyImpl) OperatorBurn(account address, amount int64, data, operatorData []byte) {\n\tpanic(\"not implemented\")\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/grc777/v0\"\ngno = \"0.9\"\n"},{"name":"igrc777.gno","body":"package grc777\n\n// TODO: use big.Int or a custom int64 instead of int64\n\ntype IGRC777 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 smallest part of the token that is not divisible. This\n\t// means all token operations (creation, movement and destruction) must\n\t// have amounts that are a multiple of this number.\n\t//\n\t// For most token contracts, this value will equal 1.\n\tGranularity() (granularity int64)\n\n\t// Returns the amount of tokens in existence.\n\tTotalSupply() (supply int64)\n\n\t// Returns the amount of tokens owned by an account (`owner`).\n\tBalanceOf(address_XXX address) int64\n\n\t// Moves `amount` tokens from the caller's account to `recipient`.\n\t//\n\t// If send or receive hooks are registered for the caller and `recipient`,\n\t// the corresponding functions will be called with `data` and empty\n\t// `operatorData`. See {IERC777Sender} and {IERC777Recipient}.\n\t//\n\t// Emits a {Sent} event.\n\t//\n\t// Requirements\n\t//\n\t// - the caller must have at least `amount` tokens.\n\t// - `recipient` cannot be the zero address.\n\t// - if `recipient` is a contract, it must implement the {IERC777Recipient}\n\t// interface.\n\tSend(recipient address, amount int64, data []byte)\n\n\t// Destroys `amount` tokens from the caller's account, reducing the\n\t// total supply.\n\t//\n\t// If a send hook is registered for the caller, the corresponding function\n\t// will be called with `data` and empty `operatorData`. See {IERC777Sender}.\n\t//\n\t// Emits a {Burned} event.\n\t//\n\t// Requirements\n\t//\n\t// - the caller must have at least `amount` tokens.\n\tBurn(amount int64, data []byte)\n\n\t// Returns true if an account is an operator of `tokenHolder`.\n\t// Operators can send and burn tokens on behalf of their owners. All\n\t// accounts are their own operator.\n\t//\n\t// See {operatorSend} and {operatorBurn}.\n\tIsOperatorFor(operator, tokenHolder address) bool\n\n\t// Make an account an operator of the caller.\n\t//\n\t// See {isOperatorFor}.\n\t//\n\t// Emits an {AuthorizedOperator} event.\n\t//\n\t// Requirements\n\t//\n\t// - `operator` cannot be calling address.\n\tAuthorizeOperator(operator address)\n\n\t// Revoke an account's operator status for the caller.\n\t//\n\t// See {isOperatorFor} and {defaultOperators}.\n\t//\n\t// Emits a {RevokedOperator} event.\n\t//\n\t// Requirements\n\t//\n\t// - `operator` cannot be calling address.\n\tRevokeOperator(operators address)\n\n\t// Returns the list of default operators. These accounts are operators\n\t// for all token holders, even if {authorizeOperator} was never called on\n\t// them.\n\t//\n\t// This list is immutable, but individual holders may revoke these via\n\t// {revokeOperator}, in which case {isOperatorFor} will return false.\n\tDefaultOperators() []address\n\n\t// Moves `amount` tokens from `sender` to `recipient`. The caller must\n\t// be an operator of `sender`.\n\t//\n\t// If send or receive hooks are registered for `sender` and `recipient`,\n\t// the corresponding functions will be called with `data` and\n\t// `operatorData`. See {IERC777Sender} and {IERC777Recipient}.\n\t//\n\t// Emits a {Sent} event.\n\t//\n\t// Requirements\n\t//\n\t// - `sender` cannot be the zero address.\n\t// - `sender` must have at least `amount` tokens.\n\t// - the caller must be an operator for `sender`.\n\t// - `recipient` cannot be the zero address.\n\t// - if `recipient` is a contract, it must implement the {IERC777Recipient}\n\t// interface.\n\tOperatorSend(sender, recipient address, amount int64, data, operatorData []byte)\n\n\t// Destroys `amount` tokens from `account`, reducing the total supply.\n\t// The caller must be an operator of `account`.\n\t//\n\t// If a send hook is registered for `account`, the corresponding function\n\t// will be called with `data` and `operatorData`. See {IERC777Sender}.\n\t//\n\t// Emits a {Burned} event.\n\t//\n\t// Requirements\n\t//\n\t// - `account` cannot be the zero address.\n\t// - `account` must have at least `amount` tokens.\n\t// - the caller must be an operator for `account`.\n\tOperatorBurn(account address, amount int64, data, operatorData []byte)\n}\n\n// Emitted when `amount` tokens are created by `operator` and assigned to `to`.\n//\n// Note that some additional user `data` and `operatorData` can be logged in the event.\ntype MintedEvent struct {\n\tOperator     address\n\tTo           address\n\tAmount       int64\n\tData         []byte\n\tOperatorData []byte\n}\n\n// Emitted when `operator` destroys `amount` tokens from `account`.\n//\n// Note that some additional user `data` and `operatorData` can be logged in the event.\ntype BurnedEvent struct {\n\tOperator     address\n\tFrom         address\n\tAmount       int64\n\tData         []byte\n\tOperatorData []byte\n}\n\n// Emitted when `operator` is made operator for `tokenHolder`\ntype AuthorizedOperatorEvent struct {\n\tOperator    address\n\tTokenHolder address\n}\n\n// Emitted when `operator` is revoked its operator status for `tokenHolder`.\ntype RevokedOperatorEvent struct {\n\tOperator    address\n\tTokenHolder address\n}\n\ntype SentEvent struct {\n\tOperator     address\n\tFrom         address\n\tTo           address\n\tAmount       int64\n\tData         []byte\n\tOperatorData []byte\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"mRakv/DeBIMSX/vujE9rUlf+3FURMQYvHKFG3bUMqYAnNFdMtpvnxFGEP35wEChUo6KrFHIbNt0bEkAOc+upcQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"pausable","path":"gno.land/p/nt/pausable/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# `pausable` - Emergency-stop pattern\n\nComposes with `gno.land/p/nt/ownable/v0` to give an owner a programmatic kill\nswitch: flip `Pause` to halt sensitive operations, `Unpause` to resume.\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/pausable/v0\"\n)\n\nvar p *pausable.Pausable\n\nfunc init() {\n    caller := runtime.PreviousRealm()\n    if !caller.IsUserCall() {\n        panic(\"must be deployed by a user\")\n    }\n    p = pausable.NewFromOwnable(ownable.NewWithAddress(caller.Address()))\n}\n\n// Transfer is blocked while paused.\nfunc Transfer(cur realm, to address, amount int64) error {\n    if p.IsPaused() {\n        return pausable.ErrPaused\n    }\n    // ... do the transfer\n    return nil\n}\n\n// Only the owner can flip the switch.\nfunc Pause(cur realm) error   { return p.Pause(0, cur) }\nfunc Unpause(cur realm) error { return p.Unpause(0, cur) }\n```\n\n`Pausable` does not enforce anything by itself — callers must check `IsPaused()`\n(or compare against `ErrPaused`) inside the functions that should be gated.\n\n## API\n\n```go\ntype Pausable struct{ /* unexported */ }\n\nvar ErrPaused = errors.New(\"pausable: realm is currently paused\")\n\n// NewFromOwnable wraps an existing Ownable. The Pausable starts unpaused.\nfunc NewFromOwnable(o *ownable.Ownable) *Pausable\n\n// State.\nfunc (p Pausable) IsPaused() bool\n\n// Owner-only mutations (thread the caller's own cur; pass 0 as the first arg).\n// Return ownable.ErrUnauthorized if the caller is not the owner.\nfunc (p *Pausable) Pause(_ int, rlm realm) error   // emits \"Paused\"   event with `by` = owner\nfunc (p *Pausable) Unpause(_ int, rlm realm) error // emits \"Unpaused\" event with `by` = owner\n\n// Access the underlying ownable (e.g. to transfer ownership).\nfunc (p *Pausable) Ownable() *ownable.Ownable\n```\n\n## Notes\n\n- Ownership is delegated to the embedded `*ownable.Ownable`. `Pause`/`Unpause`\n  assert `rlm.IsCurrent()` and require `rlm.Previous().Address()` to equal the\n  owner, so pass your own `cur` as `rlm`.\n- Pausing is advisory: a function only respects the pause flag if it explicitly\n  checks `IsPaused()`.\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 pausable provides a mechanism to programmatically pause and unpause\n// functionality, allowing an owner to restrict operations when the contract\n// is in a paused state.\npackage pausable\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/pausable/v0\"\ngno = \"0.9\"\n"},{"name":"pausable.gno","body":"// Package pausable provides a mechanism to programmatically pause and unpause\n// functionality. This package allows an owner, defined via an Ownable object,\n// to restrict operations or methods when the contract is in a \"paused\" state.\npackage pausable\n\nimport (\n\t\"chain\"\n\t\"errors\"\n\n\t\"gno.land/p/nt/ownable/v0\"\n)\n\ntype Pausable struct {\n\to      *ownable.Ownable\n\tpaused bool\n}\n\nvar ErrPaused = errors.New(\"pausable: realm is currently paused\")\n\n// NewFromOwnable is the same as New, but with a pre-existing top-level ownable\nfunc NewFromOwnable(ownable *ownable.Ownable) *Pausable {\n\treturn \u0026Pausable{\n\t\to:      ownable,\n\t\tpaused: false,\n\t}\n}\n\n// IsPaused checks if Pausable is paused\nfunc (p Pausable) IsPaused() bool {\n\treturn p.paused\n}\n\n// Pause sets the state of Pausable to true, meaning all pausable functions\n// are paused. rlm must be the caller's own captured cur;\n// rlm.Previous().Address() must equal the underlying Ownable's owner.\nfunc (p *Pausable) Pause(_ int, rlm realm) error {\n\tif !rlm.IsCurrent() {\n\t\treturn ownable.ErrUnauthorized\n\t}\n\tif !p.o.OwnedBy(rlm.Previous().Address()) {\n\t\treturn ownable.ErrUnauthorized\n\t}\n\n\tp.paused = true\n\tchain.Emit(\"Paused\", \"by\", p.o.Owner().String())\n\n\treturn nil\n}\n\n// Unpause sets the state of Pausable to false, meaning all pausable\n// functions are resumed. rlm must be the caller's own captured cur;\n// rlm.Previous().Address() must equal the underlying Ownable's owner.\nfunc (p *Pausable) Unpause(_ int, rlm realm) error {\n\tif !rlm.IsCurrent() {\n\t\treturn ownable.ErrUnauthorized\n\t}\n\tif !p.o.OwnedBy(rlm.Previous().Address()) {\n\t\treturn ownable.ErrUnauthorized\n\t}\n\n\tp.paused = false\n\tchain.Emit(\"Unpaused\", \"by\", p.o.Owner().String())\n\n\treturn nil\n}\n\n// Ownable returns the underlying ownable\nfunc (p *Pausable) Ownable() *ownable.Ownable {\n\treturn p.o\n}\n"},{"name":"pausable_test.gno","body":"package pausable\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/ownable/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nvar (\n\tfirstCaller = address(\"g1l9aypkr8xfvs82zeux486ddzec88ty69lue9de\")\n\to           = ownable.NewWithAddress(firstCaller)\n)\n\nfunc TestNewFromOwnable(cur realm, t *testing.T) {\n\tresult := NewFromOwnable(o)\n\n\turequire.Equal(t, firstCaller.String(), result.Ownable().Owner().String())\n}\n\nfunc TestSetUnpaused(cur realm, t *testing.T) {\n\tresult := NewFromOwnable(o)\n\n\ttesting.SetRealm(testing.NewUserRealm(firstCaller))\n\tfunc(cur realm) {\n\t\turequire.NoError(t, result.Unpause(0, cur))\n\t}(cross(cur))\n\tuassert.False(t, result.IsPaused(), \"Expected result to be unpaused\")\n}\n\nfunc TestSetPaused(cur realm, t *testing.T) {\n\tresult := NewFromOwnable(o)\n\n\ttesting.SetRealm(testing.NewUserRealm(firstCaller))\n\tfunc(cur realm) {\n\t\turequire.NoError(t, result.Pause(0, cur))\n\t}(cross(cur))\n\tuassert.True(t, result.IsPaused(), \"Expected result to be paused\")\n}\n\nfunc TestIsPaused(cur realm, t *testing.T) {\n\tresult := NewFromOwnable(o)\n\turequire.False(t, result.IsPaused(), \"Expected result to be unpaused\")\n\n\ttesting.SetRealm(testing.NewUserRealm(firstCaller))\n\tfunc(cur realm) {\n\t\turequire.NoError(t, result.Pause(0, cur))\n\t}(cross(cur))\n\tuassert.True(t, result.IsPaused(), \"Expected result to be paused\")\n}\n\nfunc TestOwnable(cur realm, t *testing.T) {\n\tresult := NewFromOwnable(o)\n\n\tuassert.Equal(t, result.Ownable().Owner(), o.Owner())\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"ltGZktBZKXlr89HZ0rVe2wB9rg+ovfSMHJAx4iOcemhNw4Nwvmeyo9hlGBhLTUTP2/75ffWInZ8feKtSxraLEQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"watchdog","path":"gno.land/p/nt/watchdog/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# `watchdog` - Liveness timer\n\nA deadman-switch primitive: a `Watchdog` is \"alive\" as long as `Alive()` has been\ncalled within the last `Duration`. Use it to monitor an off-chain process, an\noracle, or any actor that must check in regularly.\n\n## Usage\n\n```go\npackage myrealm\n\nimport (\n    \"time\"\n\n    \"gno.land/p/nt/watchdog/v0\"\n)\n\nvar w = \u0026watchdog.Watchdog{Duration: 10 * time.Minute}\n\n// Heartbeat: the monitored party calls this on every cycle.\nfunc Heartbeat(cur realm) {\n    w.Alive()\n}\n\n// Render shows the current liveness state.\nfunc Render(_ string) string {\n    return \"status: \" + w.Status() // \"OK\" or \"KO\"\n}\n```\n\nNote that `Watchdog` has no built-in authorization — gate `Alive()` with\n`gno.land/p/nt/ownable/v0` or another check if only a specific party should be\nable to reset the timer.\n\n## API\n\n```go\ntype Watchdog struct {\n    Duration time.Duration // how long since the last Alive() before going down\n    // unexported timestamps\n}\n\n// Heartbeat: marks the watchdog alive at time.Now().\nfunc (w *Watchdog) Alive()\n\n// Queries.\nfunc (w Watchdog)  IsAlive()   bool      // true if time.Since(lastUpdate) \u003c Duration\nfunc (w Watchdog)  Status()    string    // \"OK\" if alive, \"KO\" otherwise\nfunc (w Watchdog)  UpSince()   time.Time // last time the watchdog recovered from down\nfunc (w Watchdog)  DownSince() time.Time // last update if currently down, zero value otherwise\n```\n\n## Notes\n\n- A fresh `Watchdog{Duration: d}` starts as **down** until the first `Alive()`\n  call (`lastUpdate` is the zero time).\n- `Duration` is a plain field — it can be tuned at runtime, but consider gating\n  any write to it.\n- Liveness is measured in **block time**, not wall clock: `time.Now()` returns the\n  block timestamp, and the down/up transition is observed lazily on the next read\n  (`IsAlive`/`Status`). Size `Duration` against block cadence, not real seconds.\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 watchdog provides a simple watchdog timer for monitoring liveness.\npackage watchdog\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/watchdog/v0\"\ngno = \"0.9\"\n"},{"name":"watchdog.gno","body":"package watchdog\n\nimport \"time\"\n\ntype Watchdog struct {\n\tDuration   time.Duration\n\tlastUpdate time.Time\n\tlastDown   time.Time\n}\n\nfunc (w *Watchdog) Alive() {\n\tnow := time.Now()\n\tif !w.IsAlive() {\n\t\tw.lastDown = now\n\t}\n\tw.lastUpdate = now\n}\n\nfunc (w Watchdog) Status() string {\n\tif w.IsAlive() {\n\t\treturn \"OK\"\n\t}\n\treturn \"KO\"\n}\n\nfunc (w Watchdog) IsAlive() bool {\n\treturn time.Since(w.lastUpdate) \u003c w.Duration\n}\n\nfunc (w Watchdog) UpSince() time.Time {\n\treturn w.lastDown\n}\n\nfunc (w Watchdog) DownSince() time.Time {\n\tif !w.IsAlive() {\n\t\treturn w.lastUpdate\n\t}\n\treturn time.Time{}\n}\n"},{"name":"watchdog_test.gno","body":"package watchdog\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nfunc TestPackage(t *testing.T) {\n\tw := Watchdog{Duration: 5 * time.Minute}\n\tuassert.False(t, w.IsAlive())\n\tw.Alive()\n\tuassert.True(t, w.IsAlive())\n\t// XXX: add more tests when we'll be able to \"skip time\".\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"J/zDRSpiznwqdvN/lI3F9FdRqOjkREfNGmInz5ddDttRw+wpfwil0ycx0mils2vs9wxIuCKtellPdct5pRzLqw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"ownable2step","path":"gno.land/p/oxtekgrinder/ownable2step","files":[{"name":"errors.gno","body":"package ownable2step\n\nimport \"errors\"\n\nvar (\n\tErrNoPendingOwner      = errors.New(\"ownable2step: no pending owner\")\n\tErrUnauthorized        = errors.New(\"ownable2step: caller is not owner\")\n\tErrPendingUnauthorized = errors.New(\"ownable2step: caller is not pending owner\")\n\tErrInvalidAddress      = errors.New(\"ownable2step: new owner address is invalid\")\n)\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/oxtekgrinder/ownable2step\"\ngno = \"0.9\"\n"},{"name":"ownable.gno","body":"package ownable2step\n\nimport \"chain\"\n\nconst OwnershipTransferEvent = \"OwnershipTransfer\"\n\n// Ownable2Step is a two-step ownership transfer package.\n// It allows the current owner to set a new owner; the new owner must\n// accept ownership before the transfer completes.\n//\n// Authority-mutating methods (TransferOwnership, AcceptOwnership,\n// DropOwnership) take (_ int, rlm realm). The caller threads its own\n// cur; the method asserts rlm.IsCurrent() and identifies the principal\n// as rlm.Previous().Address() — unforgeable.\n//\n//\to.TransferOwnership(0, cur, newOwner)\n//\to.AcceptOwnership(0, cur)\n//\to.DropOwnership(0, cur)\n//\n// XXX Implement using Ownable instead of replicating it.\ntype Ownable2Step struct {\n\towner        address\n\tpendingOwner address\n}\n\n// NewWithAddress constructs an Ownable2Step with the given owner. This\n// is the only constructor — the previous NewWithOrigin / NewWithCurrent\n// sugar baked in runtime walks; the realm using this package now picks\n// the owner address explicitly (e.g. cur.Previous().Address() after\n// verifying cur.Previous().IsUserCall() in init).\nfunc NewWithAddress(addr address) *Ownable2Step {\n\treturn \u0026Ownable2Step{\n\t\towner:        addr,\n\t\tpendingOwner: \"\",\n\t}\n}\n\n// TransferOwnership initiates the transfer of ownership to newOwner by\n// setting the PendingOwner. rlm must be the caller's own captured cur;\n// rlm.Previous().Address() must equal the current owner.\nfunc (o *Ownable2Step) TransferOwnership(_ int, rlm realm, newOwner address) error {\n\tif !rlm.IsCurrent() {\n\t\treturn ErrUnauthorized\n\t}\n\tif rlm.Previous().Address() != o.owner {\n\t\treturn ErrUnauthorized\n\t}\n\tif !newOwner.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\to.pendingOwner = newOwner\n\treturn nil\n}\n\n// AcceptOwnership accepts the pending ownership transfer. rlm must be\n// the caller's own captured cur; rlm.Previous().Address() must equal\n// the pending owner.\nfunc (o *Ownable2Step) AcceptOwnership(_ int, rlm realm) error {\n\tif o.pendingOwner.String() == \"\" {\n\t\treturn ErrNoPendingOwner\n\t}\n\tif !rlm.IsCurrent() {\n\t\treturn ErrPendingUnauthorized\n\t}\n\tif rlm.Previous().Address() != o.pendingOwner {\n\t\treturn ErrPendingUnauthorized\n\t}\n\to.owner = o.pendingOwner\n\to.pendingOwner = \"\"\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 *Ownable2Step) DropOwnership(_ int, rlm realm) error {\n\tif !rlm.IsCurrent() {\n\t\treturn ErrUnauthorized\n\t}\n\tif rlm.Previous().Address() != o.owner {\n\t\treturn ErrUnauthorized\n\t}\n\tprevOwner := o.owner\n\to.owner = \"\"\n\to.pendingOwner = \"\"\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 current owner address.\nfunc (o *Ownable2Step) Owner() address {\n\treturn o.owner\n}\n\n// PendingOwner returns the pending owner address (empty if none).\nfunc (o *Ownable2Step) PendingOwner() address {\n\treturn o.pendingOwner\n}\n\n// OwnedBy reports whether the given address is the current owner.\nfunc (o *Ownable2Step) OwnedBy(addr address) bool {\n\treturn addr == o.owner\n}\n\n// AssertOwnedBy panics with ErrUnauthorized if addr is not the owner.\nfunc (o *Ownable2Step) AssertOwnedBy(addr address) {\n\tif addr != o.owner {\n\t\tpanic(ErrUnauthorized)\n\t}\n}\n"},{"name":"ownable_test.gno","body":"package ownable2step\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nvar (\n\talice = testutils.TestAddress(\"alice\")\n\tbob   = testutils.TestAddress(\"bob\")\n)\n\nfunc TestNewWithAddress(cur realm, t *testing.T) {\n\to := NewWithAddress(alice)\n\n\tgot := o.Owner()\n\tpendingOwner := o.PendingOwner()\n\n\tuassert.Equal(t, got, alice)\n\tuassert.Equal(t, pendingOwner.String(), \"\")\n}\n\nfunc TestInitiateTransferOwnership(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\to := NewWithAddress(alice)\n\tfunc(cur realm) {\n\t\terr := o.TransferOwnership(0, cur, bob)\n\t\turequire.NoError(t, err)\n\t}(cross(cur))\n\n\towner := o.Owner()\n\tpendingOwner := o.PendingOwner()\n\n\tuassert.Equal(t, owner, alice)\n\tuassert.Equal(t, pendingOwner, bob)\n}\n\nfunc TestTransferOwnership(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\to := NewWithAddress(alice)\n\tfunc(cur realm) {\n\t\terr := o.TransferOwnership(0, cur, bob)\n\t\turequire.NoError(t, err)\n\t}(cross(cur))\n\n\towner := o.Owner()\n\tpendingOwner := o.PendingOwner()\n\n\tuassert.Equal(t, owner, alice)\n\tuassert.Equal(t, pendingOwner, bob)\n\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\tfunc(cur realm) {\n\t\terr := o.AcceptOwnership(0, cur)\n\t\turequire.NoError(t, err)\n\t}(cross(cur))\n\n\towner = o.Owner()\n\tpendingOwner = o.PendingOwner()\n\n\tuassert.Equal(t, owner, bob)\n\tuassert.Equal(t, pendingOwner.String(), \"\")\n}\n\nfunc TestOwnedBy(cur realm, t *testing.T) {\n\to := NewWithAddress(alice)\n\tuassert.True(t, o.OwnedBy(alice))\n\tuassert.False(t, o.OwnedBy(bob))\n}\n\nfunc TestDropOwnership(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\to := NewWithAddress(alice)\n\tfunc(cur realm) {\n\t\terr := o.DropOwnership(0, cur)\n\t\turequire.NoError(t, err, \"DropOwnership failed\")\n\t}(cross(cur))\n\n\towner := o.Owner()\n\tuassert.Empty(t, owner, \"owner should be empty\")\n}\n\nfunc TestDropOwnershipVulnerability(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\to := NewWithAddress(alice)\n\tfunc(cur realm) {\n\t\t// alice initiates ownership transfer to bob\n\t\terr := o.TransferOwnership(0, cur, bob)\n\t\turequire.NoError(t, err)\n\n\t\t// alice drops ownership while ownership transfer is pending\n\t\terr = o.DropOwnership(0, cur)\n\t\turequire.NoError(t, err)\n\t}(cross(cur))\n\n\towner := o.Owner()\n\tpendingOwner := o.PendingOwner()\n\tuassert.Empty(t, owner, \"owner should be empty\")\n\tuassert.Empty(t, pendingOwner.String(), \"pending owner should be empty\")\n\n\t// verify bob can no longer claim ownership\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\tfunc(cur realm) {\n\t\terr := o.AcceptOwnership(0, cur)\n\t\tuassert.ErrorContains(t, err, ErrNoPendingOwner.Error())\n\t}(cross(cur))\n}\n\n// Errors\n\nfunc TestErrUnauthorized(cur realm, t *testing.T) {\n\to := NewWithAddress(alice)\n\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\tfunc(cur realm) {\n\t\tuassert.ErrorContains(t, o.TransferOwnership(0, cur, alice), ErrUnauthorized.Error())\n\t\tuassert.ErrorContains(t, o.DropOwnership(0, cur), ErrUnauthorized.Error())\n\t}(cross(cur))\n}\n\nfunc TestErrInvalidAddress(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\to := NewWithAddress(alice)\n\tfunc(cur realm) {\n\t\terr := o.TransferOwnership(0, cur, \"\")\n\t\tuassert.ErrorContains(t, err, ErrInvalidAddress.Error())\n\n\t\terr = o.TransferOwnership(0, cur, \"10000000001000000000100000000010000000001000000000\")\n\t\tuassert.ErrorContains(t, err, ErrInvalidAddress.Error())\n\t}(cross(cur))\n}\n\nfunc TestErrNoPendingOwner(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\to := NewWithAddress(alice)\n\tfunc(cur realm) {\n\t\terr := o.AcceptOwnership(0, cur)\n\t\tuassert.ErrorContains(t, err, ErrNoPendingOwner.Error())\n\t}(cross(cur))\n}\n\nfunc TestErrPendingUnauthorized(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\to := NewWithAddress(alice)\n\tfunc(cur realm) {\n\t\terr := o.TransferOwnership(0, cur, bob)\n\t\turequire.NoError(t, err)\n\n\t\t// alice (still the owner, NOT the pending) tries to AcceptOwnership.\n\t\terr = o.AcceptOwnership(0, cur)\n\t\tuassert.ErrorContains(t, err, ErrPendingUnauthorized.Error())\n\t}(cross(cur))\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"tDzIumEHamyjGW9J2VzjNZ3T8EgwCIWvfe/6ZOA6slNJhfh7ZrQeuH6izm2EWXcedNs7tc1eKA663hKls3DrAg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"levenshtein","path":"gno.land/p/pol/levenshtein","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/pol/levenshtein\"\ngno = \"0.9\"\n"},{"name":"levenshtein.gno","body":"// from www.youtube.com/watch?v=d-Eq6x1yssU\n// Levenshtein distance is an algorithm to detect how close 2 words are.\n// For example, \"Float\" and \"Boats\" have a levenstein distance of 3.\n// Why ? Because you need 3 steps to go from Boats to Float\n// - We start with the word (Boats)\n// - Remove the s (Boat)\n// - Insert the l (Bloat)\n// - Substitute b with f (Float)\npackage levenshtein\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/p/nt/avl/v0\"\n)\n\n// Distance computes the Levenshtein distance between two strings.\n//\n// The Levenshtein distance is a measure of the similarity between two strings,\n// defined as the minimum number of single-character edits (insertions,\n// deletions, or substitutions) required to change one string into the other.\n//\n// For example, the Levenshtein distance between \"Float\" and \"Boats\" is 3:\n//   - Remove 's' from \"Boats\" → \"Boat\"\n//   - Insert 'l' → \"Bloat\"\n//   - Substitute 'b' with 'f' → \"Float\"\n//\n// This implementation uses a memory-efficient dynamic programming approach\n// that computes the distance in O(len(a) * len(b)) time and O(min(len(a), len(b))) space.\nfunc Distance(a, b string) uint {\n\t// make sure a is the longest\n\tif len(a) \u003c len(b) {\n\t\ta, b = b, a\n\t}\n\n\t// init first row\n\tline := make([]uint, len(b)+1)\n\tfor j := range line {\n\t\tline[j] = uint(j)\n\t}\n\n\t// main loop\n\tfor i := 1; i \u003c= len(a); i++ {\n\t\tprev := line[0]\n\t\tline[0] = uint(i)\n\t\tfor j := 1; j \u003c= len(b); j++ {\n\t\t\told := line[j]\n\t\t\tif a[i-1] == b[j-1] {\n\t\t\t\tline[j] = prev\n\t\t\t} else {\n\t\t\t\tline[j] = min3(\n\t\t\t\t\tline[j-1]+1, // insertion\n\t\t\t\t\tline[j]+1,   // deletion\n\t\t\t\t\tprev+1,      // substitution\n\t\t\t\t)\n\t\t\t}\n\t\t\tprev = old\n\t\t}\n\t}\n\treturn line[len(b)]\n}\n\n// Levenshteinable is an interface for types that can be compared using\n// Levenshtein distance. It provides the LString method, which returns\n// the string representation used for comparison.\n//\n// Note: This differs from the standard String() method, which may be used\n// for display or debugging.\ntype Levenshteinable interface {\n\tLString() string\n}\n\n// PickFromString returns the Levenshteinable object from the slice xs\n// that is closest in Levenshtein distance to the input string xStr.\n//\n// It returns the best match along with its distance. If xs is empty,\n// it returns (nil, 0).\n//\n// This function runs in O(n) time, where n is the length of the slice.\nfunc PickFromString(xStr string, xs []Levenshteinable) (Levenshteinable, uint) {\n\t// check for empty list\n\tif len(xs) == 0 {\n\t\treturn nil, 0\n\t}\n\n\t// initialize\n\tbestDistance := Distance(xStr, xs[0].LString())\n\tbest := xs[0]\n\n\t// loop through objects\n\tfor n := 1; n \u003c len(xs); n++ {\n\t\tdist := Distance(xStr, xs[n].LString())\n\t\tif dist \u003c bestDistance {\n\t\t\tbestDistance = dist\n\t\t\tbest = xs[n]\n\t\t}\n\t}\n\treturn best, bestDistance\n}\n\n// Pick returns the Levenshteinable object from the slice xs that is\n// closest in Levenshtein distance to the input object x.\n//\n// This is a convenience wrapper around PickFromString(x.LString(), xs).\n// It runs in O(n) time.\nfunc Pick(x Levenshteinable, xs []Levenshteinable) (Levenshteinable, uint) {\n\treturn PickFromString(x.LString(), xs)\n}\n\nfunc padLeftNum(n uint, width int) string {\n\ts := strconv.FormatUint(uint64(n), 10)\n\tfor len(s) \u003c width {\n\t\ts = \"0\" + s\n\t}\n\treturn s\n}\n\n// SeveralPickFromString returns the n closest Levenshteinable objects\n// to the input string xStr from the slice xs, using Levenshtein distance.\n//\n// If xs is empty, it returns nil. If n is 0, it returns an empty slice.\n//\n// The result is not guaranteed to be sorted by distance.\n// This function runs in O(n) time using an AVL tree for efficient selection.\nfunc SeveralPickFromString(xStr string, xs []Levenshteinable, n uint) []Levenshteinable {\n\t// error casing\n\tif len(xs) == 0 {\n\t\treturn nil\n\t}\n\tif n == 0 {\n\t\treturn []Levenshteinable{}\n\t}\n\n\ttree := avl.NewTree()\n\tmaxKey := \"\"\n\n\tfor k, v := range xs {\n\t\t// calculate distance and create unique key\n\t\td := Distance(xStr, v.LString())\n\t\t// ufmt.Sprintf(\"%016u:%d\", d, k)\n\t\tkey := padLeftNum(d, 6) + \":\" + strconv.Itoa(k) // no sprintf ( ;-;)\n\t\ttree.Set(key, v)\n\n\t\t// remove the element with the greater key\n\t\tif uint(tree.Size()) \u003e n {\n\t\t\ttree.ReverseIterate(\"\", \"\", func(k string, _ any) bool {\n\t\t\t\tmaxKey = k\n\t\t\t\treturn true\n\t\t\t})\n\t\t\ttree.Remove(maxKey)\n\t\t}\n\t}\n\n\t// create the list\n\tout := make([]Levenshteinable, 0, tree.Size())\n\ttree.Iterate(\"\", \"\", func(k string, v any) bool {\n\t\tout = append(out, v.(Levenshteinable))\n\t\treturn false\n\t})\n\treturn out\n}\n\n// SeveralPick returns the n closest Levenshteinable objects to the\n// input object x from the slice xs, using Levenshtein distance.\n//\n// This is a convenience wrapper around SeveralPickFromString(x.LString(), xs, n).\n// It runs in O(n) time.\nfunc SeveralPick(x Levenshteinable, xs []Levenshteinable, n uint) []Levenshteinable {\n\treturn SeveralPickFromString(x.LString(), xs, n)\n}\n\nfunc min3(a, b, c uint) uint {\n\tmin := a\n\tif b \u003c min {\n\t\tmin = b\n\t}\n\tif c \u003c min {\n\t\tmin = c\n\t}\n\treturn min\n}\n"},{"name":"levenshtein_test.gno","body":"package levenshtein\n\nimport \"testing\"\n\nfunc TestExample(t *testing.T) {\n\tif Distance(\"float\", \"boats\") != 3 {\n\t\tt.Fatal(\"expected float vs boats to be 3\")\n\t}\n\n\tif Distance(\"kitten\", \"sitting\") != 3 {\n\t\tt.Fatal(\"expected kitten vs sitting to be 3\")\n\t}\n\n\tif Distance(\"foo\", \"foo\") != 0 {\n\t\tt.Fatal(\"expected foo and foo to be 0 (the same)\")\n\t}\n}\n\ntype person struct {\n\tname     string\n\tlastName string\n}\n\nfunc (p person) LString() string {\n\treturn p.name + \" \" + p.lastName\n}\n\nfunc TestWithMyPhoneContact(t *testing.T) {\n\tcontacts := []Levenshteinable{\n\t\tperson{\"maman\", \"\"},\n\t\tperson{\"papa\", \"\"},\n\t\tperson{\"paul\", \"parisot\"},\n\t\tperson{\"leonardo\", \"dicaprio\"},\n\t\tperson{\"jenifer\", \"lawrence\"},\n\t\tperson{\"jae\", \"kwon\"},\n\t\tperson{\"justin\", \"bieber\"},\n\t}\n\twho, _ := PickFromString(\"pau parosit\", contacts)\n\t// println(who.LString(), n)\n\tif who.LString() != \"paul parisot\" {\n\t\tt.Fatal(\"expected to be paul parisot !\")\n\t}\n\n\twho, _ = PickFromString(\"jkwo0n\", contacts)\n\t// println(who.LString(), n)\n\tif who.LString() != \"jae kwon\" {\n\t\tt.Fatal(\"expected to be jae kwon !\")\n\t}\n\n\twho, _ = PickFromString(\"paulparis\", contacts)\n\t// println(who, n)\n}\n\nfunc TestWithAVL(t *testing.T) {\n\tcontacts := []Levenshteinable{\n\t\tperson{\"maman\", \"\"},\n\t\tperson{\"papa\", \"\"},\n\t\tperson{\"paul\", \"parisot\"},\n\t\tperson{\"leonardo\", \"dicaprio\"},\n\t\tperson{\"jenifer\", \"lawrence\"},\n\t\tperson{\"jae\", \"kwon\"},\n\t\tperson{\"justin\", \"bieber\"},\n\t}\n\tlist := SeveralPickFromString(\"paulparis\", contacts, 3)\n\tif len(list) != 3 {\n\t\tt.Fatal(\"expected 3 elements, got \", len(list))\n\t}\n\tif list[0] != contacts[2] {\n\t\tt.Fatal(\"expected first element to be paul parisot\")\n\t}\n\tif list[1] != contacts[1] {\n\t\tt.Fatal(\"expected second to be papa\")\n\t}\n}\n\nfunc TestWhitespaces(t *testing.T) {\n\tif Distance(\"hello world\", \"helloworld\") != 1 {\n\t\tt.Fatal(\"'helloworld' vs 'hello world' doesnt have 1 distance\")\n\t}\n\tif Distance(\"hello  world\", \"hello world\") != 1 {\n\t\tt.Fatal(\"'hello  world' vs 'hello world' doesnt have 1 distance\")\n\t}\n\tif Distance(\"hello world\", \"hello  world\") != 1 {\n\t\tt.Fatal(\"'hello world' vs 'hello  world' doesnt have 1 distance\")\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"M5ZBat1J1E7P/KDiAUpeMe5vHoROOMbyB+8ZZfUG2UR+4AWfwdofRBw2Nbpwdk29qlUiCFIze4NeyNQUK/Jn7w=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1kfd9f5zlvcvy6aammcmqswa7cyjpu2nyt9qfen","package":{"name":"daocond","path":"gno.land/p/samcrew/daocond","files":[{"name":"ballot_avl.gno","body":"package daocond\n\nimport (\n\t\"gno.land/p/nt/avl/v0\"\n)\n\ntype BallotAVL struct {\n\ttree *avl.Tree\n}\n\nfunc NewBallot() Ballot {\n\treturn \u0026BallotAVL{tree: avl.NewTree()}\n}\n\n// Implements Ballot interface\nfunc (b *BallotAVL) Vote(voter string, vote Vote) {\n\tb.tree.Set(voter, vote)\n}\n\n// Implements Ballot interface\nfunc (b *BallotAVL) Get(voter string) Vote {\n\tvote := b.tree.Get(voter)\n\tif vote == nil {\n\t\treturn VoteAbstain\n\t}\n\treturn vote.(Vote)\n}\n\n// Implements Ballot interface\nfunc (b *BallotAVL) Total() int {\n\treturn b.tree.Size()\n}\n\n// Implements Ballot interface\nfunc (b *BallotAVL) Iterate(fn func(voter string, vote Vote) bool) {\n\tb.tree.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tv, ok := value.(Vote)\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\t\treturn fn(key, v)\n\t})\n}\n"},{"name":"cond_and.gno","body":"package daocond\n\nimport (\n\t\"strings\"\n)\n\ntype andCond struct {\n\tconditions []Condition\n}\n\nfunc And(conditions ...Condition) Condition {\n\tif len(conditions) \u003c 2 {\n\t\tpanic(\"at least two conditions are required\")\n\t}\n\treturn \u0026andCond{conditions: conditions}\n}\n\n// Eval implements Condition.\nfunc (c *andCond) Eval(ballot Ballot) bool {\n\tfor _, condition := range c.conditions {\n\t\tif !condition.Eval(ballot) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// Signal implements Condition.\nfunc (c *andCond) Signal(ballot Ballot) float64 {\n\ttotalSignal := 0.0\n\tfor _, condition := range c.conditions {\n\t\ttotalSignal += condition.Signal(ballot)\n\t}\n\treturn totalSignal / float64(len(c.conditions))\n}\n\n// Render implements Condition.\nfunc (c *andCond) Render() string {\n\trenders := []string{}\n\tfor _, condition := range c.conditions {\n\t\trenders = append(renders, condition.Render())\n\t}\n\treturn \"[\" + strings.Join(renders, \" AND \") + \"]\"\n}\n\n// RenderWithVotes implements Condition.\nfunc (c *andCond) RenderWithVotes(ballot Ballot) string {\n\trenders := []string{}\n\tfor _, condition := range c.conditions {\n\t\trenders = append(renders, condition.RenderWithVotes(ballot))\n\t}\n\treturn \"[\" + strings.Join(renders, \" AND \") + \"]\"\n}\n\nvar _ Condition = (*andCond)(nil)\n"},{"name":"cond_gnolovedao.gno","body":"package daocond\n\nimport (\n\t\"errors\"\n\t\"math\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\ntype gnolovDaoCondThreshold struct {\n\tthreshold            float64\n\troles                []string\n\thasRoleFn            func(memberId string, role string) bool\n\tusersWithRoleCountFn func(role string) uint32\n}\n\nvar roleWeights = []float64{3.0, 2.0, 1.0}\n\n// This is our implementation of the govdao codition following Jae Kwon's specification: https://gist.github.com/jaekwon/918ad325c4c8f7fb5d6e022e33cb7eb3\nfunc GnoloveDAOCondThreshold(threshold float64, roles []string, hasRoleFn func(memberId string, role string) bool, usersWithRoleCountFn func(role string) uint32) Condition {\n\tif threshold \u003c= 0 || threshold \u003e 1 {\n\t\tpanic(errors.New(\"invalid threshold\"))\n\t}\n\tif usersWithRoleCountFn == nil {\n\t\tpanic(errors.New(\"nil usersWithRoleCountFn\"))\n\t}\n\tif hasRoleFn == nil {\n\t\tpanic(errors.New(\"nil hasRoleFn\"))\n\t}\n\tif len(roles) \u003e 3 {\n\t\tpanic(\"the gnolove dao condition handles at most 3 roles\")\n\t}\n\treturn \u0026gnolovDaoCondThreshold{\n\t\tthreshold:            threshold,\n\t\troles:                roles,\n\t\thasRoleFn:            hasRoleFn,\n\t\tusersWithRoleCountFn: usersWithRoleCountFn,\n\t}\n}\n\n// Eval implements Condition.\nfunc (c *gnolovDaoCondThreshold) Eval(ballot Ballot) bool {\n\treturn c.yesRatio(ballot) \u003e= c.threshold\n}\n\n// Signal implements Condition.\nfunc (c *gnolovDaoCondThreshold) Signal(ballot Ballot) float64 {\n\treturn math.Min(c.yesRatio(ballot)/c.threshold, 1)\n}\n\n// Render implements Condition.\nfunc (c *gnolovDaoCondThreshold) Render() string {\n\trolePowers := []string{}\n\tfor i, role := range c.roles {\n\t\tweight := strconv.FormatFloat(roleWeights[i], 'f', 2, 64) // ufmt.Sprintf(\"%.2f\", ...) is not working\n\t\trolePowers = append(rolePowers, ufmt.Sprintf(\"%s =\u003e %s power\", role, weight))\n\t}\n\treturn ufmt.Sprintf(\"%g%% of total voting power | %s\", c.threshold*100, strings.Join(rolePowers, \" | \"))\n}\n\n// RenderWithVotes implements Condition.\nfunc (c *gnolovDaoCondThreshold) RenderWithVotes(ballot Ballot) string {\n\tvPowers, totalPower := c.computeVotingPowers()\n\trolePowers := []string{}\n\tfor _, role := range c.roles {\n\t\tweight := strconv.FormatFloat(vPowers[role], 'f', 2, 64) // ufmt.Sprintf(\"%.2f\", ...) is not working\n\t\trolePowers = append(rolePowers, ufmt.Sprintf(\"%s =\u003e %s power\", role, weight))\n\t}\n\ts := \"\"\n\ts += ufmt.Sprintf(\"%g%% of total voting power | %s\\n\\n\", c.threshold*100, strings.Join(rolePowers, \" | \"))\n\ts += ufmt.Sprintf(\"Threshold needed: %g%% of total voting power\\n\\n\", c.threshold*100)\n\ts += ufmt.Sprintf(\"Yes: %d/%d\\n\\n\", c.yesRatio(ballot), totalPower)\n\ts += ufmt.Sprintf(\"Voting power needed: %g%% of total voting power\\n\\n\", c.threshold*totalPower)\n\treturn s\n}\n\nvar _ Condition = (*gnolovDaoCondThreshold)(nil)\n\nfunc (c *gnolovDaoCondThreshold) yesRatio(ballot Ballot) float64 {\n\tvar totalYes float64\n\tvotingPowersByTier, totalPower := c.computeVotingPowers()\n\t// Case when there are zero T1s\n\tif totalPower == 0.0 {\n\t\treturn totalPower\n\t}\n\n\tballot.Iterate(func(voter string, vote Vote) bool {\n\t\tif vote != VoteYes {\n\t\t\treturn false\n\t\t}\n\t\ttier := c.getUserRole(voter)\n\t\ttotalYes += votingPowersByTier[tier]\n\t\treturn false\n\t})\n\treturn totalYes / totalPower\n}\n\nfunc (c *gnolovDaoCondThreshold) getUserRole(userID string) string {\n\tfor _, role := range c.roles {\n\t\tif c.hasRoleFn(userID, role) {\n\t\t\treturn role\n\t\t}\n\t}\n\tpanic(\"No role found for user\")\n}\n\nfunc (c *gnolovDaoCondThreshold) computeVotingPowers() (map[string]float64, float64) {\n\tvotingPowers := make(map[string]float64)\n\ttotalPower := 0.0\n\tcountsMembersPerRole := make(map[string]float64)\n\n\tfor _, role := range c.roles {\n\t\tcountsMembersPerRole[role] = float64(c.usersWithRoleCountFn(role))\n\t}\n\n\tfor i, role := range c.roles {\n\t\tif i == 0 {\n\t\t\tvotingPowers[role] = roleWeights[0] // Highest tier always gets max power (3.0)\n\t\t} else {\n\t\t\tvotingPowers[role] = computePower(countsMembersPerRole[c.roles[0]], countsMembersPerRole[role], roleWeights[i])\n\t\t}\n\t\ttotalPower += votingPowers[role] * countsMembersPerRole[role]\n\t}\n\n\treturn votingPowers, totalPower\n}\n\n// max power here is the number of votes each tier gets when we have\n// the same number of member on each tier\n// T2 = 2.0 and T1 = 1.0 with the ration T1/Tn\n// we compute the actual ratio\nfunc computePower(T1, Tn, maxPower float64) float64 {\n\t// If there are 0 Tn (T2, T3) just return the max power\n\t// we could also return 0.0 as voting power\n\tif Tn \u003c= 0.0 {\n\t\treturn maxPower\n\t}\n\n\tcomputedPower := (T1 / Tn) * maxPower\n\tif computedPower \u003e= maxPower {\n\t\t// If computed power is bigger than the max, this happens if Tn is lower than T1\n\t\t// cap the max power to max power.\n\t\treturn maxPower\n\t}\n\treturn computedPower\n}\n"},{"name":"cond_gnolovedao_test.gno","body":"package daocond\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\n/*\nExample 1:\nT1 100 members --\u003e 300 VP, 3 votes per member\nT2 100 members --\u003e 200 VP, 2 votes per member\nT3 100 members --\u003e 100 VP, 1 votes per member\nExample 2:\n\nT1 100 members --\u003e 300 VP, 3 votes per member\nT2 50 members --\u003e 100 VP, 2 votes per member *\nT3 10 members --\u003e 10 VP, 1 votes per member *\nExample 3:\n\nT1 100 members --\u003e 300 VP, 3 votes per member\nT2 200 members --\u003e 200 VP, 1 votes per member *\nT3 100 members --\u003e 100 VP, 1 votes per member\nExample 4:\n\nT1 100 members --\u003e 300 VP, 3 votes per member\nT2 200 members --\u003e 200 VP, 1 votes per member *\nT3 1000 members --\u003e 100 VP, 0.1 votes per member\n*/\nfunc TestComputeVotingPowers(t *testing.T) {\n\ttype gnoloveDaoComposition struct {\n\t\tt1s                int\n\t\tt2s                int\n\t\tt3s                int\n\t\tabstainT3          bool\n\t\texpectedTotalPower float64\n\t\texpectedPowers     map[string]float64\n\t}\n\ttests := map[string]gnoloveDaoComposition{\n\t\t\"example 1\": {\n\t\t\tt1s:                100,\n\t\t\tt2s:                100,\n\t\t\tt3s:                100,\n\t\t\tabstainT3:          false,\n\t\t\texpectedTotalPower: 600,\n\t\t\texpectedPowers: map[string]float64{\n\t\t\t\t\"T1\": 3.0,\n\t\t\t\t\"T2\": 2.0,\n\t\t\t\t\"T3\": 1.0,\n\t\t\t},\n\t\t},\n\t\t\"example 2\": {\n\t\t\tt1s:                100,\n\t\t\tt2s:                50,\n\t\t\tt3s:                10,\n\t\t\tabstainT3:          false,\n\t\t\texpectedTotalPower: 410,\n\t\t\texpectedPowers: map[string]float64{\n\t\t\t\t\"T1\": 3.0,\n\t\t\t\t\"T2\": 2.0,\n\t\t\t\t\"T3\": 1.0,\n\t\t\t},\n\t\t},\n\t\t\"example 3\": {\n\t\t\tt1s:                100,\n\t\t\tt2s:                200,\n\t\t\tt3s:                100,\n\t\t\tabstainT3:          false,\n\t\t\texpectedTotalPower: 600,\n\t\t\texpectedPowers: map[string]float64{\n\t\t\t\t\"T1\": 3.0,\n\t\t\t\t\"T2\": 1.0,\n\t\t\t\t\"T3\": 1.0,\n\t\t\t},\n\t\t},\n\t\t\"example 4\": {\n\t\t\tt1s:                100,\n\t\t\tt2s:                200,\n\t\t\tt3s:                1000,\n\t\t\tabstainT3:          false,\n\t\t\texpectedTotalPower: 600,\n\t\t\texpectedPowers: map[string]float64{\n\t\t\t\t\"T1\": 3.0,\n\t\t\t\t\"T2\": 1.0,\n\t\t\t\t\"T3\": 0.1,\n\t\t\t},\n\t\t},\n\t\t\"0 -T1s\": {\n\t\t\tt1s:                0,\n\t\t\tt2s:                100,\n\t\t\tt3s:                100,\n\t\t\tabstainT3:          false,\n\t\t\texpectedTotalPower: 0,\n\t\t\texpectedPowers: map[string]float64{\n\t\t\t\t\"T1\": 3.0,\n\t\t\t\t\"T2\": 0.0,\n\t\t\t\t\"T3\": 0.0,\n\t\t\t},\n\t\t},\n\t\t\"100 T1, 1 T2, 1 T3\": {\n\t\t\tt1s:                100,\n\t\t\tt2s:                1,\n\t\t\tt3s:                1,\n\t\t\tabstainT3:          false,\n\t\t\texpectedTotalPower: 303,\n\t\t\texpectedPowers: map[string]float64{\n\t\t\t\t\"T1\": 3.0,\n\t\t\t\t\"T2\": 2.0,\n\t\t\t\t\"T3\": 1.0,\n\t\t\t},\n\t\t},\n\t\t\"T3 Abstaining\": {\n\t\t\tt1s:                100,\n\t\t\tt2s:                100,\n\t\t\tt3s:                100,\n\t\t\texpectedTotalPower: 500,\n\t\t\tabstainT3:          true,\n\t\t\texpectedPowers: map[string]float64{\n\t\t\t\t\"T1\": 3.0,\n\t\t\t\t\"T2\": 2.0,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor name, composition := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tdao := newMockDAO()\n\t\t\tfor i := 0; i \u003c composition.t1s; i++ {\n\t\t\t\tdao.addUser(ufmt.Sprintf(\"%d_T1\", i), []string{\"T1\"})\n\t\t\t}\n\t\t\tfor i := 0; i \u003c composition.t2s; i++ {\n\t\t\t\tdao.addUser(ufmt.Sprintf(\"%d_T2\", i), []string{\"T2\"})\n\t\t\t}\n\t\t\tfor i := 0; i \u003c composition.t3s; i++ {\n\t\t\t\tdao.addUser(ufmt.Sprintf(\"%d_T3\", i), []string{\"T3\"})\n\t\t\t}\n\n\t\t\troles := []string{\"T1\", \"T2\"}\n\t\t\tif !composition.abstainT3 {\n\t\t\t\troles = append(roles, \"T3\")\n\t\t\t}\n\n\t\t\tcond := \u0026gnolovDaoCondThreshold{\n\t\t\t\tthreshold:            0.6,\n\t\t\t\thasRoleFn:            dao.hasRole,\n\t\t\t\troles:                roles,\n\t\t\t\tusersWithRoleCountFn: dao.usersWithRoleCount,\n\t\t\t}\n\t\t\tvotingPowers, totalPower := cond.computeVotingPowers()\n\t\t\tfor tier, expectedPower := range composition.expectedPowers {\n\t\t\t\tif votingPowers[tier] != expectedPower {\n\t\t\t\t\tt.Fail()\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif totalPower != composition.expectedTotalPower {\n\t\t\t\tt.Fail()\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEval(t *testing.T) {\n\ttype gnoloveDaoVotes struct {\n\t\tvotesT1      []Vote\n\t\tvotesT2      []Vote\n\t\tvotesT3      []Vote\n\t\tthreshold    float64\n\t\texpectedEval bool\n\t\texpectedYes  float64\n\t\tabstainT3    bool\n\t\tpanic        bool\n\t}\n\ttests := map[string]gnoloveDaoVotes{\n\t\t\"2/6% Yes\": { //0.3333\n\t\t\tvotesT1:      []Vote{VoteNo},                             // 3 voting power\n\t\t\tvotesT2:      []Vote{VoteYes, VoteYes, VoteYes, VoteYes}, // 2 voting power combined\n\t\t\tvotesT3:      []Vote{VoteNo},\n\t\t\texpectedEval: false,\n\t\t\tabstainT3:    false,\n\t\t\tthreshold:    0.45,\n\t\t\texpectedYes:  2.0 / 6.0,\n\t\t\tpanic:        false,\n\t\t},\n\t\t\"50% Yes\": {\n\t\t\tvotesT1:      []Vote{VoteNo},                             // 3 voting power\n\t\t\tvotesT2:      []Vote{VoteYes, VoteYes, VoteYes, VoteYes}, // 2 voting power combined\n\t\t\tvotesT3:      []Vote{VoteYes},\n\t\t\texpectedEval: true,\n\t\t\tabstainT3:    false,\n\t\t\tthreshold:    0.45,\n\t\t\texpectedYes:  3.0 / 6.0,\n\t\t\tpanic:        false,\n\t\t},\n\t\t\"several T2 \u0026 T3\": {\n\t\t\tvotesT1: []Vote{VoteNo, VoteNo}, // 6 voting power\n\t\t\t// 10 T2 total power (2/3) powerT1 = 4, 4/10 0.4 each\n\t\t\tvotesT2: []Vote{VoteYes, VoteYes, VoteYes, VoteYes, VoteYes, VoteYes, VoteYes, VoteYes, VoteYes, VoteYes},\n\t\t\t// 4 T3 total power (1/3) powerT1 = 2, 2/4 0.5 each\n\t\t\tvotesT3:      []Vote{VoteYes, VoteNo, VoteYes, VoteNo},\n\t\t\texpectedEval: false,\n\t\t\tabstainT3:    false,\n\t\t\tthreshold:    0.42, //total power = 6+4+2 T3yes = 1, T2yes = 4 T1yes = 0 totalYes = 0.41666666666 (5/12)\n\t\t\texpectedYes:  5.0 / 12.0,\n\t\t\tpanic:        false,\n\t\t},\n\t\t\"several T2 \u0026 T3 eval true\": {\n\t\t\tvotesT1: []Vote{VoteNo, VoteNo}, // 6 voting power\n\t\t\t// 10 T2 total power (2/3) powerT1 = 4, 4/10 0.4 each\n\t\t\tvotesT2: []Vote{VoteYes, VoteYes, VoteYes, VoteYes, VoteYes, VoteYes, VoteYes, VoteYes, VoteYes, VoteYes},\n\t\t\t// 4 T3 total power (1/3) powerT1 = 2, 2/4 0.5 each\n\t\t\tvotesT3:      []Vote{VoteYes, VoteYes, VoteYes, VoteNo},\n\t\t\texpectedEval: true,\n\t\t\tabstainT3:    false,\n\t\t\tthreshold:    0.42, //total power = 6+4+2 T3yes = 1.5, T2yes = 4 T1yes = 0 totalYes = 0.45833333333 (5.5/12)\n\t\t\texpectedYes:  5.5 / 12.0,\n\t\t\tpanic:        false,\n\t\t},\n\t\t\"only T1s\": {\n\t\t\tvotesT1:      []Vote{VoteYes, VoteNo}, // 6 voting power\n\t\t\texpectedEval: false,\n\t\t\tabstainT3:    false,\n\t\t\tthreshold:    0.6,\n\t\t\texpectedYes:  3.0 / 6.0,\n\t\t\tpanic:        false,\n\t\t},\n\t\t\"only T3s\": { // as T2 \u0026 T3 power is capped as power of T1, in this case the power will be 0 everywhere\n\t\t\tvotesT3:      []Vote{VoteYes, VoteYes, VoteYes, VoteYes}, // voting power = 0, 0 each\n\t\t\texpectedEval: false,\n\t\t\tabstainT3:    false,\n\t\t\tthreshold:    0.6,\n\t\t\texpectedYes:  0.0,\n\t\t},\n\t\t\"T3 abstaining\": {\n\t\t\tvotesT1:      []Vote{VoteYes, VoteNo}, // 6 voting power\n\t\t\tvotesT2:      []Vote{VoteYes, VoteYes},\n\t\t\tvotesT3:      []Vote{},\n\t\t\texpectedEval: true,\n\t\t\tabstainT3:    true,\n\t\t\tthreshold:    0.6,\n\t\t\texpectedYes:  7.0 / 10.0,\n\t\t\tpanic:        false,\n\t\t},\n\t\t\"T3 abstaining w/ votes\": {\n\t\t\tvotesT1:      []Vote{VoteYes, VoteNo}, // 6 voting power\n\t\t\tvotesT2:      []Vote{VoteYes, VoteYes},\n\t\t\tvotesT3:      []Vote{VoteYes, VoteNo},\n\t\t\texpectedEval: true,\n\t\t\tabstainT3:    true,\n\t\t\tthreshold:    0.6,\n\t\t\texpectedYes:  7.0 / 10.0,\n\t\t\tpanic:        true, // a user with T3 when T3 is abstaining should panic\n\t\t},\n\t}\n\n\tfor name, tdata := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tif tdata.panic {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r == nil {\n\t\t\t\t\t\tt.Errorf(\"The code did not panic\")\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t}\n\t\t\tvotes := map[string]Vote{}\n\t\t\tdao := newMockDAO()\n\t\t\tfor i, vote := range tdata.votesT1 {\n\t\t\t\tuserID := ufmt.Sprintf(\"%d_T1\", i)\n\t\t\t\tdao.addUser(userID, []string{\"T1\"})\n\t\t\t\tvotes[userID] = vote\n\t\t\t}\n\n\t\t\tfor i, vote := range tdata.votesT2 {\n\t\t\t\tuserID := ufmt.Sprintf(\"%d_T2\", i)\n\t\t\t\tdao.addUser(userID, []string{\"T2\"})\n\t\t\t\tvotes[userID] = vote\n\t\t\t}\n\n\t\t\tfor i, vote := range tdata.votesT3 {\n\t\t\t\tuserID := ufmt.Sprintf(\"%d_T3\", i)\n\t\t\t\tdao.addUser(userID, []string{\"T3\"})\n\t\t\t\tvotes[userID] = vote\n\t\t\t}\n\t\t\tballot := NewBallot()\n\t\t\tfor userId, vote := range votes {\n\t\t\t\tballot.Vote(userId, vote)\n\t\t\t}\n\t\t\troles := []string{\"T1\", \"T2\"}\n\t\t\tif !tdata.abstainT3 {\n\t\t\t\troles = append(roles, \"T3\")\n\t\t\t}\n\t\t\tcond := GnoloveDAOCondThreshold(tdata.threshold, roles, dao.hasRole, dao.usersWithRoleCount)\n\t\t\t// Get percent of total yes\n\t\t\turequire.Equal(t, tdata.expectedEval, cond.Eval(ballot))\n\t\t})\n\t}\n}\n\ntype mockDAO struct {\n\tmembers map[string][]string\n\troles   map[string][]string\n}\n\nfunc newMockDAO() *mockDAO {\n\treturn \u0026mockDAO{\n\t\tmembers: map[string][]string{},\n\t\troles:   map[string][]string{}, // roles to users\n\t}\n}\n\nfunc (m *mockDAO) addUser(memberId string, roles []string) {\n\tm.members[memberId] = roles\n\tfor _, memberRole := range roles {\n\t\tm.roles[memberRole] = append(m.roles[memberRole], memberId)\n\t}\n}\nfunc (m *mockDAO) usersWithRoleCount(role string) uint32 {\n\treturn uint32(len(m.roles[role]))\n}\n\nfunc (m *mockDAO) hasRole(memberId string, role string) bool {\n\troles, ok := m.members[memberId]\n\tif !ok {\n\t\treturn false\n\t}\n\tfor _, memberRole := range roles {\n\t\tif memberRole == role {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"},{"name":"cond_members_threshold.gno","body":"package daocond\n\nimport (\n\t\"errors\"\n\t\"math\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\ntype membersThresholdCond struct {\n\tisMemberFn     func(memberId string) bool\n\tmembersCountFn func() uint64\n\tthreshold      float64\n}\n\n// SECURITY XXX: MembersThreshold captures two predicate functions\n// (isMemberFn, membersCountFn) at construction. Whoever constructs the\n// condition controls what counts as \"is this voter a member\" and \"how\n// many members exist\" — both inputs to the threshold check. Hostile\n// predicates can pass any vote regardless of actual membership, or\n// reject all votes (DoS).\n// In practice these are wired up by basedao/commondao with their own\n// member store, so the DAO author IS the trust boundary. Don't\n// construct a MembersThreshold from external-realm-supplied predicates.\nfunc MembersThreshold(threshold float64, isMemberFn func(memberId string) bool, membersCountFn func() uint64) Condition {\n\tif threshold \u003c= 0 || threshold \u003e 1 {\n\t\tpanic(errors.New(\"invalid threshold\"))\n\t}\n\tif isMemberFn == nil {\n\t\tpanic(errors.New(\"nil isMemberFn\"))\n\t}\n\tif membersCountFn == nil {\n\t\tpanic(errors.New(\"nil membersCountFn\"))\n\t}\n\treturn \u0026membersThresholdCond{\n\t\tthreshold:      threshold,\n\t\tisMemberFn:     isMemberFn,\n\t\tmembersCountFn: membersCountFn,\n\t}\n}\n\n// Eval implements Condition.\nfunc (c *membersThresholdCond) Eval(ballot Ballot) bool {\n\treturn c.yesRatio(ballot) \u003e= c.threshold\n}\n\n// Signal implements Condition.\nfunc (c *membersThresholdCond) Signal(ballot Ballot) float64 {\n\treturn math.Min(c.yesRatio(ballot)/c.threshold, 1)\n}\n\n// Render implements Condition.\nfunc (c *membersThresholdCond) Render() string {\n\treturn ufmt.Sprintf(\"%g%% of members\", c.threshold*100)\n}\n\n// RenderWithVotes implements Condition.\nfunc (c *membersThresholdCond) RenderWithVotes(ballot Ballot) string {\n\ts := \"\"\n\ts += ufmt.Sprintf(\"to meet the condition, %g%% of members must vote yes\\n\\n\", c.threshold*100)\n\ts += ufmt.Sprintf(\"Yes: %d/%d = %g%%\\n\\n\", c.totalYes(ballot), c.membersCountFn(), c.yesRatio(ballot)*100)\n\treturn s\n}\n\nvar _ Condition = (*membersThresholdCond)(nil)\n\nfunc (c *membersThresholdCond) yesRatio(ballot Ballot) float64 {\n\treturn float64(c.totalYes(ballot)) / float64(c.membersCountFn())\n}\n\nfunc (c *membersThresholdCond) totalYes(ballot Ballot) uint64 {\n\ttotalYes := uint64(0)\n\tballot.Iterate(func(voter string, vote Vote) bool {\n\t\tif vote == VoteYes \u0026\u0026 c.isMemberFn(voter) {\n\t\t\ttotalYes += 1\n\t\t}\n\t\treturn false\n\t})\n\treturn totalYes\n}\n"},{"name":"cond_or.gno","body":"package daocond\n\nimport (\n\t\"math\"\n\t\"strings\"\n)\n\ntype orCond struct {\n\tconditions []Condition\n}\n\nfunc Or(conditions ...Condition) Condition {\n\tif len(conditions) \u003c 2 {\n\t\tpanic(\"at least two conditions are required\")\n\t}\n\treturn \u0026orCond{conditions: conditions}\n}\n\n// Eval implements Condition.\nfunc (c *orCond) Eval(ballot Ballot) bool {\n\tfor _, condition := range c.conditions {\n\t\tif condition.Eval(ballot) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// Signal implements Condition.\nfunc (c *orCond) Signal(ballot Ballot) float64 {\n\tmaxSignal := 0.0\n\tfor _, condition := range c.conditions {\n\t\tmaxSignal = math.Max(maxSignal, condition.Signal(ballot))\n\t}\n\treturn maxSignal\n}\n\n// Render implements Condition.\nfunc (c *orCond) Render() string {\n\trenders := []string{}\n\tfor _, condition := range c.conditions {\n\t\trenders = append(renders, condition.Render())\n\t}\n\treturn \"[\" + strings.Join(renders, \" OR \") + \"]\"\n}\n\n// RenderWithVotes implements Condition.\nfunc (c *orCond) RenderWithVotes(ballot Ballot) string {\n\trenders := []string{}\n\tfor _, condition := range c.conditions {\n\t\trenders = append(renders, condition.RenderWithVotes(ballot))\n\t}\n\treturn \"[\" + strings.Join(renders, \" OR \") + \"]\"\n}\n\nvar _ Condition = (*orCond)(nil)\n"},{"name":"cond_role_count.gno","body":"package daocond\n\nimport (\n\t\"errors\"\n\t\"math\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\ntype roleCountCond struct {\n\thasRoleFn func(memberId string, role string) bool\n\tcount     uint64\n\trole      string\n}\n\nfunc RoleCount(count uint64, role string, hasRoleFn func(memberId string, role string) bool) Condition {\n\tif count == 0 {\n\t\tpanic(errors.New(\"count must be greater than 0\"))\n\t}\n\tif role == \"\" {\n\t\tpanic(errors.New(\"role must not be empty\"))\n\t}\n\tif hasRoleFn == nil {\n\t\tpanic(errors.New(\"nil hasRoleFn\"))\n\t}\n\treturn \u0026roleCountCond{\n\t\tcount:     count,\n\t\thasRoleFn: hasRoleFn,\n\t\trole:      role,\n\t}\n}\n\n// Eval implements Condition.\nfunc (c *roleCountCond) Eval(ballot Ballot) bool {\n\treturn c.totalYes(ballot) \u003e= c.count\n}\n\n// Signal implements Condition.\nfunc (c *roleCountCond) Signal(ballot Ballot) float64 {\n\treturn math.Min(float64(c.totalYes(ballot))/float64(c.count), 1)\n}\n\n// Render implements Condition.\nfunc (c *roleCountCond) Render() string {\n\treturn ufmt.Sprintf(\"%d %s\", c.count, c.role)\n}\n\n// RenderWithVotes implements Condition.\nfunc (c *roleCountCond) RenderWithVotes(ballot Ballot) string {\n\ts := \"\"\n\ts += ufmt.Sprintf(\"to meet the condition, %d members with role %s must vote yes\\n\\n\", c.count, c.role)\n\ts += ufmt.Sprintf(\"Yes: %d/%d\\n\\n\", c.totalYes(ballot), c.count)\n\treturn s\n}\n\nvar _ Condition = (*roleCountCond)(nil)\n\nfunc (c *roleCountCond) totalYes(ballot Ballot) uint64 {\n\ttotalYes := uint64(0)\n\tballot.Iterate(func(voter string, vote Vote) bool {\n\t\tif vote == VoteYes \u0026\u0026 c.hasRoleFn(voter, c.role) {\n\t\t\ttotalYes += 1\n\t\t}\n\t\treturn false\n\t})\n\treturn totalYes\n}\n"},{"name":"cond_role_treshold.gno","body":"package daocond\n\nimport (\n\t\"errors\"\n\t\"math\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\ntype roleThresholdCond struct {\n\thasRoleFn        func(memberId string, role string) bool\n\tusersRoleCountFn func(role string) uint32\n\tthreshold        float64\n\trole             string\n}\n\nfunc RoleThreshold(threshold float64, role string, hasRoleFn func(memberId string, role string) bool, usersRoleCountFn func(role string) uint32) Condition {\n\tif threshold \u003c= 0 || threshold \u003e 1 {\n\t\tpanic(errors.New(\"invalid threshold\"))\n\t}\n\tif hasRoleFn == nil {\n\t\tpanic(errors.New(\"nil hasRoleFn\"))\n\t}\n\tif usersRoleCountFn == nil {\n\t\tpanic(errors.New(\"nil usersRoleCountFn\"))\n\t}\n\treturn \u0026roleThresholdCond{\n\t\tthreshold:        threshold,\n\t\thasRoleFn:        hasRoleFn,\n\t\tusersRoleCountFn: usersRoleCountFn,\n\t\trole:             role,\n\t}\n}\n\n// Eval implements Condition.\nfunc (c *roleThresholdCond) Eval(ballot Ballot) bool {\n\treturn c.yesRatio(ballot) \u003e= c.threshold\n}\n\n// Signal implements Condition.\nfunc (c *roleThresholdCond) Signal(ballot Ballot) float64 {\n\treturn math.Min(c.yesRatio(ballot)/c.threshold, 1)\n}\n\n// Render implements Condition.\nfunc (c *roleThresholdCond) Render() string {\n\treturn ufmt.Sprintf(\"%g%% of %s members\", c.threshold*100, c.role)\n}\n\n// RenderWithVotes implements Condition.\nfunc (c *roleThresholdCond) RenderWithVotes(ballot Ballot) string {\n\ts := \"\"\n\ts += ufmt.Sprintf(\"To meet the condition, %g%% of %s members with role %s must vote yes\\n\\n\", c.threshold*100, c.role)\n\ts += ufmt.Sprintf(\"Yes: %d/%d = %g%%\\n\\n\", c.totalYes(ballot), c.usersRoleCountFn(c.role), c.yesRatio(ballot)*100)\n\treturn s\n}\n\nvar _ Condition = (*roleThresholdCond)(nil)\n\nfunc (c *roleThresholdCond) yesRatio(ballot Ballot) float64 {\n\treturn float64(c.totalYes(ballot)) / float64(c.usersRoleCountFn(c.role))\n}\n\nfunc (c *roleThresholdCond) totalYes(ballot Ballot) uint32 {\n\ttotalYes := uint32(0)\n\tballot.Iterate(func(voter string, vote Vote) bool {\n\t\tif vote == VoteYes \u0026\u0026 c.hasRoleFn(voter, c.role) {\n\t\t\ttotalYes += 1\n\t\t}\n\t\treturn false\n\t})\n\treturn totalYes\n}\n"},{"name":"daocond.gno","body":"package daocond\n\n// We made conditions stateless to avoid complexity of handling events like (member added, member removed, role assigned, role removed, etc.)\n// This model should work pretty good for small DAOs with few voters.\n// In future, we should probably add a stateful model for large DAOs with many voters.\n\ntype Condition interface {\n\tEval(ballot Ballot) bool\n\tSignal(ballot Ballot) float64\n\n\tRender() string\n\tRenderWithVotes(ballot Ballot) string\n}\n\ntype Ballot interface {\n\tVote(voter string, vote Vote)\n\tGet(voter string) Vote\n\tTotal() int\n\n\tIterate(fn func(voter string, vote Vote) bool)\n}\n\ntype Vote string\n\nconst (\n\tVoteYes     = \"yes\"\n\tVoteNo      = \"no\"\n\tVoteAbstain = \"abstain\"\n)\n"},{"name":"daocond_test.gno","body":"package daocond_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/urequire/v0\"\n\t\"gno.land/p/samcrew/daocond\"\n)\n\nfunc TestCondition(t *testing.T) {\n\tdao := newMockDAO()\n\n\t// leaf conditions\n\tmembersMajority := daocond.MembersThreshold(0.6, dao.isMember, dao.membersCount)\n\tpublicRelationships := daocond.RoleCount(1, \"public-relationships\", dao.hasRole)\n\tfinanceOfficer := daocond.RoleCount(1, \"finance-officer\", dao.hasRole)\n\n\turequire.Equal(t, \"60% of members\", membersMajority.Render())\n\turequire.Equal(t, \"1 public-relationships\", publicRelationships.Render())\n\turequire.Equal(t, \"1 finance-officer\", financeOfficer.Render())\n\n\t// ressource expressions\n\tressources := map[string]daocond.Condition{\n\t\t\"social.post\":    daocond.And(publicRelationships, membersMajority),\n\t\t\"finance.invest\": daocond.Or(financeOfficer, membersMajority),\n\t}\n\n\turequire.Equal(t, \"[1 public-relationships AND 60% of members]\", ressources[\"social.post\"].Render())\n\turequire.Equal(t, \"[1 finance-officer OR 60% of members]\", ressources[\"finance.invest\"].Render())\n}\n\nfunc TestEval(t *testing.T) {\n\tsetups := []struct {\n\t\tname  string\n\t\tsetup func(dao *mockDAO)\n\t}{\n\t\t{name: \"basic\", setup: func(dao *mockDAO) {\n\t\t\tmembersMajority := daocond.MembersThreshold(0.6, dao.isMember, dao.membersCount)\n\t\t\tpublicRelationships := daocond.RoleCount(1, \"public-relationships\", dao.hasRole)\n\t\t\tfinanceOfficer := daocond.RoleCount(1, \"finance-officer\", dao.hasRole)\n\t\t\tdao.resources = map[string]daocond.Condition{\n\t\t\t\t\"social.post\":    daocond.And(publicRelationships, membersMajority),\n\t\t\t\t\"finance.invest\": daocond.Or(financeOfficer, membersMajority),\n\t\t\t}\n\t\t}},\n\t}\n\n\tcases := []struct {\n\t\tname     string\n\t\tresource string\n\t\tphases   []testPhase\n\t}{\n\t\t{\n\t\t\tname:     \"post with public-relationships\",\n\t\t\tresource: \"social.post\",\n\t\t\tphases: []testPhase{{\n\t\t\t\tvotes: map[string]daocond.Vote{\n\t\t\t\t\t\"alice\": \"yes\",\n\t\t\t\t\t\"bob\":   \"yes\",\n\t\t\t\t\t\"eve\":   \"no\",\n\t\t\t\t},\n\t\t\t\tresult: true,\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tname:     \"post without public-relationships\",\n\t\t\tresource: \"social.post\",\n\t\t\tphases: []testPhase{{\n\t\t\t\tvotes: map[string]daocond.Vote{\n\t\t\t\t\t\"alice\": \"yes\",\n\t\t\t\t\t\"bob\":   \"no\",\n\t\t\t\t\t\"eve\":   \"yes\",\n\t\t\t\t},\n\t\t\t\tresult: false,\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tname:     \"post after public-relationships changes\",\n\t\t\tresource: \"social.post\",\n\t\t\tphases: []testPhase{\n\t\t\t\t{\n\t\t\t\t\tvotes: map[string]daocond.Vote{\n\t\t\t\t\t\t\"alice\": \"yes\",\n\t\t\t\t\t\t\"bob\":   \"yes\",\n\t\t\t\t\t\t\"eve\":   \"no\",\n\t\t\t\t\t},\n\t\t\t\t\tresult: true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tchanges: func(dao *mockDAO) {\n\t\t\t\t\t\tdao.unassignRole(\"bob\", \"public-relationships\")\n\t\t\t\t\t},\n\t\t\t\t\tresult: false,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tchanges: func(dao *mockDAO) {\n\t\t\t\t\t\tdao.assignRole(\"alice\", \"public-relationships\")\n\t\t\t\t\t},\n\t\t\t\t\tresult: true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"post public-relationships alone\",\n\t\t\tresource: \"social.post\",\n\t\t\tphases: []testPhase{{\n\t\t\t\tvotes: map[string]daocond.Vote{\n\t\t\t\t\t\"alice\": \"no\",\n\t\t\t\t\t\"bob\":   \"yes\",\n\t\t\t\t\t\"eve\":   \"no\",\n\t\t\t\t},\n\t\t\t\tresult: false,\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tname:     \"invest with finance officer\",\n\t\t\tresource: \"finance.invest\",\n\t\t\tphases: []testPhase{{\n\t\t\t\tvotes: map[string]daocond.Vote{\n\t\t\t\t\t\"alice\": \"yes\",\n\t\t\t\t\t\"bob\":   \"no\",\n\t\t\t\t\t\"eve\":   \"no\",\n\t\t\t\t},\n\t\t\t\tresult: true,\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tname:     \"invest without finance officer\",\n\t\t\tresource: \"finance.invest\",\n\t\t\tphases: []testPhase{{\n\t\t\t\tvotes: map[string]daocond.Vote{\n\t\t\t\t\t\"alice\": \"no\",\n\t\t\t\t\t\"bob\":   \"yes\",\n\t\t\t\t\t\"eve\":   \"yes\",\n\t\t\t\t},\n\t\t\t\tresult: true,\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tname:     \"invest alone\",\n\t\t\tresource: \"finance.invest\",\n\t\t\tphases: []testPhase{{\n\t\t\t\tvotes: map[string]daocond.Vote{\n\t\t\t\t\t\"alice\": \"no\",\n\t\t\t\t\t\"bob\":   \"no\",\n\t\t\t\t\t\"eve\":   \"yes\",\n\t\t\t\t},\n\t\t\t\tresult: false,\n\t\t\t}},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tfor _, s := range setups {\n\t\t\tt.Run(tc.name+\" \"+s.name, func(t *testing.T) {\n\t\t\t\tdao := newMockDAO()\n\t\t\t\ts.setup(dao)\n\n\t\t\t\tresource, ok := dao.resources[tc.resource]\n\t\t\t\turequire.True(t, ok)\n\n\t\t\t\tballot := daocond.NewBallot()\n\t\t\t\tfor _, phase := range tc.phases {\n\t\t\t\t\tif phase.changes != nil {\n\t\t\t\t\t\tphase.changes(dao)\n\t\t\t\t\t}\n\t\t\t\t\tif phase.votes != nil {\n\t\t\t\t\t\tfor memberId, vote := range phase.votes {\n\t\t\t\t\t\t\tballot.Vote(memberId, vote)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tresult := resource.Eval(ballot)\n\t\t\t\t\tif phase.result != result {\n\t\t\t\t\t\tprintln(\"State:\", resource.RenderWithVotes(ballot))\n\t\t\t\t\t}\n\t\t\t\t\turequire.Equal(t, phase.result, result)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n\ntype testPhase struct {\n\tchanges func(dao *mockDAO)\n\tvotes   map[string]daocond.Vote\n\tresult  bool\n}\n\ntype mockDAO struct {\n\tmembers   map[string][]string\n\troles     map[string][]string\n\tresources map[string]daocond.Condition\n}\n\nfunc newMockDAO() *mockDAO {\n\treturn \u0026mockDAO{\n\t\tmembers: map[string][]string{\n\t\t\t\"alice\": []string{\"finance-officer\"},\n\t\t\t\"bob\":   []string{\"public-relationships\"},\n\t\t\t\"eve\":   []string{},\n\t\t},\n\t\troles: map[string][]string{\n\t\t\t\"finance-officer\":      []string{\"alice\"},\n\t\t\t\"public-relationships\": []string{\"bob\"},\n\t\t}, // roles to users\n\t\tresources: make(map[string]daocond.Condition),\n\t}\n}\n\nfunc (m *mockDAO) assignRole(userId string, role string) {\n\troles, ok := m.members[userId]\n\tif !ok {\n\t\tpanic(errors.New(\"unknown member\"))\n\t}\n\tm.members[userId], ok = strsadd(roles, role)\n}\n\nfunc (m *mockDAO) unassignRole(userId string, role string) {\n\troles, ok := m.members[userId]\n\tif !ok {\n\t\tpanic(errors.New(\"unknown member\"))\n\t}\n\tm.members[userId], ok = strsrm(roles, role)\n}\n\nfunc (m *mockDAO) isMember(memberId string) bool {\n\t_, ok := m.members[memberId]\n\treturn ok\n}\n\nfunc (m *mockDAO) membersCount() uint64 {\n\treturn uint64(len(m.members))\n}\n\nfunc (m *mockDAO) hasRole(memberId string, role string) bool {\n\troles, ok := m.members[memberId]\n\tif !ok {\n\t\treturn false\n\t}\n\tfor _, memberRole := range roles {\n\t\tif memberRole == role {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc strsrm(strs []string, val string) ([]string, bool) {\n\tremoved := false\n\tres := []string{}\n\tfor _, str := range strs {\n\t\tif str == val {\n\t\t\tremoved = true\n\t\t\tcontinue\n\t\t}\n\t\tres = append(res, str)\n\t}\n\treturn res, removed\n}\n\nfunc strsadd(strs []string, val string) ([]string, bool) {\n\tfor _, str := range strs {\n\t\tif str == val {\n\t\t\treturn strs, false\n\t\t}\n\t}\n\treturn append(strs, val), true\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/samcrew/daocond\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1kfd9f5zlvcvy6aammcmqswa7cyjpu2nyt9qfen\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"7GyACOIPsXTzzCu+ujJcYvbkBM9TZDIT7tSG2GY6q/IzETyxe5+G2WYIHs00iOsZbU6PQ8SrmpMwZaj8WnUZyA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1kfd9f5zlvcvy6aammcmqswa7cyjpu2nyt9qfen","package":{"name":"daokit","path":"gno.land/p/samcrew/daokit","files":[{"name":"README.md","body":"# daokit\n\n# 1. Introduction\n\nA **Decentralized Autonomous Organization (DAO)** is a self-governing entity that operates through smart contracts, enabling transparent decision-making without centralized control.\n\n`daokit` is a gnolang package for creating complex DAO models. It introduces a new framework based on conditions, composed of :\n- `daokit` : Core package for building DAOs, proposals, and actions\n- `basedao` : Extension with membership and role management\n- `daocond`: Stateless condition engine for evaluating proposals\n\n# 2. What is `daokit` ?\n\n`daokit` provides a powerful condition and role-based system to build flexible and programmable DAOs.\n\n## Key Features:\n- Create proposals that include complex execution logic\n- Attach rules (conditions) to each resource\n- Assign roles to users to structure permissions and governance\n\n## 2.1 Key Concepts\n\n- **Proposal**: A request to execute a **resource**. Proposals are voted on and executed only if predefined **conditions** are met.\n- **Resource**: An executable action within the DAO. Each resource is governed by a **condition**.\n- **Condition**: A set of rules that determine whether a proposal can be executed.\n- **Role**: Labels that assign governance power or permissions to DAO members.\n\n**Example Use Case**: A DAO wants to create a proposal to spend money from its treasury.\n\n**Rules**:\n- `SpendMoney` is a resource with a condition requiring:\n\t- 50% approval from the administration board\n\t- Approval from the CFO\n\n**Outcome**:\n- Any user can propose to spend money\n- Only board and CFO votes are considered\n- The proposal executes only if the condition is satisfied\n\n# 3. Architecture\n\nDAOkit framework is composed of three packages:\n\n## 3.1 [daocond](/p/samcrew/daocond)\n\n`daocond` provides a stateless condition engine used to evaluate if a proposal should be executed.\n\n### 3.1.1 Interface\n```go\ntype Condition interface {\n\t// Eval checks if the condition is satisfied based on current votes.\n\tEval(ballot Ballot) bool\n\t// Signal returns a value from 0.0 to 1.0 to indicate how close the condition is to being met.\n\tSignal(ballot Ballot) float64\n\n\t// Render returns a static human-readable representation of the condition.\n\tRender() string\n\t// RenderWithVotes returns a dynamic representation with vote context included.\n\tRenderWithVotes(ballot Ballot) string\n}\n\ntype Ballot interface {\n\t// Vote allows a user to vote on a proposal.\n\tVote(voter string, vote Vote)\n\t// Get returns the vote of a user.\n\tGet(voter string) Vote\n\t// Total returns the total number of votes.\n\tTotal() int\n\t// Iterate iterates over all votes, similar as avl.Tree.Iterate.\n\tIterate(fn func(voter string, vote Vote) bool)\n}\n```\n\n### 3.1.2 Built-in Conditions\n`daocond` provides several built-in conditions to cover common governance scenarios.\n\n```go\n// MembersThreshold requires that a specified fraction of all DAO members approve the proposal.\nfunc MembersThreshold(threshold float64, isMemberFn func(memberId string) bool, membersCountFn func() uint64) Condition\n\n// RoleThreshold requires that a certain percentage of members holding a specific role approve.\nfunc RoleThreshold(threshold float64, role string, hasRoleFn func(memberId string, role string) bool, usersRoleCountFn func(role string) uint32) Condition\n\n// RoleCount requires a fixed minimum number of members holding a specific role to approve.\nfunc RoleCount(count uint64, role string, hasRoleFn func(memberId string, role string) bool) Condition\n```\n\n### 3.1.3 Logical Composition\nYou can combine multiple conditions to create complex governance rules using logical operators:\n\n```go\n// And returns a condition that is satisfied only if *all* provided conditions are met.\nfunc And(conditions ...Condition) Condition\n// Or returns a condition that is satisfied if *any* one of the provided conditions is met.\nfunc Or(conditions ...Condition) Condition\n```\n\n**Example**:\n```go\n// Require both admin approval and at least one CFO\ncond := daocond.And(\n    daocond.RoleThreshold(0.5, \"admin\", hasRole, roleCount),\n    daocond.RoleCount(1, \"CFO\", hasRole),\n)\n```\n\nConditions are stateless for flexibility and scalability.\n\n## 3.2 daokit\n\n`daokit` provides the core mechanics:\n\n### 3.2.1 Core Structure:\nIt's the central component of a DAO, responsible for managing both available resources that can be executed and the proposals.\n```go\ntype Core struct {\n\tResources *ResourcesStore\n\tProposals *ProposalsStore\n}\n```\n\n### 3.2.2 DAO Interface:\nThe interface defines the external functions that users or other modules interact with. It abstracts the core governance flow: proposing, voting, and executing.\n```go\ntype DAO interface {\n\tPropose(req ProposalRequest) uint64\n\tVote(id uint64, vote daocond.Vote)\n\tExecute(id uint64)\n}\n```\n\u003e 📖 [Code Example of a Basic DAO](#4-code-example-of-a-basic-dao)\n\n### 3.2.3 Proposal Lifecycle\n\nEach proposal goes through the following states:\n\n1. **Open**: \n- Initial state after proposal creation.\n- Accepts votes from eligible participants.\n\n2. **Passed**\n- Proposal has gathered enough valid votes to meet the condition.\n- Voting is **closed** and cannot be modified.\n- The proposal is now eligible for **execution**.\n\n3. **Executed**\n- Proposal action has been successfully carried out.\n- Final state — proposal can no longer be voted on or modified.\n\n\n## 3.3 [basedao](/p/samcrew/basedao)\n\n`basedao` extends `daokit` to handle members and roles management.\nIt handles who can participate in a DAO and what permissions they have.\n\n### 3.3.1 Core Types\n```go\ntype MembersStore struct {\n\tRoles   *avl.Tree \n\tMembers *avl.Tree \n}\n```\n\n### 3.3.2 Initialize the DAO\nCreate a `MembersStore` structure to initialize the DAO with predefined roles and members.\n\n```go\nroles := []basedao.RoleInfo{\n\t{Name: \"admin\", Description: \"Administrators\"},\n\t{Name: \"finance\", Description: \"Handles treasury\"},\n}\n\nmembers := []basedao.Member{\n\t{Address: \"g1abc...\", Roles: []string{\"admin\"}},\n\t{Address: \"g1xyz...\", Roles: []string{\"finance\"}},\n}\n\nstore := basedao.NewMembersStore(roles, members)\n```\n\n### 3.3.3 Example Usage\n```go\nstore := basedao.NewMembersStore(nil, nil)\n\n// Add a role and assign it\nstore.AddRole(basedao.RoleInfo{Name: \"moderator\", Description: \"Can moderate posts\"})\nstore.AddMember(\"g1alice...\", []string{\"moderator\"})\n\n// Update role assignment\nstore.AddRoleToMember(\"g1alice...\", \"editor\")\nstore.RemoveRoleFromMember(\"g1alice...\", \"moderator\")\n\n// Inspect the state\nisMember := store.IsMember(\"g1alice...\") // \"Is Alice a member?\"\nhasRole := store.HasRole(\"g1alice...\", \"editor\") // \"Is Alice an editor?\"\nmembers := store.GetMembersJSON() // \"All Members (JSON):\"\n```\n\n### 3.3.4 Creating a DAO:\n\n```go\nfunc New(conf *Config) *DAOWrapper\n```\n\n#### 3.3.4.1 Key Structures:\n- `*basedao.DAOWrapper`: The public, rlm-validating surface. Mutating\n  methods take `(_ int, rlm realm, ...)`, validate `rlm.IsCurrent()`,\n  and derive the principal from `rlm.Previous().Address()` before\n  invoking the private impl.\n- `*basedao.DAOPrivate`: Internal impl with address-typed writers.\n  Accessible from the owning realm via `wrapper.Private()` for custom\n  handler registration. **Never expose this externally** — its writer\n  methods trust the supplied caller address (Class-2 designation-forgery\n  if a foreign realm holds it).\n- `daokit.DAO`: Internal dispatch interface (address-typed). **Never\n  store as a public var or return from a public constructor** — exposes\n  designation-forgery surface. Hold `*DAOWrapper` instead.\n\nSealing via an unexported marker method would NOT defend against\nforeign impls — embedding bypasses the seal. See\n`p/test/seal/filetests/z_seal_*_filetest.gno` for the four bypass\nfiletests.\n\n\n### 3.3.5 Configuration:\n```go\ntype Config struct {\n\tName              string\n\tDescription       string\n\tImageURI          string\n\t// Use `basedao.NewMembersStore(...)` to create members and roles.\n\tMembers           *MembersStore\n\t// Set to `true` to disable built-in actions like add/remove member.\n\tNoDefaultHandlers bool\n\t// Default rule applied to all built-in DAO actions.\n\tInitialCondition  daocond.Condition\n\t// Optional helpers to store profile data (e.g., from `/r/demo/profile`).\n\tSetProfileString  ProfileStringSetter\n\tGetProfileString  ProfileStringGetter\n\t// Set to `true` if you don’t want a \"DAO Created\" event to be emitted.\n\tNoCreationEvent   bool\n}\n```\n\n# 4. Code Example of a Basic DAO\n\n```go\npackage daokit_demo\n\nimport (\n\t\"gno.land/p/samcrew/basedao\"\n\t\"gno.land/p/samcrew/daocond\"\n\t\"gno.land/p/samcrew/daokit\"\n\t\"gno.land/r/demo/profile\"\n)\n\n// Hold the concrete *DAOWrapper, NOT the daokit.DAO interface.\n// The wrapper provides rlm-validating methods; the interface is an\n// internal contract that would expose designation-forgery if returned\n// publicly.\nvar DAO *basedao.DAOWrapper\n\nfunc init() {\n\tinitialRoles := []basedao.RoleInfo{\n\t\t{Name: \"admin\", Description: \"Admin is the superuser\"},\n\t\t{Name: \"public-relationships\", Description: \"Responsible of communication with the public\"},\n\t\t{Name: \"finance-officer\", Description: \"Responsible of funds management\"},\n\t}\n\n\tinitialMembers := []basedao.Member{\n\t\t{Address: \"g126...zlg\", Roles: []string{\"admin\", \"public-relationships\"}},\n\t\t{Address: \"g1ld6...3jv\", Roles: []string{\"public-relationships\"}},\n\t\t{Address: \"g1r69...0tth\", Roles: []string{\"finance-officer\"}},\n\t\t{Address: \"g16jv...6e0r\", Roles: []string{}},\n\t}\n\n\tmemberStore := basedao.NewMembersStore(initialRoles, initialMembers)\n\n\tmembersMajority := daocond.MembersThreshold(0.6, memberStore.IsMember, memberStore.MembersCount)\n\tpublicRelationships := daocond.RoleCount(1, \"public-relationships\", memberStore.HasRole)\n\tfinanceOfficer := daocond.RoleCount(1, \"finance-officer\", memberStore.HasRole)\n\n\t// `and` and `or` use va_args so you can pass as many conditions as needed\n\tadminCond := daocond.And(membersMajority, publicRelationships, financeOfficer)\n\n\tDAO = basedao.New(\u0026basedao.Config{\n\t\tName:             \"Demo DAOKIT DAO\",\n\t\tDescription:      \"This is a demo DAO built with DAOKIT\",\n\t\tMembers:          memberStore,\n\t\tInitialCondition: adminCond,\n\t\tGetProfileString: profile.GetStringField,\n\t\tSetProfileString: profile.SetStringField,\n\t}, cur)\n}\n\nfunc Vote(cur realm, proposalID uint64, vote daocond.Vote) {\n\tDAO.Vote(0, cur, proposalID, vote)\n}\n\nfunc Execute(cur realm, proposalID uint64) {\n\tDAO.Execute(0, cur, proposalID)\n}\n\nfunc Render(path string) string {\n\treturn DAO.Render(path)\n}\n```\n\n# 5. Create Custom Resources\n\nTo add new behavior to your DAO — or to enable others to integrate your package into their own DAOs — define custom resources by implementing:\n\n```go\ntype Action interface {\n\tType() string // return the type of the action. e.g.: \"gno.land/p/samcrew/blog.NewPost\"\n\tString() string // return stringify content of the action\n}\n\ntype ActionHandler interface {\n\tType() string // return the type of the action. e.g.: \"gno.land/p/samcrew/blog\"\n\tExecute(action Action) // executes logic associated with the action\n}\n```\nThis allows DAOs to execute arbitrary logic or interact with Gno packages through governance-approved decisions.\n\n## Steps to Add a Custom Resource:\n1. Define the path of the action, it should be unique \n```go\n// XXX: pkg \"/p/samcrew/blog\" - does not exist, it's just an example\nconst ActionNewPostKind = \"gno.land/p/samcrew/blog.NewPost\"\n```\n\n2. Create the structure type of the payload\n```go\ntype ActionNewPost struct {\n\tTitle string\n\tContent string\n}\n```\n\n3. Implement the action and handler\n```go\nfunc NewPostAction(title, content string) daokit.Action {\n\t// def: daoKit.NewAction(kind: String, payload: interface{})\n\treturn daokit.NewAction(ActionNewPostKind, \u0026ActionNewPost{\n\t\tTitle:   title,\n\t\tContent: content,\n\t})\n}\n\nfunc NewPostHandler(blog *Blog) daokit.ActionHandler {\n\t// def: daoKit.NewActionHandler(kind: String, payload: func(interface{}))\n\treturn daokit.NewActionHandler(ActionNewPostKind, func(payload interface{}) {\n\t\taction, ok := payload.(*ActionNewPost)\n\t\tif !ok {\n\t\t\tpanic(errors.New(\"invalid action type\"))\n\t\t}\n\t\tblog.NewPost(action.Title, action.Content)\n\t})\n}\n```\n\n4. Register the resource\n```go\nresource := daokit.Resource{\n    Condition: daocond.NewRoleCount(1, \"CEO\", daoPrivate.Members.HasRole),\n    Handler: blog.NewPostHandler(blog),\n}\ndaoPrivate.Core.Resources.Set(\u0026resource)\n```\n"},{"name":"actions.gno","body":"package daokit\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// SECURITY XXX: Action, ActionHandler, and the daocond.Condition /\n// daocond.Ballot interfaces are open extension points. Whatever you\n// register via Core.Resources.Set runs WITH THE DAO'S AUTHORITY when\n// a proposal executes — same level of trust as code inside the DAO\n// package itself. A hostile handler can do anything the DAO can do\n// (modify members, transfer treasury funds via an action that calls\n// a token, etc.). The owning realm is responsible for only registering\n// handlers/conditions from trusted code.\n//\n// Related re-entrancy risk: Core.Execute calls Condition.Eval before\n// running Action. A malicious Condition.Eval can call back into the\n// same DAO (Propose / Vote / Execute on another proposal) during the\n// same crossing frame. IsCurrent still passes because we're inside\n// the original caller's frame, so the re-entrant call runs as the\n// original caller. Same shape as Ethereum's DAO hack.\n// Fix: add a re-entrancy guard in Core.Execute — a package-level lock\n// or per-proposal in-flight flag — set before any user-supplied code\n// (Eval, Action.String, handler Execute) runs and cleared on return.\ntype Action interface {\n\tString() string\n\tType() string\n}\n\ntype ActionHandler interface {\n\tExecute(action Action)\n\tType() string\n}\n\nfunc NewAction(kind string, payload interface{}) Action {\n\treturn \u0026genericAction{kind: kind, payload: payload}\n}\n\ntype genericAction struct {\n\tkind    string\n\tpayload interface{}\n}\n\n// String implements Action.\nfunc (g *genericAction) String() string {\n\treturn ufmt.Sprintf(\"%v\", g.payload)\n}\n\n// Type implements Action.\nfunc (g *genericAction) Type() string {\n\treturn g.kind\n}\n\nfunc NewActionHandler(kind string, executor func(interface{})) ActionHandler {\n\treturn \u0026genericActionHandler{kind: kind, executor: executor}\n}\n\ntype genericActionHandler struct {\n\tkind     string\n\texecutor func(payload interface{})\n}\n\n// Execute implements ActionHandler.\nfunc (g *genericActionHandler) Execute(iaction Action) {\n\taction, ok := iaction.(*genericAction)\n\tif !ok {\n\t\tpanic(errors.New(\"invalid action type\"))\n\t}\n\tg.executor(action.payload)\n}\n\n// Instantiate implements ActionHandler.\nfunc (g *genericActionHandler) Instantiate() Action {\n\treturn \u0026genericAction{\n\t\tkind: g.kind,\n\t}\n}\n\n// Type implements ActionHandler.\nfunc (g *genericActionHandler) Type() string {\n\treturn g.kind\n}\n\nconst ActionExecuteLambdaKind = \"gno.land/p/samcrew/daokit.ExecuteLambda\"\n\nfunc NewExecuteLambdaHandler() ActionHandler {\n\treturn NewActionHandler(ActionExecuteLambdaKind, func(i interface{}) {\n\t\tcb, ok := i.(func())\n\t\tif !ok {\n\t\t\tpanic(errors.New(\"invalid action type\"))\n\t\t}\n\t\tcb()\n\t})\n}\n\nfunc NewExecuteLambdaAction(cb func()) Action {\n\treturn NewAction(ActionExecuteLambdaKind, cb)\n}\n\n// InstantExecute-as-an-action (NewInstantExecuteHandler /\n// NewInstantExecuteAction) was removed during the rlm-based auth\n// migration: the action-handler callback shape `func(i interface{})`\n// has no rlm in scope, so it cannot drive Propose/Vote/Execute which\n// now all require an IsCurrent-validated rlm. The free function\n// InstantExecute(_ int, rlm realm, d DAO, req) remains for direct\n// caller use.\n"},{"name":"daokit.gno","body":"// Package daokit is a reference DAO toolkit (propose / vote / execute,\n// resource registration, action handlers) used internally by\n// p/samcrew/basedao and the r/samcrew/daodemo example realm.\n//\n// ─── STATUS: EXPERIMENTAL / INSECURE / INCOMPLETE / COMPLICATED ───\n//\n// Do not use as the basis for a production DAO without significant\n// hardening. The gnoland chain's actual governance is r/gov/dao/v0\n// (independent code path; not built on daokit).\n//\n// Known issues (non-exhaustive):\n//\n//   - Re-entrancy: Core.ExecuteWithDispatch evaluates the\n//     caller-supplied daocond.Condition.Eval BEFORE mutating proposal\n//     status. A hostile Condition can re-enter Propose/Vote/Execute\n//     mid-flight on the same DAO. The SECURITY XXX in actions.gno\n//     (lines 17-26) proposes a per-proposal in-flight flag — not\n//     implemented.\n//\n//   - Status state machine is incomplete: there is no Failed or\n//     Closed state. ExecuteWithDispatch moves Status Open→Passed\n//     BEFORE invoking the handler, then sets Executed only on\n//     success. A handler panic leaves the proposal stuck in Passed\n//     with no path to retry, fail, or cancel.\n//\n//   - ResourcesStore.Set silently overwrites entries by Action kind.\n//     The Handler is re-fetched at execute time, so a malicious\n//     owner can swap the handler between propose and execute. The\n//     captured Condition is immune (resolved at propose time) but\n//     Handler is not.\n//\n//   - Core.Vote does not bound the Vote payload size. Anyone\n//     reaching Core directly (e.g. via *DAOPrivate.Core in basedao)\n//     can grief storage. basedao.DAOPrivate.Vote bounds at 16 bytes;\n//     the underlying Core does not.\n//\n//   - daokit.ActionHandler and daocond.Condition are open interfaces.\n//     Hostile implementations registered via Resources.Set run with\n//     the DAO's authority — same trust level as code inside the DAO\n//     package itself. This is documented in actions.gno but not\n//     enforced.\n//\n//   - Stale comment at actions.gno:102-108 references a removed\n//     InstantExecute free function that no longer exists.\n//\n// Architectural complexity notes:\n//\n//   - The Action / ActionHandler / Resources triangle requires three\n//     coordinated registrations per action kind (Action type,\n//     Handler closure, Resource entry with Condition).\n//   - basedao layers a DAOWrapper / DAOPrivate split on top.\n//     DAOWrapper.ExecuteWithDispatch routes actions that need\n//     cross-realm authority through an rlm-aware handler before\n//     falling back to the address-typed Core.Execute path. Reading\n//     the execute path requires tracing through three packages.\n//\n// Use at your own risk. If you adopt this package, fork it and\n// harden the items above before exposing it to untrusted input.\npackage daokit\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/samcrew/daocond\"\n)\n\n// DAO is the internal dispatch interface for a DAO. All methods are\n// address-typed (no rlm crosses the interface boundary) so a malicious\n// implementer cannot capture a cur from a consumer's wrapper. This is the\n// Authority-pattern: the trusted rlm-validating wrapper (e.g.\n// *basedao.DAOWrapper) is the only public surface; this interface is\n// dispatch-only.\n//\n// CONTRACT: NEVER store this interface as a public package var, NEVER\n// accept it as a public function parameter, and NEVER return it from a\n// public constructor. Public APIs should expose a concrete wrapper type\n// (such as *basedao.DAOWrapper) whose rlm-typed methods derive the\n// caller from cur.Previous().Address() under an IsCurrent() guard.\n//\n// Sealing via an unexported marker method would NOT help — embedding\n// bypasses the seal; see p/test/seal/filetests/z_seal_*_filetest.gno.\ntype DAO interface {\n\tPropose(proposer address, req ProposalRequest) uint64\n\tExecute(executor address, id uint64)\n\tVote(voter address, id uint64, vote daocond.Vote)\n}\n\ntype Core struct {\n\tResources *ResourcesStore\n\tProposals *ProposalsStore\n}\n\nfunc NewCore() *Core {\n\treturn \u0026Core{\n\t\tResources: NewResourcesStore(),\n\t\tProposals: NewProposalsStore(),\n\t}\n}\n\nfunc (d *Core) Vote(voterID string, proposalID uint64, vote daocond.Vote) {\n\tproposal := d.Proposals.GetProposal(proposalID)\n\tif proposal == nil {\n\t\tpanic(\"proposal not found\")\n\t}\n\n\tif proposal.Status != ProposalStatusOpen {\n\t\tpanic(\"proposal is not open\")\n\t}\n\n\tproposal.Ballot.Vote(voterID, daocond.Vote(vote))\n}\n\n// Execute runs the proposal's registered action handler via the generic\n// daokit.ActionHandler.Execute path. Equivalent to\n// ExecuteWithDispatch(proposalID, nil).\nfunc (d *Core) Execute(proposalID uint64) {\n\td.ExecuteWithDispatch(proposalID, nil)\n}\n\n// ExecuteWithDispatch validates the proposal (existence, Open status,\n// Condition.Eval, status → Passed) and then either runs the registered\n// Handler or yields to the caller-supplied dispatch override. If\n// `dispatch(action)` returns true the generic Handler.Execute call is\n// skipped — the caller is asserting that it dispatched the action via\n// its own path. Used by basedao to thread a live `rlm` into the\n// EditProfile dispatcher without putting `rlm` on the public\n// daokit.ActionHandler interface (Class-1a defense — see\n// docs/resources/gno-security.md).\n//\n// The dispatch callback receives only the public `Action` value; no\n// `rlm` flows through this function's signature. rlm-bearing dispatch\n// is the caller's responsibility, captured in their closure.\n//\n// CAVEAT (documented contract-violation symptom): if the registered\n// generic Handler.Execute panics — e.g. basedao's editProfileStubHandler\n// when EditProfile is reached via the legacy DAOPrivate.Execute →\n// Core.Execute path — the proposal's status has already moved\n// Open → Passed (by UpdateStatus above), but Status = Executed is never\n// set. The proposal becomes stuck in Passed. This is only reachable\n// when a caller violates the \"DAOPrivate is internal-only\" contract\n// (basedao.gno DAOWrapper.Private() doc); production callers go through\n// DAOWrapper.Execute → DAOPrivate.executeWithRlm → here with a non-nil\n// dispatch that intercepts EditProfile before the stub is reached.\nfunc (d *Core) ExecuteWithDispatch(proposalID uint64, dispatch func(Action) bool) {\n\tproposal := d.Proposals.GetProposal(proposalID)\n\tif proposal == nil {\n\t\tpanic(\"proposal not found\")\n\t}\n\n\tif proposal.Status != ProposalStatusOpen {\n\t\tpanic(\"proposal is not open\")\n\t}\n\n\tif !proposal.Condition.Eval(proposal.Ballot) {\n\t\tpanic(\"proposal condition is not met\")\n\t}\n\n\tproposal.UpdateStatus()\n\tif proposal.Status != ProposalStatusPassed {\n\t\tpanic(\"proposal does not meet the condition(s) or is already closed/executed\")\n\t}\n\n\tif dispatch == nil || !dispatch(proposal.Action) {\n\t\td.Resources.Get(proposal.Action.Type()).Handler.Execute(proposal.Action)\n\t}\n\tproposal.Status = ProposalStatusExecuted\n\tproposal.ExecutedAt = time.Now()\n}\n\nfunc (d *Core) Propose(proposerID string, req ProposalRequest) uint64 {\n\tactionType := req.Action.Type()\n\n\tresource := d.Resources.Get(actionType)\n\tif resource == nil {\n\t\tpanic(\"action type is not registered as a resource\")\n\t}\n\n\tprop := d.Proposals.newProposal(proposerID, req, resource.Condition)\n\treturn uint64(prop.ID)\n}\n\nfunc (d *Core) ResourcesCount() int {\n\treturn d.Resources.Tree.Size()\n}\n"},{"name":"daokit_test.gno","body":"package daokit\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n)\n\nvar (\n\talice = testutils.TestAddress(\"alice\")\n\tbob   = testutils.TestAddress(\"bob\")\n\tcarol = testutils.TestAddress(\"carol\")\n\tdave  = testutils.TestAddress(\"dave\")\n)\n\nfunc TestNewDAO(t *testing.T) {\n\ttesting.SetOriginCaller(alice)\n\tdao := NewCore()\n\n\tresourcesLen := dao.Resources.Tree.Size()\n\tif resourcesLen != 0 { // There is 0 default resources\n\t\tt.Errorf(\"Expected 0 resources, got %d\", resourcesLen)\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/samcrew/daokit\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1kfd9f5zlvcvy6aammcmqswa7cyjpu2nyt9qfen\"\n"},{"name":"proposals.gno","body":"package daokit\n\nimport (\n\t\"chain/runtime\"\n\t\"errors\"\n\t\"time\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/p/onbloc/json/v0\"\n\t\"gno.land/p/samcrew/daocond\"\n)\n\ntype ProposalStatus int\n\nconst (\n\tProposalStatusOpen ProposalStatus = iota\n\tProposalStatusPassed\n\tProposalStatusExecuted\n)\n\nfunc (s ProposalStatus) String() string {\n\tswitch s {\n\tcase ProposalStatusOpen:\n\t\treturn \"Open\"\n\tcase ProposalStatusPassed:\n\t\treturn \"Passed\"\n\tcase ProposalStatusExecuted:\n\t\treturn \"Executed\"\n\tdefault:\n\t\treturn \"Unknown\"\n\t}\n}\n\ntype Proposal struct {\n\tID            seqid.ID\n\tTitle         string\n\tDescription   string\n\tCreatedAt     time.Time\n\tCreatedHeight int64\n\tProposerID    string\n\tCondition     daocond.Condition\n\tAction        Action\n\tStatus        ProposalStatus\n\tExecutorID    string\n\tExecutedAt    time.Time\n\tBallot        daocond.Ballot\n}\n\ntype ProposalsStore struct {\n\tTree  *avl.Tree // int -\u003e Proposal\n\tgenID seqid.ID\n}\n\ntype ProposalRequest struct {\n\tTitle       string\n\tDescription string\n\tAction      Action\n}\n\nfunc NewProposalsStore() *ProposalsStore {\n\treturn \u0026ProposalsStore{\n\t\tTree: avl.NewTree(),\n\t}\n}\n\nfunc (p *ProposalsStore) newProposal(proposer string, req ProposalRequest, condition daocond.Condition) *Proposal {\n\tid := p.genID.Next()\n\tproposal := \u0026Proposal{\n\t\tID:            id,\n\t\tTitle:         req.Title,\n\t\tDescription:   req.Description,\n\t\tProposerID:    proposer,\n\t\tStatus:        ProposalStatusOpen,\n\t\tAction:        req.Action,\n\t\tCondition:     condition,\n\t\tBallot:        daocond.NewBallot(),\n\t\tCreatedAt:     time.Now(),\n\t\tCreatedHeight: runtime.ChainHeight(),\n\t}\n\tp.Tree.Set(id.String(), proposal)\n\treturn proposal\n}\n\nfunc (p *ProposalsStore) GetProposal(id uint64) *Proposal {\n\tvalue := p.Tree.Get(seqid.ID(id).String())\n\tif value == nil {\n\t\treturn nil\n\t}\n\tproposal := value.(*Proposal)\n\treturn proposal\n}\n\nfunc (p *Proposal) UpdateStatus() {\n\tconditionsAreMet := p.Condition.Eval(p.Ballot)\n\tif p.Status == ProposalStatusOpen \u0026\u0026 conditionsAreMet {\n\t\tp.Status = ProposalStatusPassed\n\t}\n}\n\nfunc (p *ProposalsStore) GetProposalsJSON() string {\n\tprops := make([]*json.Node, 0, p.Tree.Size())\n\t// XXX: pagination\n\tp.Tree.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tprop, ok := value.(*Proposal)\n\t\tif !ok {\n\t\t\tpanic(errors.New(\"unexpected invalid proposal type\"))\n\t\t}\n\t\tprop.UpdateStatus()\n\t\tprops = append(props, json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\t\"id\":          json.NumberNode(\"\", float64(prop.ID)),\n\t\t\t\"title\":       json.StringNode(\"\", prop.Title),\n\t\t\t\"description\": json.StringNode(\"\", prop.Description),\n\t\t\t\"proposer\":    json.StringNode(\"\", prop.ProposerID),\n\t\t\t\"status\":      json.StringNode(\"\", prop.Status.String()),\n\t\t\t\"startHeight\": json.NumberNode(\"\", float64(prop.CreatedHeight)),\n\t\t\t\"signal\":      json.NumberNode(\"\", prop.Condition.Signal(prop.Ballot)),\n\t\t}))\n\t\treturn false\n\t})\n\tbz, err := json.Marshal(json.ArrayNode(\"\", props))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn string(bz)\n}\n"},{"name":"resources.gno","body":"package daokit\n\nimport (\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/samcrew/daocond\"\n)\n\ntype ResourcesStore struct {\n\tTree *avl.Tree // string -\u003e daocond.Condition\n}\n\n// XXX: should condition be a pointer?\ntype Resource struct {\n\tHandler     ActionHandler\n\tCondition   daocond.Condition\n\tDisplayName string\n\tDescription string\n}\n\nfunc NewResourcesStore() *ResourcesStore {\n\treturn \u0026ResourcesStore{\n\t\tTree: avl.NewTree(),\n\t}\n}\n\nfunc (r *ResourcesStore) Set(resource *Resource) {\n\tr.Tree.Set(resource.Handler.Type(), resource)\n}\n\nfunc (r *ResourcesStore) Get(name string) *Resource {\n\tvalue := r.Tree.Get(name)\n\tif value == nil {\n\t\treturn nil\n\t}\n\tres := value.(*Resource)\n\treturn res\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"ctxcXI74PHmCsrVIYfHASCuRnfn7kau/spnw+NRmfwVvaL0q3WHofGCj22JECzRD1tzQjssplTgrKdE7Rj0ijQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1kfd9f5zlvcvy6aammcmqswa7cyjpu2nyt9qfen","package":{"name":"basedao","path":"gno.land/p/samcrew/basedao","files":[{"name":"actions.gno","body":"package basedao\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/samcrew/daokit\"\n)\n\n// ADD MEMBER\n\nconst ActionAddMemberKind = \"gno.land/p/samcrew/basedao.AddMember\"\n\ntype ActionAddMember struct {\n\tAddress address\n\tRoles   []string\n}\n\nfunc (a *ActionAddMember) String() string {\n\treturn ufmt.Sprintf(\"Add member %s with roles %v\", a.Address, a.Roles)\n}\n\nfunc NewAddMemberHandler(dao *DAOPrivate) daokit.ActionHandler {\n\treturn daokit.NewActionHandler(ActionAddMemberKind, func(ipayload interface{}) {\n\t\tpayload, ok := ipayload.(*ActionAddMember)\n\t\tif !ok {\n\t\t\tpanic(errors.New(\"invalid payload type\"))\n\t\t}\n\t\tdao.Members.AddMember(payload.Address.String(), payload.Roles)\n\t})\n}\n\nfunc NewAddMemberAction(payload *ActionAddMember) daokit.Action {\n\treturn daokit.NewAction(ActionAddMemberKind, payload)\n}\n\n// REMOVE MEMBER\n\nconst ActionRemoveMemberKind = \"gno.land/p/samcrew/basedao.RemoveMember\"\n\nfunc NewRemoveMemberHandler(dao *DAOPrivate) daokit.ActionHandler {\n\treturn daokit.NewActionHandler(ActionRemoveMemberKind, func(ipayload interface{}) {\n\t\taddr, ok := ipayload.(address)\n\t\tif !ok {\n\t\t\tpanic(errors.New(\"invalid payload type\"))\n\t\t}\n\t\tdao.Members.RemoveMember(addr.String())\n\t})\n}\n\nfunc NewRemoveMemberAction(addr address) daokit.Action {\n\treturn daokit.NewAction(ActionRemoveMemberKind, addr)\n}\n\n// ASSIGN ROLE\n\nconst ActionAssignRoleKind = \"gno.land/p/samcrew/basedao.AssignRole\"\n\ntype ActionAssignRole struct {\n\tAddress address\n\tRole    string\n}\n\nfunc (a *ActionAssignRole) String() string {\n\treturn ufmt.Sprintf(\"Assign role %q to user %s\", a.Role, a.Address)\n}\n\nfunc NewAssignRoleHandler(dao *DAOPrivate) daokit.ActionHandler {\n\treturn daokit.NewActionHandler(ActionAssignRoleKind, func(i interface{}) {\n\t\tpayload, ok := i.(*ActionAssignRole)\n\t\tif !ok {\n\t\t\tpanic(errors.New(\"invalid payload type\"))\n\t\t}\n\t\tdao.Members.AddRoleToMember(payload.Address.String(), payload.Role)\n\t})\n}\n\nfunc NewAssignRoleAction(payload *ActionAssignRole) daokit.Action {\n\treturn daokit.NewAction(ActionAssignRoleKind, payload)\n}\n\n// UNASSIGN ROLE\n\nconst ActionUnassignRoleKind = \"gno.land/p/samcrew/basedao.UnassignRole\"\n\ntype ActionUnassignRole struct {\n\tAddress address\n\tRole    string\n}\n\nfunc (a *ActionUnassignRole) String() string {\n\treturn ufmt.Sprintf(\"Remove role %q from user %s\", a.Role, a.Address)\n}\n\nfunc NewUnassignRoleHandler(dao *DAOPrivate) daokit.ActionHandler {\n\treturn daokit.NewActionHandler(ActionUnassignRoleKind, func(i interface{}) {\n\t\tpayload, ok := i.(*ActionUnassignRole)\n\t\tif !ok {\n\t\t\tpanic(errors.New(\"invalid payload type\"))\n\t\t}\n\t\tdao.Members.RemoveRoleFromMember(payload.Address.String(), payload.Role)\n\t})\n}\n\nfunc NewUnassignRoleAction(payload *ActionUnassignRole) daokit.Action {\n\treturn daokit.NewAction(ActionUnassignRoleKind, payload)\n}\n\n// EDIT PROFILE\n//\n// EditProfile is the only basedao action that crosses into a foreign\n// realm (the consumer-supplied profile store). It cannot go through the\n// generic daokit.ActionHandler dispatch because that interface is\n// cur-free by design — putting rlm on it would be Class-1a (see\n// docs/resources/gno-security.md). Instead:\n//\n//   - editProfileHandler is a concrete basedao-internal type with an\n//     UNEXPORTED execute method. No foreign type can satisfy an\n//     interface that includes an unexported method, so no hostile\n//     implementation can be substituted in.\n//   - editProfileStubHandler is registered in daokit.Core.Resources so\n//     that proposal-creation can find the action's Condition. Its\n//     Execute method panics — if reached, a caller bypassed\n//     DAOWrapper.Execute (the rlm-aware path).\n//   - DAOWrapper.Execute → DAOPrivate.executeWithRlm uses\n//     Core.ExecuteWithDispatch to intercept ActionEditProfileKind and\n//     dispatch through editProfileHandler.execute(0, rlm, action),\n//     enabling cross(rlm) instead of a bare cross.\n\nconst ActionEditProfileKind = \"gno.land/p/samcrew/basedao.EditProfile\"\n\n// ActionEditProfile implements daokit.Action directly (rather than being\n// wrapped via daokit.NewAction's *genericAction). The rlm-aware\n// dispatcher in basedao type-asserts action.(*ActionEditProfile) to\n// reach the kv payload; that only works if the proposal carries\n// *ActionEditProfile as the Action value, not a generic wrapper around\n// it. (genericAction's payload field is unexported in daokit, so basedao\n// cannot unwrap.)\ntype ActionEditProfile struct {\n\tkv [][2]string\n}\n\nfunc (a *ActionEditProfile) String() string {\n\telems := []string{}\n\tfor _, v := range a.kv {\n\t\telems = append(elems, v[0]+\": \"+v[1])\n\t}\n\treturn md.BulletList(elems)\n}\n\nfunc (a *ActionEditProfile) Type() string { return ActionEditProfileKind }\n\nfunc NewEditProfileAction(kv ...[2]string) daokit.Action {\n\treturn \u0026ActionEditProfile{kv: kv}\n}\n\n// editProfileHandler is the concrete, rlm-aware EditProfile dispatcher.\n// Stored on *DAOPrivate.editProfile (not in Core.Resources), invoked\n// from DAOPrivate.executeWithRlm under DAOWrapper.Execute.\ntype editProfileHandler struct {\n\tsetter        ProfileStringSetter\n\tallowedFields []string\n}\n\n// execute is unexported intentionally — see the file-level EditProfile\n// comment. The unexported method name binds the interface signature to\n// the basedao package, preventing any foreign type from being\n// substituted into basedao's rlm-bearing dispatch path.\nfunc (h *editProfileHandler) execute(_ int, rlm realm, action daokit.Action) {\n\ta, ok := action.(*ActionEditProfile)\n\tif !ok {\n\t\tpanic(errors.New(\"invalid action type\"))\n\t}\n\tfor _, elem := range a.kv {\n\t\tk, v := elem[0], elem[1]\n\t\tif len(h.allowedFields) \u003e 0 \u0026\u0026 !stringSliceContains(h.allowedFields, k) {\n\t\t\tpanic(ufmt.Errorf(\"unauthorized field %q\", k))\n\t\t}\n\t\th.setter(cross(rlm), k, v)\n\t}\n}\n\n// editProfileStubHandler is a daokit.ActionHandler registered in\n// Core.Resources for ActionEditProfileKind so that Core.Propose's\n// Condition lookup succeeds. Its Execute panics — EditProfile MUST be\n// dispatched via DAOWrapper.Execute (the rlm-aware path); reaching this\n// method means the caller bypassed that contract.\ntype editProfileStubHandler struct{}\n\nfunc (editProfileStubHandler) Type() string { return ActionEditProfileKind }\n\nfunc (editProfileStubHandler) Execute(action daokit.Action) {\n\tpanic(errors.New(\"ActionEditProfileKind must be dispatched via basedao.DAOWrapper.Execute (rlm-aware path)\"))\n}\n\nfunc stringSliceContains(s []string, target string) bool {\n\tfor _, elem := range s {\n\t\tif elem == target {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"},{"name":"basedao.gno","body":"// Package basedao is a reference DAO implementation built on\n// p/samcrew/daokit. Used by the r/samcrew/daodemo example realm.\n//\n// ─── STATUS: EXPERIMENTAL / INSECURE / INCOMPLETE / COMPLICATED ───\n//\n// Do not use as the basis for a production DAO without significant\n// hardening. The gnoland chain's actual governance is r/gov/dao/v0\n// (independent code path; not built on basedao).\n//\n// Known issues (non-exhaustive):\n//\n//   - DAOWrapper.Private() returns the raw *DAOPrivate. Its Core,\n//     Members, and Resources fields are all exported and mutable.\n//     A caller that holds *DAOPrivate can mutate members, swap\n//     handlers, or invoke Core.Execute directly with any address,\n//     bypassing IsCurrent / member-only / proposal-vote gates. The\n//     \"DAOPrivate is internal-only\" contract is documented but\n//     unenforced — anyone exposing *DAOPrivate outside their own\n//     realm has handed the DAO away.\n//\n//   - MembersStore.RemoveMember has a role-bookkeeping bug: the line\n//     `role.Members.Remove(role.Name)` should be `Remove(member)`.\n//     Removed members continue to count toward Role-keyed quorum\n//     math (RoleThreshold / RoleCount conditions) until the role\n//     itself is removed.\n//\n//   - No \"at-least-one-member\" invariant. RemoveMember accepts going\n//     to zero members; the resulting DAO is permanently bricked\n//     (IsMember returns false for all callers, all writes panic).\n//     Similarly, no \"at-least-one-admin\" guard if you use a\n//     RoleThreshold condition.\n//\n//   - Address/role-name input validation is missing in handlers.\n//     ActionAddMember.Address is not checked for .IsValid(); empty\n//     or zero addresses produce phantom voters that can be added\n//     and granted roles. Role names accept arbitrary strings\n//     including empty / unicode / slashes (the last breaks\n//     render-URL construction).\n//\n//   - EditProfile dispatch uses an rlm-aware path via\n//     ExecuteWithDispatch + editProfileStubHandler. If a caller\n//     reaches the legacy DAOPrivate.Execute → Core.Execute path\n//     for an EditProfile proposal, the stub panics AFTER\n//     UpdateStatus mutates Open→Passed, leaving the proposal\n//     permanently stuck. Production daodemo doesn't trigger this;\n//     tests don't either.\n//\n//   - See p/samcrew/daokit's package doc for additional issues\n//     inherited from the underlying toolkit (re-entrancy via\n//     Condition.Eval, stuck-Passed handler-panic state,\n//     resource-overwrite mid-flight, etc.)\n//\n// Architectural complexity notes:\n//\n//   - The action dispatch path threads through three packages\n//     (basedao → daokit Core → registered Handler), with an\n//     additional carve-out (DAOPrivate.executeWithRlm +\n//     editProfileHandler) for cross-realm dispatch that needs a\n//     live cur. Auditing any single action requires understanding\n//     all three layers.\n//   - DAOWrapper (rlm-typed public surface) and DAOPrivate\n//     (address-typed internal) is the Authority-pattern split;\n//     the split is correct in intent but Private() escapes it.\n//\n// Use at your own risk. If you adopt this package, fork it and\n// harden the items above before exposing it to untrusted input.\npackage basedao\n\nimport (\n\t\"chain\"\n\t\"errors\"\n\n\t\"gno.land/p/nt/mux/v0\"\n\t\"gno.land/p/samcrew/daocond\"\n\t\"gno.land/p/samcrew/daokit\"\n)\n\n// DAOWrapper is the public, rlm-validating surface of a basedao DAO. All\n// mutating methods take (_ int, rlm realm, ...), validate via\n// rlm.IsCurrent(), and derive the principal from rlm.Previous().Address()\n// before invoking the private impl's address-typed methods. This is the\n// trusted boundary — the daokit.DAO interface itself is dispatch-only and\n// never exposed publicly.\n//\n// Consumers in /r/ should hold `*DAOWrapper`, never `daokit.DAO`.\ntype DAOWrapper struct {\n\timpl *DAOPrivate\n}\n\nfunc (w *DAOWrapper) Propose(_ int, rlm realm, req daokit.ProposalRequest) uint64 {\n\tif !rlm.IsCurrent() {\n\t\tpanic(errors.New(\"caller is not a member\"))\n\t}\n\tproposer := rlm.Previous().Address()\n\treturn w.impl.Propose(proposer, req)\n}\n\nfunc (w *DAOWrapper) Execute(_ int, rlm realm, id uint64) {\n\tif !rlm.IsCurrent() {\n\t\tpanic(errors.New(\"caller is not a member\"))\n\t}\n\texecutor := rlm.Previous().Address()\n\t// Route through the rlm-aware path: ActionEditProfileKind is dispatched\n\t// via editProfileHandler.execute(0, rlm, ...) which uses cross(rlm).\n\t// All other action kinds fall through to generic Core dispatch.\n\tw.impl.executeWithRlm(0, rlm, executor, id)\n}\n\nfunc (w *DAOWrapper) Vote(_ int, rlm realm, id uint64, vote daocond.Vote) {\n\tif !rlm.IsCurrent() {\n\t\tpanic(errors.New(\"caller is not a member\"))\n\t}\n\tvoter := rlm.Previous().Address()\n\tw.impl.Vote(voter, id, vote)\n}\n\n// InstantExecute proposes, votes Yes, and executes in one call. Each step\n// goes through this wrapper's rlm-validated methods, so IsCurrent is\n// re-checked at every step (defense-in-depth against drift if the impl is\n// later changed to skip per-call validation).\nfunc (w *DAOWrapper) InstantExecute(_ int, rlm realm, req daokit.ProposalRequest) uint64 {\n\tid := w.Propose(0, rlm, req)\n\tw.Vote(0, rlm, id, daocond.VoteYes)\n\tw.Execute(0, rlm, id)\n\treturn id\n}\n\n// Render delegates to the private impl's renderer. The impl's render\n// surface is read-only (no rlm/cur in any render method).\nfunc (w *DAOWrapper) Render(path string) string {\n\treturn w.impl.Render(path)\n}\n\n// Private returns the internal *DAOPrivate. This is intended ONLY for\n// the realm that owns the DAOWrapper to wire up custom handlers or\n// extend rendering — never expose the result to other realms. Foreign\n// code that holds a *DAOPrivate can call its address-typed writers\n// directly with any address argument, which would bypass IsCurrent.\nfunc (w *DAOWrapper) Private() *DAOPrivate {\n\treturn w.impl\n}\n\n// DAOPrivate is meant for internal realm usage and should not be exposed.\n//\n// SECURITY: writer methods (Propose, Vote, Execute) are address-typed and\n// trust the supplied caller. They DO NOT validate IsCurrent — that\n// validation lives in DAOWrapper. Holding *DAOPrivate and calling its\n// methods with a forged caller address is a Class-2 designation-forgery.\n// Owning realms must keep their *DAOPrivate in an unexported package var\n// and never return it from any exported function. Use DAOWrapper.Private()\n// only at construction time to register custom action handlers.\ntype DAOPrivate struct {\n\tCore             *daokit.Core\n\tMembers          *MembersStore\n\tRenderRouter     *mux.Router\n\tGetProfileString ProfileStringGetter\n\t// RealmPkgPath / RealmAddress are captured at construction time as\n\t// plain string/address values rather than a runtime.Realm struct.\n\t// Storing a runtime.Realm in a struct field would be a parallel\n\t// anti-pattern to storing a `cur realm` (capability token in a\n\t// struct). PkgPath/Address are stable identity strings, safe to\n\t// persist.\n\tRealmPkgPath string\n\tRealmAddress address\n\t// editProfile carries the rlm-aware dispatcher for\n\t// ActionEditProfileKind. nil if Config.SetProfileString was nil.\n\t// See actions.gno for the rationale (concrete type + unexported\n\t// method closes Class-1a; basedao routes around the generic\n\t// daokit.ActionHandler interface for this one action).\n\teditProfile *editProfileHandler\n}\n\ntype Config struct {\n\tName               string\n\tDescription        string\n\tImageURI           string\n\tMembers            *MembersStore\n\tNoDefaultHandlers  bool\n\tNoDefaultRendering bool\n\tInitialCondition   daocond.Condition\n\tSetProfileString   ProfileStringSetter\n\tGetProfileString   ProfileStringGetter\n\tNoCreationEvent    bool\n}\n\ntype ProfileStringSetter func(cur realm, field string, value string) bool\ntype ProfileStringGetter func(addr address, field string, def string) string\n\nconst EventBaseDAOCreated = \"BaseDAOCreated\"\n\n// New constructs a basedao DAO and returns its public wrapper. The\n// returned *DAOWrapper is the canonical type for /r/ consumers to hold;\n// it exposes rlm-validating Propose / Vote / Execute / InstantExecute /\n// Render methods, and a .Private() accessor for the owning realm to wire\n// custom action handlers at init time.\n//\n// rlm is threaded into the optional `conf.SetProfileString` callback via\n// cross(rlm). /p/ production code cannot declare crossing functions, so\n// rlm is taken as a non-first parameter — callers pass `cur` directly.\nfunc New(conf *Config, rlm realm) *DAOWrapper {\n\t// XXX: emit events from memberstore\n\n\t// IsCurrent gate: rlm must be the caller's live cur. Otherwise\n\t// RealmPkgPath/RealmAddress (captured below into a long-lived\n\t// struct) would be set from a stale or sibling-frame value, and\n\t// the cross(rlm) calls further down would panic anyway when\n\t// SetProfileString is non-nil. Catch the misuse up-front so the\n\t// `SetProfileString == nil` path is also guarded.\n\tif !rlm.IsCurrent() {\n\t\tpanic(\"basedao.New: rlm is not the caller's live cur\")\n\t}\n\n\tmembers := conf.Members\n\tif members == nil {\n\t\tmembers = NewMembersStore(nil, nil)\n\t}\n\n\tif conf.GetProfileString == nil {\n\t\tpanic(errors.New(\"GetProfileString is required\"))\n\t}\n\n\tcore := daokit.NewCore()\n\tdao := \u0026DAOPrivate{\n\t\tCore:             core,\n\t\tMembers:          members,\n\t\tGetProfileString: conf.GetProfileString,\n\t\tRealmPkgPath:     rlm.PkgPath(),\n\t\tRealmAddress:     rlm.Address(),\n\t}\n\tdao.initRenderingRouter()\n\n\tif !conf.NoDefaultRendering {\n\t\tdao.InitDefaultRendering()\n\t}\n\n\tif conf.SetProfileString != nil {\n\t\tconf.SetProfileString(cross(rlm), \"DisplayName\", conf.Name)\n\t\tconf.SetProfileString(cross(rlm), \"Bio\", conf.Description)\n\t\tconf.SetProfileString(cross(rlm), \"Avatar\", conf.ImageURI)\n\t}\n\n\tif !conf.NoDefaultHandlers {\n\t\tif conf.InitialCondition == nil {\n\t\t\tconf.InitialCondition = daocond.MembersThreshold(0.6, members.IsMember, members.MembersCount)\n\t\t}\n\n\t\tif conf.SetProfileString != nil {\n\t\t\t// editProfile is the rlm-aware dispatcher used by\n\t\t\t// DAOPrivate.executeWithRlm. The Resource entry below uses a\n\t\t\t// panicking stub (editProfileStubHandler) so Core.Propose's\n\t\t\t// Condition lookup succeeds; the stub is unreachable on the\n\t\t\t// production execute path (DAOWrapper.Execute routes via\n\t\t\t// executeWithRlm with an override that intercepts EditProfile).\n\t\t\tdao.editProfile = \u0026editProfileHandler{\n\t\t\t\tsetter:        conf.SetProfileString,\n\t\t\t\tallowedFields: []string{\"DisplayName\", \"Bio\", \"Avatar\"},\n\t\t\t}\n\t\t\tdao.Core.Resources.Set(\u0026daokit.Resource{\n\t\t\t\tHandler:   editProfileStubHandler{},\n\t\t\t\tCondition: conf.InitialCondition,\n\t\t\t})\n\t\t}\n\n\t\tdefaultResources := []daokit.Resource{\n\t\t\t{\n\t\t\t\tHandler:     NewAddMemberHandler(dao),\n\t\t\t\tCondition:   conf.InitialCondition,\n\t\t\t\tDisplayName: \"Add Member\",\n\t\t\t\tDescription: \"This proposal allows you to add a new member to the DAO.\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tHandler:     NewRemoveMemberHandler(dao),\n\t\t\t\tCondition:   conf.InitialCondition,\n\t\t\t\tDisplayName: \"Remove Member\",\n\t\t\t\tDescription: \"This proposal allows you to remove a member from the DAO.\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tHandler:     NewAssignRoleHandler(dao),\n\t\t\t\tCondition:   conf.InitialCondition,\n\t\t\t\tDisplayName: \"Assign Role\",\n\t\t\t\tDescription: \"This proposal allows you to assign a role to a member.\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tHandler:     NewUnassignRoleHandler(dao),\n\t\t\t\tCondition:   conf.InitialCondition,\n\t\t\t\tDisplayName: \"Unassign Role\",\n\t\t\t\tDescription: \"This proposal allows you to unassign a role from a member.\",\n\t\t\t},\n\t\t}\n\t\t// register management handlers\n\t\tfor _, resource := range defaultResources {\n\t\t\tdao.Core.Resources.Set(\u0026resource)\n\t\t}\n\n\t}\n\n\tif !conf.NoCreationEvent {\n\t\tchain.Emit(EventBaseDAOCreated)\n\t}\n\n\treturn \u0026DAOWrapper{impl: dao}\n}\n\n// Vote on a proposal. caller must be a current DAO member; the wrapper\n// (DAOWrapper.Vote) derives this from rlm.Previous().Address() under\n// IsCurrent. Direct callers of DAOPrivate.Vote forge the caller at their\n// own risk — see the type-level security note.\nfunc (d *DAOPrivate) Vote(caller address, proposalID uint64, vote daocond.Vote) {\n\tif len(vote) \u003e 16 {\n\t\tpanic(\"invalid vote\")\n\t}\n\tvoterID := d.assertCallerIsMember(caller)\n\td.Core.Vote(voterID, proposalID, vote)\n}\n\n// Execute a proposal. See Vote for caller semantics.\n//\n// Address-typed legacy path (preserves the daokit.DAO interface contract\n// of no rlm crossing the dispatch boundary). EditProfile proposals\n// reaching this method will panic via the registered editProfileStubHandler\n// — they must be dispatched via DAOPrivate.executeWithRlm (which\n// DAOWrapper.Execute calls). See daokit.gno ExecuteWithDispatch CAVEAT.\nfunc (d *DAOPrivate) Execute(caller address, proposalID uint64) {\n\t_ = d.assertCallerIsMember(caller)\n\td.Core.Execute(proposalID)\n}\n\n// executeWithRlm is the rlm-aware execute path. Called by DAOWrapper.Execute\n// after IsCurrent has been validated. Asserts caller membership (same as\n// DAOPrivate.Execute), then dispatches through Core.ExecuteWithDispatch\n// with an override that routes ActionEditProfileKind into the\n// editProfile concrete handler. All other action types fall through to\n// generic Core dispatch unchanged.\nfunc (d *DAOPrivate) executeWithRlm(_ int, rlm realm, caller address, proposalID uint64) {\n\t_ = d.assertCallerIsMember(caller)\n\td.Core.ExecuteWithDispatch(proposalID, func(action daokit.Action) bool {\n\t\tif action.Type() == ActionEditProfileKind \u0026\u0026 d.editProfile != nil {\n\t\t\td.editProfile.execute(0, rlm, action)\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t})\n}\n\n// Propose a proposal. See Vote for caller semantics.\nfunc (d *DAOPrivate) Propose(caller address, req daokit.ProposalRequest) uint64 {\n\tproposerID := d.assertCallerIsMember(caller)\n\treturn d.Core.Propose(proposerID, req)\n}\n\n// assertCallerIsMember returns the caller as a string after verifying\n// it's a current DAO member.\nfunc (d *DAOPrivate) assertCallerIsMember(caller address) string {\n\tid := caller.String()\n\tif !d.Members.IsMember(id) {\n\t\tpanic(errors.New(\"caller is not a member\"))\n\t}\n\treturn id\n}\n"},{"name":"basedao_test.gno","body":"package basedao\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n\t\"gno.land/p/samcrew/daocond\"\n\t\"gno.land/p/samcrew/daokit\"\n)\n\nvar (\n\talice = testutils.TestAddress(\"alice\")\n\tbob   = testutils.TestAddress(\"bob\")\n\tcarol = testutils.TestAddress(\"carol\")\n\tdave  = testutils.TestAddress(\"dave\")\n)\n\nfunc TestNewDAO(cur realm, t *testing.T) {\n\tinitialRoles := []RoleInfo{\n\t\t{Name: \"admin\", Description: \"Admin is the superuser\"},\n\t}\n\tinitialMembers := []Member{\n\t\t{alice.String(), []string{\"admin\"}},\n\t\t{bob.String(), []string{}},\n\t\t{carol.String(), []string{}},\n\t}\n\tconf := \u0026Config{\n\t\tName:             \"My DAO\",\n\t\tDescription:      \"My DAO Description\",\n\t\tMembers:          NewMembersStore(initialRoles, initialMembers),\n\t\tGetProfileString: func(addr address, field, def string) string { return \"\" },\n\t}\n\n\tdaoRealm := testing.NewCodeRealm(\"gno.land/r/testing/daorealm\")\n\ttesting.SetOriginCaller(alice)\n\ttesting.SetRealm(daoRealm)\n\twrapper := New(conf, cur)\n\tdao := wrapper.Private()\n\troles := dao.Members.GetRoles()\n\tif len(roles) != 1 {\n\t\tt.Errorf(\"Expected 1 role, got %d\", len(roles))\n\t}\n\tif roles[0] != \"admin\" {\n\t\tt.Errorf(\"Expected role 'admin', got %s\", roles[0])\n\t}\n\n\tfor _, member := range initialMembers {\n\t\taddr := member.Address\n\t\tif !dao.Members.IsMember(addr) {\n\t\t\tt.Errorf(\"Expected member %s to be a member\", addr)\n\t\t}\n\t\tif len(member.Roles) == 1 \u0026\u0026 !dao.Members.HasRole(addr, member.Roles[0]) {\n\t\t\tt.Errorf(\"Expected member %s to have role %s\", addr, member.Roles[0])\n\t\t}\n\t}\n\n\turequire.Equal(t, 4, dao.Core.ResourcesCount(), \"expected 4 resources\")\n\turequire.Equal(t, dao.RealmPkgPath, cur.PkgPath())\n\n\t// XXX: check realm and profile\n}\n\nfunc TestPropose(cur realm, t *testing.T) {\n\tmembers := []Member{\n\t\t{alice.String(), []string{\"admin\"}},\n\t\t{bob.String(), []string{}},\n\t\t{carol.String(), []string{}},\n\t}\n\ttdao := newTestingDAO(0, cur, t, 0.6, members)\n\n\ttype testNewProposalInput struct {\n\t\tproposalReq daokit.ProposalRequest\n\t\tproposer    address\n\t}\n\n\ttype tesNewProposalExpected struct {\n\t\ttitle        string\n\t\tdescription  string\n\t\tproposer     address\n\t\tmesssageType string\n\t\tpanic        bool\n\t}\n\n\ttype testNewProposal struct {\n\t\tinput    testNewProposalInput\n\t\texpected tesNewProposalExpected\n\t}\n\n\ttype testNewProposalTable map[string]testNewProposal\n\n\ttests := testNewProposalTable{\n\t\t\"Success\": {\n\t\t\tinput: testNewProposalInput{\n\t\t\t\tproposalReq: tdao.mockProposalRequest,\n\t\t\t\tproposer:    alice,\n\t\t\t},\n\t\t\texpected: tesNewProposalExpected{\n\t\t\t\ttitle:        \"My Proposal\",\n\t\t\t\tdescription:  \"My Proposal Description\",\n\t\t\t\tproposer:     alice,\n\t\t\t\tmesssageType: \"valid\",\n\t\t\t\tpanic:        false,\n\t\t\t},\n\t\t},\n\t\t\"Non-member\": {\n\t\t\tinput: testNewProposalInput{\n\t\t\t\tproposalReq: tdao.mockProposalRequest,\n\t\t\t\tproposer:    dave,\n\t\t\t},\n\t\t\texpected: tesNewProposalExpected{\n\t\t\t\tpanic: true,\n\t\t\t},\n\t\t},\n\t\t\"Unknown action type\": {\n\t\t\tinput: testNewProposalInput{\n\t\t\t\tproposalReq: tdao.unknownProposalRequest,\n\t\t\t\tproposer:    alice,\n\t\t\t},\n\t\t\texpected: tesNewProposalExpected{\n\t\t\t\tpanic: true,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor testName, test := range tests {\n\t\tt.Run(testName, func(cur realm, t *testing.T) {\n\t\t\t*tdao.mockHandlerCalled = false\n\t\t\ttesting.SetRealm(testing.NewUserRealm(test.input.proposer))\n\n\t\t\t// tdao.propose crosses into basedao via an IIFE same-realm\n\t\t\t// cross; any panic inside the crossing frame is cross-realm\n\t\t\t// and cannot be caught by defer/recover here. Use revive\n\t\t\t// to capture the cross-boundary abort.\n\t\t\tvar id uint64\n\t\t\texc := revive(func() {\n\t\t\t\tid = tdao.propose(0, cur, test.input.proposalReq)\n\t\t\t})\n\t\t\tif test.expected.panic {\n\t\t\t\tif exc == nil {\n\t\t\t\t\tt.Errorf(\"Expected panic, got none\")\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif exc != nil {\n\t\t\t\tt.Errorf(\"Unexpected panic: %v\", exc)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.False(t, *tdao.mockHandlerCalled, \"execute should not be called\")\n\n\t\t\tproposal := tdao.privdao.Core.Proposals.GetProposal(id)\n\t\t\tif proposal.Title != test.expected.title {\n\t\t\t\tt.Errorf(\"Expected title %s, got %s\", test.expected.title, proposal.Title)\n\t\t\t}\n\t\t\tif proposal.Description != test.expected.description {\n\t\t\t\tt.Errorf(\"Expected description %s, got %s\", test.expected.description, proposal.Description)\n\t\t\t}\n\t\t\tif proposal.ProposerID != test.expected.proposer.String() {\n\t\t\t\tt.Errorf(\"Expected proposer %s, got %s\", test.expected.proposer, proposal.ProposerID)\n\t\t\t}\n\t\t\tif proposal.Action.Type() != test.expected.messsageType {\n\t\t\t\tt.Errorf(\"Expected action type %s, got %s\", test.expected.messsageType, proposal.Action.Type())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestVote(cur realm, t *testing.T) {\n\tmembers := []Member{\n\t\t{alice.String(), []string{\"admin\"}},\n\t\t{bob.String(), []string{}},\n\t\t{carol.String(), []string{}},\n\t}\n\ttdao := newTestingDAO(0, cur, t, 0.6, members)\n\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\ttdao.propose(0, cur, tdao.mockProposalRequest)\n\n\ttype testVoteInput struct {\n\t\tproposalID uint64\n\t\tvote       daocond.Vote\n\t\tvoter      address\n\t}\n\n\ttype testVoteExpected struct {\n\t\teval  bool\n\t\tpanic string\n\t}\n\n\ttype testVote struct {\n\t\tinput    testVoteInput\n\t\texpected testVoteExpected\n\t}\n\n\ttype testVoteTable map[string]testVote\n\n\ttests := testVoteTable{\n\t\t\"Success no\": {\n\t\t\tinput: testVoteInput{\n\t\t\t\tproposalID: 1,\n\t\t\t\tvote:       \"no\",\n\t\t\t\tvoter:      alice,\n\t\t\t},\n\t\t\texpected: testVoteExpected{\n\t\t\t\teval: false,\n\t\t\t},\n\t\t},\n\t\t\"Success yes\": {\n\t\t\tinput: testVoteInput{\n\t\t\t\tproposalID: 1,\n\t\t\t\tvote:       \"yes\",\n\t\t\t\tvoter:      alice,\n\t\t\t},\n\t\t\texpected: testVoteExpected{\n\t\t\t\teval: true,\n\t\t\t},\n\t\t},\n\t\t\"Unknown proposal\": {\n\t\t\tinput: testVoteInput{\n\t\t\t\tproposalID: 2,\n\t\t\t\tvote:       \"yes\",\n\t\t\t\tvoter:      alice,\n\t\t\t},\n\t\t\texpected: testVoteExpected{\n\t\t\t\tpanic: \"proposal not found\",\n\t\t\t},\n\t\t},\n\t\t\"Non-member\": {\n\t\t\tinput: testVoteInput{\n\t\t\t\tproposalID: 1,\n\t\t\t\tvote:       \"yes\",\n\t\t\t\tvoter:      dave,\n\t\t\t},\n\t\t\texpected: testVoteExpected{\n\t\t\t\tpanic: \"caller is not a member\",\n\t\t\t},\n\t\t},\n\t\t\"Invalid vote\": {\n\t\t\tinput: testVoteInput{\n\t\t\t\tproposalID: 1,\n\t\t\t\tvote:       \"very-long-vote-very-long-vote-very-long-vote\",\n\t\t\t\tvoter:      alice,\n\t\t\t},\n\t\t\texpected: testVoteExpected{\n\t\t\t\tpanic: \"invalid vote\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor testName, test := range tests {\n\t\tt.Run(testName, func(cur realm, t *testing.T) {\n\t\t\t// SetRealm must run here, not inside `run`: SetRealm targets\n\t\t\t// the deepest non-testing frame's fr.Cur HIV. A non-crossing\n\t\t\t// `run` closure has no fr.Cur, so a SetRealm-from-inside-run\n\t\t\t// silently no-ops on the closure-captured cur.\n\t\t\ttesting.SetRealm(testing.NewUserRealm(test.input.voter))\n\t\t\trun := func() {\n\t\t\t\ttdao.vote(0, cur, test.input.proposalID, test.input.vote)\n\t\t\t}\n\n\t\t\t*tdao.mockHandlerCalled = false\n\n\t\t\tif test.expected.panic != \"\" {\n\t\t\t\t// tdao.vote crosses into basedao; panic is cross-realm\n\t\t\t\t// and shows up as an abort, not a same-realm panic.\n\t\t\t\tuassert.AbortsWithMessage(t, cur, test.expected.panic, run)\n\t\t\t} else {\n\t\t\t\turequire.NotPanics(t, cur, run)\n\t\t\t\turequire.False(t, *tdao.mockHandlerCalled, \"execute should not be called\")\n\t\t\t\tproposal := tdao.privdao.Core.Proposals.GetProposal(test.input.proposalID)\n\t\t\t\teval := proposal.Condition.Eval(proposal.Ballot)\n\t\t\t\tif eval != test.expected.eval {\n\t\t\t\t\tt.Errorf(\"Expected eval %t, got %t\", test.expected.eval, eval)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestExecuteProposal(cur realm, t *testing.T) {\n\tmembers := []Member{\n\t\t{alice.String(), []string{\"admin\"}},\n\t\t{bob.String(), []string{}},\n\t\t{carol.String(), []string{}},\n\t}\n\ttdao := newTestingDAO(0, cur, t, 0.6, members)\n\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\ttdao.propose(0, cur, tdao.mockProposalRequest)\n\n\ttype testExecuteInput struct {\n\t\tproposalID uint64\n\t\texecutor   address\n\t\thaveVote   bool\n\t\tvoter      address\n\t}\n\n\ttype testExecuteExpected struct {\n\t\tpanic bool\n\t}\n\n\ttype testExecute struct {\n\t\tinput    testExecuteInput\n\t\texpected testExecuteExpected\n\t}\n\n\ttype testExecuteTable map[string]testExecute\n\n\ttests := testExecuteTable{\n\t\t\"Conditions not met\": {\n\t\t\tinput: testExecuteInput{\n\t\t\t\tproposalID: 1,\n\t\t\t\texecutor:   alice,\n\t\t\t\thaveVote:   false,\n\t\t\t\tvoter:      alice,\n\t\t\t},\n\t\t\texpected: testExecuteExpected{\n\t\t\t\tpanic: true,\n\t\t\t},\n\t\t},\n\t\t\"Success\": {\n\t\t\tinput: testExecuteInput{\n\t\t\t\tproposalID: 1,\n\t\t\t\texecutor:   alice,\n\t\t\t\thaveVote:   true,\n\t\t\t\tvoter:      alice,\n\t\t\t},\n\t\t\texpected: testExecuteExpected{\n\t\t\t\tpanic: false,\n\t\t\t},\n\t\t},\n\t\t\"Unknown proposal\": {\n\t\t\tinput: testExecuteInput{\n\t\t\t\tproposalID: 2,\n\t\t\t\texecutor:   alice,\n\t\t\t\thaveVote:   false,\n\t\t\t\tvoter:      alice,\n\t\t\t},\n\t\t\texpected: testExecuteExpected{\n\t\t\t\tpanic: true,\n\t\t\t},\n\t\t},\n\t\t\"Non-member\": {\n\t\t\tinput: testExecuteInput{\n\t\t\t\tproposalID: 1,\n\t\t\t\texecutor:   dave,\n\t\t\t\thaveVote:   false,\n\t\t\t\tvoter:      alice,\n\t\t\t},\n\t\t\texpected: testExecuteExpected{\n\t\t\t\tpanic: true,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor testName, test := range tests {\n\t\tt.Run(testName, func(cur realm, t *testing.T) {\n\t\t\tif test.input.haveVote {\n\t\t\t\ttesting.SetRealm(testing.NewUserRealm(test.input.voter))\n\t\t\t\ttdao.vote(0, cur, test.input.proposalID, \"yes\")\n\t\t\t}\n\n\t\t\ttesting.SetRealm(testing.NewUserRealm(test.input.executor))\n\n\t\t\t// tdao.execute crosses into basedao; cross-realm panic is\n\t\t\t// not catchable by defer/recover, so use revive.\n\t\t\texc := revive(func() {\n\t\t\t\ttdao.execute(0, cur, test.input.proposalID)\n\t\t\t})\n\t\t\tif test.expected.panic {\n\t\t\t\tif exc == nil {\n\t\t\t\t\tt.Errorf(\"Expected panic, got none\")\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif exc != nil {\n\t\t\t\tt.Errorf(\"Unexpected panic: %v\", exc)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tproposal := tdao.privdao.Core.Proposals.GetProposal(test.input.proposalID)\n\n\t\t\tif proposal.Status != daokit.ProposalStatusExecuted {\n\t\t\t\tt.Errorf(\"Expected status %s, got %s\", daokit.ProposalStatusExecuted, proposal.Status)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestInstantExecute(cur realm, t *testing.T) {\n\tmembers := []Member{\n\t\t{alice.String(), []string{\"admin\"}},\n\t\t{bob.String(), []string{}},\n\t\t{carol.String(), []string{}},\n\t}\n\ttdao := newTestingDAO(0, cur, t, 0.6, members)\n\n\ttype testInstantExecuteInput struct {\n\t\tproposalReq daokit.ProposalRequest\n\t\texecutor    address\n\t}\n\n\ttype testInstantExecuteExpected struct {\n\t\tpanic bool\n\t}\n\n\ttype testInstantExecute struct {\n\t\tinput    testInstantExecuteInput\n\t\texpected testInstantExecuteExpected\n\t}\n\n\ttype testInstantExecuteTable map[string]testInstantExecute\n\n\ttests := testInstantExecuteTable{\n\t\t\"Success\": {\n\t\t\tinput: testInstantExecuteInput{\n\t\t\t\tproposalReq: tdao.mockProposalRequest,\n\t\t\t\texecutor:    alice,\n\t\t\t},\n\t\t\texpected: testInstantExecuteExpected{\n\t\t\t\tpanic: false,\n\t\t\t},\n\t\t},\n\t\t\"Unknown action type\": {\n\t\t\tinput: testInstantExecuteInput{\n\t\t\t\tproposalReq: tdao.unknownProposalRequest,\n\t\t\t\texecutor:    alice,\n\t\t\t},\n\t\t\texpected: testInstantExecuteExpected{\n\t\t\t\tpanic: true,\n\t\t\t},\n\t\t},\n\t\t\"Non-member\": {\n\t\t\tinput: testInstantExecuteInput{\n\t\t\t\tproposalReq: tdao.mockProposalRequest,\n\t\t\t\texecutor:    dave,\n\t\t\t},\n\t\t\texpected: testInstantExecuteExpected{\n\t\t\t\tpanic: true,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor testName, test := range tests {\n\t\tt.Run(testName, func(cur realm, t *testing.T) {\n\t\t\ttesting.SetRealm(testing.NewUserRealm(test.input.executor))\n\n\t\t\t// tdao.propose crosses into basedao; cross-realm panic\n\t\t\t// is not catchable by defer/recover, so use revive.\n\t\t\texc := revive(func() {\n\t\t\t\ttdao.propose(0, cur, test.input.proposalReq)\n\t\t\t})\n\t\t\tif test.expected.panic {\n\t\t\t\tif exc == nil {\n\t\t\t\t\tt.Errorf(\"Expected panic, got none\")\n\t\t\t\t}\n\t\t\t} else if exc != nil {\n\t\t\t\tt.Errorf(\"Unexpected panic: %v\", exc)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetMembers(cur realm, t *testing.T) {\n\tmembers := []Member{\n\t\t{alice.String(), []string{\"admin\"}},\n\t\t{bob.String(), []string{}},\n\t\t{carol.String(), []string{}},\n\t}\n\ttdao := newTestingDAO(0, cur, t, 0.6, members)\n\n\texpectedMembers := []string{alice.String(), bob.String(), carol.String()}\n\tm := tdao.privdao.Members.GetMembers()\n\tif len(m) != len(expectedMembers) {\n\t\tt.Errorf(\"Expected %d members, got %d\", len(expectedMembers), len(m))\n\t}\n\n\tfor _, eMember := range expectedMembers {\n\t\tif !tdao.privdao.Members.IsMember(eMember) {\n\t\t\tt.Errorf(\"Expected member %s to be a member\", eMember)\n\t\t}\n\t}\n}\n\nfunc TestAddMemberProposal(cur realm, t *testing.T) {\n\tmembers := []Member{\n\t\t{alice.String(), []string{\"admin\"}},\n\t}\n\ttdao := newTestingDAO(0, cur, t, 0.2, members)\n\n\tif tdao.privdao.Members.IsMember(bob.String()) {\n\t\tt.Errorf(\"Expected member %s to not be a member\", bob.String())\n\t}\n\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\ttdao.instantExecute(0, cur, daokit.ProposalRequest{\n\t\tTitle:       \"My Proposal\",\n\t\tDescription: \"My Proposal Description\",\n\t\tAction: NewAddMemberAction(\u0026ActionAddMember{\n\t\t\tAddress: bob,\n\t\t\tRoles:   []string{\"admin\"},\n\t\t}),\n\t})\n\n\tif !tdao.privdao.Members.IsMember(bob.String()) {\n\t\tt.Errorf(\"Expected member %s to be a member\", bob.String())\n\t}\n\n\tif !tdao.privdao.Members.HasRole(bob.String(), \"admin\") {\n\t\tt.Errorf(\"Expected member %s to have role 'admin'\", bob.String())\n\t}\n\n\tproposalWithUnknownRole := daokit.ProposalRequest{\n\t\tTitle:       \"My Proposal\",\n\t\tDescription: \"My Proposal Description\",\n\t\tAction: NewAddMemberAction(\u0026ActionAddMember{\n\t\t\tAddress: bob,\n\t\t\tRoles:   []string{\"unknown\"},\n\t\t}),\n\t}\n\t// Cross-realm panic from instantExecute is not catchable by\n\t// defer/recover; use revive.\n\tif revive(func() { tdao.instantExecute(0, cur, proposalWithUnknownRole) }) == nil {\n\t\tt.Errorf(\"Expected panic, got none\")\n\t}\n}\n\nfunc TestRemoveMemberProposal(cur realm, t *testing.T) {\n\tmembers := []Member{\n\t\t{alice.String(), []string{\"admin\"}},\n\t\t{bob.String(), []string{\"admin\"}},\n\t}\n\ttdao := newTestingDAO(0, cur, t, 0.2, members)\n\n\tif !tdao.privdao.Members.IsMember(bob.String()) {\n\t\tt.Errorf(\"Expected member %s to be a member\", bob.String())\n\t}\n\n\tif !tdao.privdao.Members.HasRole(bob.String(), \"admin\") {\n\t\tt.Errorf(\"Expected member %s to have role 'admin'\", bob.String())\n\t}\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\ttdao.instantExecute(0, cur, daokit.ProposalRequest{\n\t\tTitle:       \"My Proposal\",\n\t\tDescription: \"My Proposal Description\",\n\t\tAction:      NewRemoveMemberAction(bob),\n\t})\n\n\tif tdao.privdao.Members.IsMember(bob.String()) {\n\t\tt.Errorf(\"Expected user %s to not be a member\", bob.String())\n\t}\n\n\tif tdao.privdao.Members.HasRole(bob.String(), \"admin\") {\n\t\tt.Errorf(\"Expected user %s to not have role 'admin'\", bob.String())\n\t}\n}\n\nfunc TestAddRoleToUserProposal(cur realm, t *testing.T) {\n\tmembers := []Member{\n\t\t{alice.String(), []string{\"admin\"}},\n\t\t{bob.String(), []string{}},\n\t}\n\ttdao := newTestingDAO(0, cur, t, 0.2, members)\n\n\tif tdao.privdao.Members.HasRole(bob.String(), \"admin\") {\n\t\tt.Errorf(\"Expected member %s to not have role 'admin'\", bob.String())\n\t}\n\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\ttdao.instantExecute(0, cur, daokit.ProposalRequest{\n\t\tTitle:       \"My Proposal\",\n\t\tDescription: \"My Proposal Description\",\n\t\tAction:      NewAssignRoleAction(\u0026ActionAssignRole{Address: bob, Role: \"admin\"}),\n\t})\n\tif !tdao.privdao.Members.HasRole(bob.String(), \"admin\") {\n\t\tt.Errorf(\"Expected member %s to have role 'admin'\", bob.String())\n\t}\n\n\t// Cross-realm panics aren't catchable by defer/recover; use revive\n\t// per failing case.\n\tif revive(func() {\n\t\ttdao.instantExecute(0, cur, daokit.ProposalRequest{\n\t\t\tTitle:       \"My Proposal\",\n\t\t\tDescription: \"My Proposal Description\",\n\t\t\tAction:      NewAssignRoleAction(\u0026ActionAssignRole{Address: alice, Role: \"unknown\"}),\n\t\t})\n\t}) == nil {\n\t\tt.Errorf(\"Expected panic on unknown role, got none\")\n\t}\n\n\tif revive(func() {\n\t\ttdao.instantExecute(0, cur, daokit.ProposalRequest{\n\t\t\tTitle:       \"My Proposal\",\n\t\t\tDescription: \"My Proposal Description\",\n\t\t\tAction:      NewAssignRoleAction(\u0026ActionAssignRole{Address: carol, Role: \"admin\"}),\n\t\t})\n\t}) == nil {\n\t\tt.Errorf(\"Expected panic on non-member, got none\")\n\t}\n\n\tif revive(func() {\n\t\ttdao.instantExecute(0, cur, daokit.ProposalRequest{\n\t\t\tTitle:       \"My Proposal\",\n\t\t\tDescription: \"My Proposal Description\",\n\t\t\tAction:      NewAssignRoleAction(\u0026ActionAssignRole{Address: bob, Role: \"admin\"}),\n\t\t})\n\t}) == nil {\n\t\tt.Errorf(\"Expected panic on already-assigned role, got none\")\n\t}\n}\n\nfunc TestRemoveRoleFromUserProposal(cur realm, t *testing.T) {\n\tmembers := []Member{\n\t\t{\n\t\t\talice.String(),\n\t\t\t[]string{\"admin\"},\n\t\t},\n\t\t{\n\t\t\tbob.String(),\n\t\t\t[]string{\"admin\"},\n\t\t},\n\t}\n\ttdao := newTestingDAO(0, cur, t, 0.2, members)\n\n\tif !tdao.privdao.Members.HasRole(bob.String(), \"admin\") {\n\t\tt.Errorf(\"Expected member %s to have role 'admin'\", bob.String())\n\t}\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\ttdao.instantExecute(0, cur, daokit.ProposalRequest{\n\t\tTitle:       \"My Proposal\",\n\t\tDescription: \"My Proposal Description\",\n\t\tAction:      NewUnassignRoleAction(\u0026ActionUnassignRole{Address: bob, Role: \"admin\"}),\n\t})\n\n\tif tdao.privdao.Members.HasRole(bob.String(), \"admin\") {\n\t\tt.Errorf(\"Expected member %s to not have role 'admin'\", bob.String())\n\t}\n\n\tproposalWithUnknowkRole := daokit.ProposalRequest{\n\t\tTitle:       \"My Proposal\",\n\t\tDescription: \"My Proposal Description\",\n\t\tAction:      NewUnassignRoleAction(\u0026ActionUnassignRole{Address: alice, Role: \"unknown\"}),\n\t}\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\tif revive(func() { tdao.instantExecute(0, cur, proposalWithUnknowkRole) }) == nil {\n\t\tt.Errorf(\"Expected panic on unknown role, got none\")\n\t}\n\n\tproposalWithNonMember := daokit.ProposalRequest{\n\t\tTitle:       \"My Proposal\",\n\t\tDescription: \"My Proposal Description\",\n\t\tAction:      NewUnassignRoleAction(\u0026ActionUnassignRole{Address: carol, Role: \"admin\"}),\n\t}\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\tif revive(func() { tdao.instantExecute(0, cur, proposalWithNonMember) }) == nil {\n\t\tt.Errorf(\"Expected panic on non-member, got none\")\n\t}\n\n\tproposalWithNonRole := daokit.ProposalRequest{\n\t\tTitle:       \"My Proposal\",\n\t\tDescription: \"My Proposal Description\",\n\t\tAction:      NewUnassignRoleAction(\u0026ActionUnassignRole{Address: bob, Role: \"admin\"}),\n\t}\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\tif revive(func() { tdao.instantExecute(0, cur, proposalWithNonRole) }) == nil {\n\t\tt.Errorf(\"Expected panic on already-unassigned role, got none\")\n\t}\n}\n\ntype testingDAOContext struct {\n\tdao                    *DAOWrapper\n\tprivdao                *DAOPrivate\n\tmockHandlerCalled      *bool\n\tmockHandler            daokit.ActionHandler\n\tmockProposalRequest    daokit.ProposalRequest\n\tunknownProposalRequest daokit.ProposalRequest\n}\n\nfunc newTestingDAO(_ int, rlm realm, t *testing.T, threshold float64, members []Member) *testingDAOContext {\n\troles := []RoleInfo{{Name: \"admin\", Description: \"Admin is the superuser\"}}\n\tmembersStore := NewMembersStore(roles, members)\n\tinitialCondition := daocond.MembersThreshold(threshold, membersStore.IsMember, membersStore.MembersCount)\n\n\tconf := \u0026Config{\n\t\tName:             \"My DAO\",\n\t\tDescription:      \"My DAO Description\",\n\t\tMembers:          membersStore,\n\t\tInitialCondition: initialCondition,\n\t\tGetProfileString: func(addr address, field, def string) string { return \"\" },\n\t}\n\n\tdaoRealm := testing.NewCodeRealm(\"gno.land/r/testing/daorealm\")\n\ttesting.SetOriginCaller(alice)\n\ttesting.SetRealm(daoRealm)\n\tpubdao := New(conf, rlm)\n\tdao := pubdao.Private()\n\n\tmockHandlerCalled := false\n\tmockHandler := daokit.NewActionHandler(\"valid\", func(_ interface{}) { mockHandlerCalled = true })\n\tdao.Core.Resources.Set(\u0026daokit.Resource{\n\t\tHandler:   mockHandler,\n\t\tCondition: daocond.MembersThreshold(0.2, dao.Members.IsMember, dao.Members.MembersCount),\n\t})\n\n\tmockProposalRequest := daokit.ProposalRequest{\n\t\tTitle:       \"My Proposal\",\n\t\tDescription: \"My Proposal Description\",\n\t\tAction:      daokit.NewAction(\"valid\", nil),\n\t}\n\n\tunknownProposalRequest := daokit.ProposalRequest{\n\t\tTitle:       \"My Proposal\",\n\t\tDescription: \"My Proposal Description\",\n\t\tAction:      daokit.NewAction(\"unknown\", nil),\n\t}\n\n\treturn \u0026testingDAOContext{\n\t\tdao:                    pubdao,\n\t\tprivdao:                dao,\n\t\tmockHandlerCalled:      \u0026mockHandlerCalled,\n\t\tmockHandler:            mockHandler,\n\t\tmockProposalRequest:    mockProposalRequest,\n\t\tunknownProposalRequest: unknownProposalRequest,\n\t}\n}\n\n// Crossing wrappers for dao.X calls.\n\nfunc (c *testingDAOContext) propose(_ int, rlm realm, req daokit.ProposalRequest) (id uint64) {\n\tfunc(cur realm) { id = c.dao.Propose(0, cur, req) }(cross(rlm))\n\treturn\n}\n\nfunc (c *testingDAOContext) vote(_ int, rlm realm, id uint64, v daocond.Vote) {\n\tfunc(cur realm) { c.dao.Vote(0, cur, id, v) }(cross(rlm))\n}\n\nfunc (c *testingDAOContext) execute(_ int, rlm realm, id uint64) {\n\tfunc(cur realm) { c.dao.Execute(0, cur, id) }(cross(rlm))\n}\n\nfunc (c *testingDAOContext) instantExecute(_ int, rlm realm, req daokit.ProposalRequest) (id uint64) {\n\tfunc(cur realm) { id = c.dao.InstantExecute(0, cur, req) }(cross(rlm))\n\treturn\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/samcrew/basedao\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1kfd9f5zlvcvy6aammcmqswa7cyjpu2nyt9qfen\"\n"},{"name":"members.gno","body":"package basedao\n\nimport (\n\t\"chain\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/onbloc/json/v0\"\n)\n\ntype MembersStore struct {\n\tRoles   *avl.Tree // role name -\u003e *Role\n\tMembers *avl.Tree // string -\u003e *avl.Tree [roles -\u003e struct{}]\n}\n\ntype Member struct {\n\tAddress string\n\tRoles   []string\n}\n\ntype RoleInfo struct {\n\tName        string\n\tDescription string\n}\n\ntype Role struct {\n\tName        string\n\tDescription string\n\tMembers     *avl.Tree // string -\u003e struct{}\n}\n\nconst EventAddMember = \"BaseDAOAddMember\"\nconst EventRemoveMember = \"BaseDAORemoveMember\"\n\nfunc NewMembersStore(initialRoles []RoleInfo, initialMembers []Member) *MembersStore {\n\tres := \u0026MembersStore{\n\t\tRoles:   avl.NewTree(),\n\t\tMembers: avl.NewTree(),\n\t}\n\tres.setRoles(initialRoles)\n\tres.setMembers(initialMembers)\n\treturn res\n}\n\nfunc (m *MembersStore) HasRole(member string, role string) bool {\n\trolesRaw := m.Members.Get(member)\n\tif rolesRaw == nil {\n\t\treturn false\n\t}\n\troles, ok := rolesRaw.(*avl.Tree)\n\tif !ok {\n\t\treturn false\n\t}\n\treturn roles.Has(role)\n}\n\nfunc (m *MembersStore) IsMember(member string) bool {\n\treturn m.Members.Has(member)\n}\n\nfunc (m *MembersStore) RoleInfo(role string) RoleInfo {\n\troleDataRaw := m.Roles.Get(role)\n\tif roleDataRaw == nil {\n\t\tpanic(\"role does not exist\")\n\t}\n\troleData, ok := roleDataRaw.(*Role)\n\tif !ok {\n\t\tpanic(\"a value of memberstore.roles is not a Role, should not happen\")\n\t}\n\treturn RoleInfo{\n\t\tName:        roleData.Name,\n\t\tDescription: roleData.Description,\n\t}\n}\n\nfunc (m *MembersStore) MembersCount() uint64 {\n\treturn uint64(m.Members.Size())\n}\n\nfunc (m *MembersStore) GetMembers() []string {\n\tmembers := make([]string, 0, m.Members.Size())\n\tm.Members.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tmembers = append(members, key)\n\t\treturn false\n\t})\n\treturn members\n}\n\nfunc (m *MembersStore) GetRoles() []string {\n\ti := 0\n\tres := make([]string, m.Roles.Size())\n\tm.Roles.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tres[i] = key\n\t\ti++\n\t\treturn false\n\t})\n\treturn res\n}\n\nfunc (m *MembersStore) GetMemberRoles(member string) []string {\n\trolesRaw := m.Members.Get(member)\n\tif rolesRaw == nil {\n\t\treturn []string{}\n\t}\n\troles, ok := rolesRaw.(*avl.Tree)\n\tif !ok {\n\t\treturn []string{}\n\t}\n\ti := 0\n\tres := make([]string, roles.Size())\n\troles.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tres[i] = key\n\t\ti++\n\t\treturn false\n\t})\n\treturn res\n}\n\nfunc (m *MembersStore) CountMemberRoles(member string) int {\n\trolesRaw := m.Members.Get(member)\n\tif rolesRaw == nil {\n\t\treturn 0\n\t}\n\troles, ok := rolesRaw.(*avl.Tree)\n\tif !ok {\n\t\treturn 0\n\t}\n\treturn roles.Size()\n}\n\nfunc (m *MembersStore) GetMembersWithRole(role string) []string {\n\troleDataRaw := m.Roles.Get(role)\n\tif roleDataRaw == nil {\n\t\treturn []string{}\n\t}\n\troleData, ok := roleDataRaw.(*Role)\n\tif !ok {\n\t\treturn []string{}\n\t}\n\ti := 0\n\tres := make([]string, roleData.Members.Size())\n\troleData.Members.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tres[i] = key\n\t\ti++\n\t\treturn false\n\t})\n\treturn res\n}\n\nfunc (m *MembersStore) CountMembersWithRole(role string) uint32 {\n\treturn uint32(len(m.GetMembersWithRole(role)))\n}\n\nfunc (m *MembersStore) setRoles(roles []RoleInfo) {\n\tfor _, role := range roles {\n\t\tm.AddRole(role)\n\t}\n}\n\nfunc (m *MembersStore) setMembers(members []Member) {\n\tfor _, member := range members {\n\t\tm.AddMember(member.Address, member.Roles)\n\t}\n}\n\nfunc (m *MembersStore) AddMember(member string, roles []string) {\n\tif m.IsMember(member) {\n\t\tpanic(\"member already exists\")\n\t}\n\n\tmembersRoles := avl.NewTree()\n\tfor _, role := range roles {\n\t\tif !m.Roles.Has(role) {\n\t\t\tpanic(\"role: \" + role + \" does not exist\")\n\t\t}\n\t\tmembersRoles.Set(role, struct{}{})\n\t}\n\tm.Members.Set(member, membersRoles)\n\n\tchain.Emit(EventAddMember,\n\t\t\"address\", member,\n\t)\n}\n\nfunc (m *MembersStore) RemoveMember(member string) {\n\tif !m.IsMember(member) {\n\t\tpanic(\"member does not exist\")\n\t}\n\n\tmemberRolesRaw := m.Members.Get(member)\n\tif memberRolesRaw == nil {\n\t\tpanic(\"should not happen\")\n\t}\n\tmemberRoles, ok := memberRolesRaw.(*avl.Tree)\n\tif !ok {\n\t\tpanic(\"a value of memberstore.members is not an avl.Tree, should not happen\")\n\t}\n\n\tmemberRoles.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\troleRaw := m.Roles.Get(key)\n\t\tif roleRaw == nil {\n\t\t\treturn false\n\t\t}\n\t\trole, ok := roleRaw.(*Role)\n\t\tif !ok {\n\t\t\tpanic(\"a value of memberstore.roles is not a Role, should not happen\")\n\t\t}\n\t\trole.Members.Remove(role.Name)\n\t\treturn false\n\t})\n\tm.Members.Remove(member)\n\n\tchain.Emit(EventRemoveMember,\n\t\t\"address\", member,\n\t)\n}\n\nfunc (m *MembersStore) AddRole(role RoleInfo) {\n\tif m.Roles.Has(role.Name) {\n\t\tpanic(\"role already exists\")\n\t}\n\n\troleData := \u0026Role{\n\t\tName:        role.Name,\n\t\tDescription: role.Description,\n\t\tMembers:     avl.NewTree(),\n\t}\n\n\tm.Roles.Set(role.Name, roleData)\n}\n\nfunc (m *MembersStore) RemoveRole(role string) {\n\troleDataRaw := m.Roles.Get(role)\n\tif roleDataRaw == nil {\n\t\tpanic(\"role does not exist\")\n\t}\n\troleData, ok := roleDataRaw.(*Role)\n\tif !ok {\n\t\tpanic(\"a value of memberstore.roles is not a Role, should not happen\")\n\t}\n\n\troleData.Members.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tmemberRaw := m.Members.Get(key)\n\t\tif memberRaw == nil {\n\t\t\treturn false\n\t\t}\n\t\tmember, ok := memberRaw.(*avl.Tree)\n\t\tif !ok {\n\t\t\tpanic(\"a value of memberstore.members is not an avl.Tree, should not happen\")\n\t\t}\n\t\tmember.Remove(role)\n\t\treturn false\n\t})\n\tm.Roles.Remove(role)\n}\n\nfunc (m *MembersStore) AddRoleToMember(member string, role string) {\n\tif !m.IsMember(member) {\n\t\tpanic(\"member does not exist\")\n\t}\n\tif !m.Roles.Has(role) {\n\t\tpanic(\"role \" + role + \" does not exist\")\n\t}\n\tif m.HasRole(member, role) {\n\t\tpanic(\"member already has the role\")\n\t}\n\n\tmemberRolesRaw := m.Members.Get(member)\n\tif memberRolesRaw == nil {\n\t\tpanic(\"should not happen\")\n\t}\n\tmemberRoles, ok := memberRolesRaw.(*avl.Tree)\n\tif !ok {\n\t\tpanic(\"a value of memberstore.members is not an avl.Tree, should not happen\")\n\t}\n\n\troleDataRaw := m.Roles.Get(role)\n\tif roleDataRaw == nil {\n\t\tpanic(\"should not happen\")\n\t}\n\troleData, ok := roleDataRaw.(*Role)\n\tif !ok {\n\t\tpanic(\"a value of memberstore.roles is not a Role, should not happen\")\n\t}\n\n\troleData.Members.Set(member, struct{}{})\n\tmemberRoles.Set(role, struct{}{})\n}\n\nfunc (m *MembersStore) RemoveRoleFromMember(member string, role string) {\n\tif !m.IsMember(member) {\n\t\tpanic(\"member does not exist\")\n\t}\n\tif !m.Roles.Has(role) {\n\t\tpanic(\"role \" + role + \" does not exist\")\n\t}\n\tif !m.HasRole(member, role) {\n\t\tpanic(\"member does not have the role\")\n\t}\n\n\tmemberRolesRaw := m.Members.Get(member)\n\tif memberRolesRaw == nil {\n\t\tpanic(\"should not happen\")\n\t}\n\tmemberRoles, ok := memberRolesRaw.(*avl.Tree)\n\tif !ok {\n\t\tpanic(\"a value of memberstore.members is not an avl.Tree, should not happen\")\n\t}\n\n\troleDataRaw := m.Roles.Get(role)\n\tif roleDataRaw == nil {\n\t\tpanic(\"should not happen\")\n\t}\n\troleData, ok := roleDataRaw.(*Role)\n\tif !ok {\n\t\tpanic(\"a value of memberstore.roles is not a Role, should not happen\")\n\t}\n\n\tmemberRoles.Remove(role)\n\troleData.Members.Remove(member)\n}\n\nfunc (m *MembersStore) GetMembersJSON() string {\n\t// XXX: replace with protoc-gen-gno\n\tmembers := []*json.Node{}\n\tfor _, memberID := range m.GetMembers() {\n\t\troles := []*json.Node{}\n\t\tfor _, role := range m.GetMemberRoles(memberID) {\n\t\t\troles = append(roles, json.StringNode(\"\", role))\n\t\t}\n\t\tmembers = append(members, json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\t\"address\": json.StringNode(\"\", memberID),\n\t\t\t\"roles\":   json.ArrayNode(\"\", roles),\n\t\t}))\n\t}\n\tnode := json.ArrayNode(\"\", members)\n\tbz, err := json.Marshal(node)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn string(bz)\n}\n"},{"name":"render.gno","body":"package basedao\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\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/p/samcrew/daokit\"\n)\n\n// WELL KNOWN PATHS\nconst (\n\tHOME_PATH             = \"\"\n\tHOME_NO_PROFILE_PATH  = \"noprofile\"\n\tCONFIG_PATH           = \"config\"\n\tPROPOSAL_HISTORY_PATH = \"history\"\n\tMEMBER_DETAIL_PATH    = \"member/{address}\"\n\tPROPOSAL_DETAIL_PATH  = \"proposal/{id}\"\n\tFALLBACK_DISPLAY_NAME = \"Anon\"\n)\n\nfunc (d *DAOPrivate) initRenderingRouter() {\n\tif d.RenderRouter == nil {\n\t\td.RenderRouter = mux.NewRouter()\n\t}\n}\n\nfunc (d *DAOPrivate) InitDefaultRendering() {\n\td.RenderRouter.HandleFunc(HOME_PATH, d.MuxHomePage)\n\td.RenderRouter.HandleFunc(HOME_NO_PROFILE_PATH, d.MuxHomePage)\n\td.RenderRouter.HandleFunc(CONFIG_PATH, d.MuxConfigPage)\n\td.RenderRouter.HandleFunc(PROPOSAL_HISTORY_PATH, d.MuxProposalHistoryPage)\n\td.RenderRouter.HandleFunc(MEMBER_DETAIL_PATH, d.MuxMemberDetailPage)\n\td.RenderRouter.HandleFunc(PROPOSAL_DETAIL_PATH, d.MuxProposalDetailPage)\n}\n\nfunc (d *DAOPrivate) Render(path string) string {\n\treturn d.RenderRouter.Render(path)\n}\n\n// DEFAULT_HANDLERS\n\nfunc (d *DAOPrivate) MuxHomePage(res *mux.ResponseWriter, req *mux.Request) {\n\tres.Write(d.HomePageView())\n}\n\nfunc (d *DAOPrivate) MuxConfigPage(res *mux.ResponseWriter, req *mux.Request) {\n\tres.Write(d.ConfigPageView())\n}\n\nfunc (d *DAOPrivate) MuxProposalHistoryPage(res *mux.ResponseWriter, req *mux.Request) {\n\tres.Write(d.ProposalHistoryPageView())\n}\n\nfunc (d *DAOPrivate) MuxMemberDetailPage(res *mux.ResponseWriter, req *mux.Request) {\n\tres.Write(d.MemberDetailPageView(req.GetVar(\"address\")))\n}\n\nfunc (d *DAOPrivate) MuxProposalDetailPage(res *mux.ResponseWriter, req *mux.Request) {\n\tid, err := strconv.ParseUint(req.GetVar(\"id\"), 10, 64)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tres.Write(d.ProposalDetailPageView(id))\n}\n\n// PUBLIC PAGES\n\nfunc (d *DAOPrivate) HomePageView() string {\n\treturn d.HomeProfileHeaderView(false) +\n\t\td.HomeMembersView() +\n\t\td.HomeProposalsView()\n}\n\nfunc (d *DAOPrivate) ConfigPageView() string {\n\treturn d.ConfigHeaderView() +\n\t\td.ConfigRolesView() +\n\t\td.ConfigResourcesView()\n}\n\nfunc (d *DAOPrivate) ProposalHistoryPageView() string {\n\treturn d.ProposalHistoryHeaderView() +\n\t\td.ProposalHistoryView()\n}\n\nfunc (d *DAOPrivate) MemberDetailPageView(address string) string {\n\treturn d.MemberDetailHeaderView() +\n\t\td.MemberDetailView(address)\n}\n\nfunc (d *DAOPrivate) ProposalDetailPageView(idu uint64) string {\n\treturn d.ProposalDetailHeaderView() +\n\t\td.ProposalDetailView(idu)\n}\n\n// --- PUBLIC COMPONENT VIEWS ---\n\n// HOME PAGE COMPONENTS VIEWS\n\nfunc (d *DAOPrivate) HomeProfileHeaderView(noprofile bool) string {\n\ts := \"\"\n\tname := d.GetProfileString(d.RealmAddress, \"DisplayName\", \"DAO\")\n\tdescription := d.GetProfileString(d.RealmAddress, \"Bio\", \"Created with daokit\")\n\tpkgPath := d.RealmPkgPath\n\tlinkPath := getLinkPath(pkgPath)\n\n\tif !noprofile {\n\t\ts += ufmt.Sprintf(\"# %s\\n\\n\", name)\n\t\timageURI := d.GetProfileString(d.RealmAddress, \"Avatar\", \"\")\n\t\tif imageURI != \"\" {\n\t\t\ts += ufmt.Sprintf(\"![Image](%s)\\n\\n\", imageURI)\n\t\t}\n\t\ts += ufmt.Sprintf(\"%s\\n\\n\", description)\n\t}\n\n\ts += ufmt.Sprintf(\"\u003e Realm address: %s\\n\\n\", d.RealmAddress)\n\ts += ufmt.Sprintf(\"Discover more about this DAO on the [configuration page ⚙️](%s:%s)\\n\\n\", linkPath, CONFIG_PATH)\n\ts += ufmt.Sprintf(\"\\n--------------------------------\\n\")\n\n\treturn s\n}\n\nfunc (d *DAOPrivate) HomeMembersView() string {\n\tpkgPath := d.RealmPkgPath\n\tlinkPath := getLinkPath(pkgPath)\n\ts := \"\"\n\ts += ufmt.Sprintf(\"## Members 👤 \\n\\n\")\n\ti := 1\n\td.Members.Members.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\ts += ufmt.Sprintf(\"- **Member %d: [%s](%s:%s/%s)**\\n\\n\", i, key, linkPath, \"member\", key)\n\t\ts += ufmt.Sprintf(\"\t- **Profile:** %s\\n\", d.GetProfileString(address(key), \"DisplayName\", FALLBACK_DISPLAY_NAME))\n\t\ts += ufmt.Sprintf(\" \t- **Roles:** %s\\n\\n\", strings.Join(d.Members.GetMemberRoles(key), \", \"))\n\t\ti += 1\n\t\treturn false\n\t})\n\ts += ufmt.Sprintf(\"\u003e You can find more information about a member by clicking on their address\\n\\n\")\n\ts += ufmt.Sprintf(\"\\n--------------------------------\\n\")\n\treturn s\n}\n\nfunc (d *DAOPrivate) HomeProposalsView() string {\n\tpkgPath := d.RealmPkgPath\n\tlinkPath := getLinkPath(pkgPath)\n\ts := \"\"\n\ts += ufmt.Sprintf(\"## Proposals 🗳️ \\n\\n\")\n\ti := 0\n\td.Core.Proposals.Tree.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tproposal := value.(*daokit.Proposal)\n\t\tif proposal.Status != daokit.ProposalStatusOpen {\n\t\t\treturn false\n\t\t}\n\t\tid, err := seqid.FromString(key)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\ts += ufmt.Sprintf(\"- **Proposal %d: [%s](%s:%s/%d)**\\n\\n\", uint64(id), proposal.Title, linkPath, \"proposal\", uint64(id))\n\t\ti += 1\n\t\treturn false\n\t})\n\tif i == 0 {\n\t\ts += ufmt.Sprintf(\"\\t⚠️ There are no running proposals at the moment\\n\\n\")\n\t}\n\ts += ufmt.Sprintf(\"\u003e See the [proposal history 📜](%s:%s) for more information\\n\\n\", linkPath, PROPOSAL_HISTORY_PATH)\n\ts += ufmt.Sprintf(\"\\n--------------------------------\\n\")\n\treturn s\n}\n\n// CONFIG PAGE COMPONENTS VIEWS\n\nfunc (d *DAOPrivate) ConfigHeaderView() string {\n\tname := d.GetProfileString(d.RealmAddress, \"DisplayName\", \"DAO\")\n\ts := \"\"\n\ts += ufmt.Sprintf(\"# %s - Config ⚙️\\n\\n\", name)\n\ts += ufmt.Sprintf(\"\\n--------------------------------\\n\")\n\treturn s\n}\n\nfunc (d *DAOPrivate) ConfigRolesView() string {\n\troles := d.Members.GetRoles()\n\ts := \"\"\n\ts += ufmt.Sprintf(\"## Roles 🏷️\\n\\n\")\n\tfor _, role := range roles {\n\t\ts += ufmt.Sprintf(\"- %s\\n\\n\", role)\n\t\tinfo := d.Members.RoleInfo(role)\n\t\ts += ufmt.Sprintf(\"  %s\\n\\n\", info.Description)\n\t}\n\ts += ufmt.Sprintf(\"\\n--------------------------------\\n\")\n\treturn s\n}\n\nfunc (d *DAOPrivate) ConfigResourcesView() string {\n\ts := \"\"\n\ts += ufmt.Sprintf(\"## Resources 📦\\n\\n\")\n\ti := 1\n\td.Core.Resources.Tree.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tresource := value.(*daokit.Resource)\n\t\ts += ufmt.Sprintf(\"- **Resource #%d: %s**\\n\\n\", i, key)\n\t\t// TODO: add doc to handler and print here\n\t\ts += ufmt.Sprintf(\"  - **Name:** %s\\n\", resource.DisplayName)\n\t\ts += ufmt.Sprintf(\"  - **Description:** %s\\n\", resource.Description)\n\t\ts += ufmt.Sprintf(\"  - **Condition:** %s\\n\\n\", resource.Condition.Render())\n\t\ti += 1\n\t\treturn false\n\t})\n\ts += ufmt.Sprintf(\"\\n--------------------------------\\n\")\n\treturn s\n}\n\n// HISTORY PAGE COMPONENTS VIEWS\n\nfunc (d *DAOPrivate) ProposalHistoryHeaderView() string {\n\tname := d.GetProfileString(d.RealmAddress, \"DisplayName\", \"DAO\")\n\ts := \"\"\n\ts += ufmt.Sprintf(\"# %s - Proposal History\\n\\n\", name)\n\ts += ufmt.Sprintf(\"\\n--------------------------------\\n\")\n\treturn s\n}\n\nfunc (d *DAOPrivate) ProposalHistoryView() string {\n\tpkgPath := d.RealmPkgPath\n\tlinkPath := getLinkPath(pkgPath)\n\ts := \"\"\n\ts += ufmt.Sprintf(\"## Proposals 🗳️\\n\\n\")\n\ti := 1\n\td.Core.Proposals.Tree.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tproposal, ok := value.(*daokit.Proposal)\n\t\tif !ok {\n\t\t\tpanic(\"unexpected invalid proposal type\")\n\t\t}\n\t\tid, err := seqid.FromString(key)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\ts += ufmt.Sprintf(\"- **Proposal %d: [%s](%s:%s/%d) - %s**\\n\\n\", uint64(id), proposal.Title, linkPath, \"proposal\", uint64(id), proposal.Status)\n\t\ti += 1\n\t\treturn false\n\t})\n\ts += ufmt.Sprintf(\"[Add a new proposal 🗳️](%s$help)\\n\\n\", linkPath)\n\ts += ufmt.Sprintf(\"\\n--------------------------------\\n\")\n\treturn s\n}\n\n// MEMBER DETAIL PAGE COMPONENTS VIEWS\n\nfunc (d *DAOPrivate) MemberDetailHeaderView() string {\n\tname := d.GetProfileString(d.RealmAddress, \"DisplayName\", \"DAO\")\n\ts := \"\"\n\ts += ufmt.Sprintf(\"# %s - Member Detail\\n\\n\", name)\n\ts += ufmt.Sprintf(\"\\n--------------------------------\\n\")\n\treturn s\n}\n\nfunc (d *DAOPrivate) MemberDetailView(addr string) string {\n\tpkgPath := d.RealmPkgPath\n\tlinkPath := getLinkPath(pkgPath)\n\troles := d.Members.GetMemberRoles(addr)\n\tdisplayName := d.GetProfileString(address(addr), \"DisplayName\", FALLBACK_DISPLAY_NAME)\n\tbio := d.GetProfileString(address(addr), \"Bio\", \"No bio\")\n\tpp := d.GetProfileString(address(addr), \"Avatar\", \"\")\n\ts := \"\"\n\ts += ufmt.Sprintf(\"## Profile 👤\\n\\n\")\n\ts += ufmt.Sprintf(\"- **Display Name:** %s\\n\\n\", displayName)\n\ts += ufmt.Sprintf(\"- **Bio:** %s\\n\\n\", bio)\n\tif pp != \"\" {\n\t\ts += ufmt.Sprintf(\"![Avatar](%s)\\n\\n\", pp)\n\t}\n\ts += ufmt.Sprintf(\"## Roles 🏷️\\n\\n\")\n\tfor _, role := range roles {\n\t\ts += ufmt.Sprintf(\"- %s\\n\\n\", role)\n\t}\n\ts += ufmt.Sprintf(\"\u003e Learn more about the roles on the [configuration page ⚙️](%s:%s)\\n\\n\", linkPath, CONFIG_PATH)\n\ts += ufmt.Sprintf(\"\\n--------------------------------\\n\")\n\treturn s\n}\n\n// PROPOSAL DETAIL PAGE COMPONENTS VIEWS\n\nfunc (d *DAOPrivate) ProposalDetailHeaderView() string {\n\tname := d.GetProfileString(d.RealmAddress, \"DisplayName\", \"DAO\")\n\ts := \"\"\n\ts += ufmt.Sprintf(\"# %s - Proposal Detail\\n\\n\", name)\n\ts += ufmt.Sprintf(\"\\n--------------------------------\\n\")\n\treturn s\n}\n\nfunc (d *DAOPrivate) ProposalDetailView(idu uint64) string {\n\tpkgPath := d.RealmPkgPath\n\tlinkPath := getLinkPath(pkgPath)\n\tid := seqid.ID(idu)\n\tproposal := d.Core.Proposals.GetProposal(uint64(id))\n\ts := \"\"\n\ts += ufmt.Sprintf(\"## Title - %s 📜\\n\\n\", proposal.Title)\n\ts += ufmt.Sprintf(\"## Description 📝\\n\\n%s\\n\\n\", proposal.Description)\n\ts += ufmt.Sprintf(\"## Resource - %s 📦\\n\\n\", proposal.Action.Type())\n\tresource := d.Core.Resources.Get(proposal.Action.Type())\n\ts += ufmt.Sprintf(\"  - **Name:** %s\\n\", resource.DisplayName)\n\ts += ufmt.Sprintf(\"  - **Description:** %s\\n\", resource.Description)\n\ts += ufmt.Sprintf(\"  - **Condition:** %s\\n\\n\", resource.Condition.Render())\n\ts += (\"---\\n\\n\")\n\ts += proposal.Action.String() + \"\\n\\n\"\n\ts += (\"---\\n\\n\")\n\tif proposal.Status == daokit.ProposalStatusOpen {\n\t\ts += ufmt.Sprintf(\"## Status - Open 🟡\\n\\n\")\n\t\ts += ufmt.Sprintf(\"[Vote on this proposal 🗳️](%s$help)\\n\\n\", linkPath)\n\t} else if proposal.Status == daokit.ProposalStatusPassed {\n\t\ts += ufmt.Sprintf(\"## Status - Passed 🟢\\n\\n\")\n\t\ts += ufmt.Sprintf(\"[Execute this proposal 🗳️](%s$help)\\n\\n\", linkPath)\n\t} else if proposal.Status == daokit.ProposalStatusExecuted {\n\t\ts += ufmt.Sprintf(\"## Status - Executed ✅\\n\\n\")\n\t} else {\n\t\ts += ufmt.Sprintf(\"## Status - Closed 🔴\\n\\n\")\n\t}\n\ts += ufmt.Sprintf(\"\u003e proposed by %s 👤\\n\\n\", proposal.ProposerID)\n\ts += ufmt.Sprintf(\"\\n--------------------------------\\n\")\n\ts += ufmt.Sprintf(\"## Votes 🗳️\\n\\n%s\\n\\n\", proposal.Condition.RenderWithVotes(proposal.Ballot))\n\ts += ufmt.Sprintf(\"\\n--------------------------------\\n\")\n\treturn s\n}\n\nfunc getLinkPath(pkgPath string) string {\n\tslashIdx := strings.IndexRune(pkgPath, '/')\n\tif slashIdx != 1 {\n\t\treturn pkgPath[slashIdx:]\n\t}\n\treturn \"\"\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"gCOrU6EQZCJrz+Y//AsvmzdwjwvyMG7JgObc55wy7dIAVW50ZMoifndgIojYANOgD5EOGqxPhYr6jkh/SqXwFw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1kfd9f5zlvcvy6aammcmqswa7cyjpu2nyt9qfen","package":{"name":"gauge","path":"gno.land/p/samcrew/gauge","files":[{"name":"README.md","body":"# `gauge` - SVG progress bars\n\nGenerate progress bars and gauges as SVG images with customizable styling and labels.\n\n## Usage\n\n```go\n// Basic gauge with default config\ngauge := gauge.Render(75, 100, \"Progress\", \"#4caf50\", gauge.DefaultConfig)\n\n// Custom configuration\nconfig := gauge.Config{\n    PercentOnly:  true,\n    Width:        400,\n    CanvasHeight: 40,\n    FontSize:     18,\n    PaddingH:     10,\n}\ngauge := gauge.Render(33, 50, \"Loading\", \"#2196f3\", config)\n\n// Different styles\nprogress := gauge.Render(8, 10, \"Health\", \"#f44336\", gauge.DefaultConfig)\n```\n\n## API Reference\n\n```go\ntype Config struct {\n    PercentOnly  bool // Only display the percentage on the right side\n    Width        int  // Width of the gauge in pixels\n    CanvasHeight int  // Height of the gauge in pixels\n    FontSize     int  // Font size of the text in pixels\n    PaddingH     int  // Horizontal padding (for the text) in pixels\n}\n\nvar DefaultConfig = Config{\n    PercentOnly:  false,\n    Width:        300,\n    CanvasHeight: 30,\n    FontSize:     16,\n    PaddingH:     6,\n}\n\n// value: Current value (must be ≤ total)\n// total: Maximum value (must be \u003e 0)\n// label: Text label displayed on the left\n// color: Fill color (hex format, e.g., \"#4caf50\")\n// config: Configuration options\n// Returns: SVG string as markdown image\nfunc Render(value int, total int, label string, color string, config Config) string\n```\n\n## Live Example\n\n- [/r/docs/charts:gauge](/r/docs/charts:gauge)\n"},{"name":"gauge.gno","body":"// Package gauge provides a simple way to render a gauge bar in SVG format.\npackage gauge\n\nimport (\n\t\"gno.land/p/nt/svg/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\ntype Config struct {\n\tPercentOnly  bool\n\tWidth        int\n\tCanvasHeight int\n\tFontSize     int\n\tPaddingH     int\n}\n\nvar DefaultConfig = Config{\n\tPercentOnly:  false,\n\tWidth:        300,\n\tCanvasHeight: 30,\n\tFontSize:     16,\n\tPaddingH:     6,\n}\n\n// Render generates an SVG gauge bar.\n// Parameters:\n// - value: current value (must be \u003c= total)\n// - total: maximum value (must be \u003e 0)\n// - label: text label to display on the left side of the gauge\n// - color: color of the filled part of the gauge (e.g., \"#4caf50\")\n// - config: configuration options:\n//   - PercentOnly: Only display the percentage on the right side; otherwise, display \"value / total · percentage\"\n//   - Width: Width of the gauge in pixels\n//   - CanvasHeight: Height of the gauge in pixels\n//   - FontSize: Font size of the text in pixels\n//   - PaddingH: Horizontal padding (for the text) in pixels\n//\n// Example usage: gauge.Render(30, 100, \"Progress\", \"#4caf50\", \u0026gauge.Config{PercentOnly: true, Width: 400})\nfunc Render(value int, total int, label string, color string, config Config) string {\n\tif total \u003c= 0 {\n\t\treturn \"jauge fails: total must be greater than 0\"\n\t}\n\tif value \u003c= 0 {\n\t\treturn \"jauge fails: value must be greater than 0\"\n\t}\n\tif value \u003e total {\n\t\treturn \"jauge fails: value cannot be greater than total\"\n\t}\n\n\tcanvasWidth := config.Width\n\tcanvasHeight := config.CanvasHeight\n\tfontSize := config.FontSize\n\tpaddingH := config.PaddingH\n\tcanvas := svg.NewCanvas(canvasWidth, canvasHeight)\n\n\tratio := 0.0\n\tif total \u003e 0 {\n\t\tratio = float64(value) / float64(total)\n\t}\n\n\tleftText := label\n\trightText := ufmt.Sprintf(\"%d / %d · %0.0f%%\", value, total, ratio*100)\n\tif config.PercentOnly {\n\t\trightText = ufmt.Sprintf(\"%0.0f%%\", ratio*100)\n\t}\n\n\t// Background\n\tcanvas.Append(svg.Rectangle{\n\t\tX:      0,\n\t\tY:      0,\n\t\tWidth:  canvasWidth,\n\t\tHeight: canvasHeight,\n\t\tFill:   \"#e0e0e0\",\n\t\tRX:     2,\n\t\tRY:     2,\n\t})\n\t// Filled bar\n\tcanvas.Append(svg.Rectangle{\n\t\tX:      0,\n\t\tY:      0,\n\t\tWidth:  int(ratio * float64(canvasWidth)),\n\t\tHeight: canvasHeight,\n\t\tFill:   color,\n\t\tRX:     2,\n\t\tRY:     2,\n\t})\n\t// Left text (label)\n\tcanvas.Append(svg.Text{\n\t\tX:    paddingH,\n\t\tY:    canvasHeight / 2,\n\t\tText: leftText,\n\t\tFill: \"#000000\",\n\t\tAttr: svg.BaseAttrs{\n\t\t\tStyle: ufmt.Sprintf(\n\t\t\t\t\"font-family:'Inter var',sans-serif;font-size:%dpx;dominant-baseline:middle;text-anchor:start;\",\n\t\t\t\tfontSize,\n\t\t\t),\n\t\t},\n\t})\n\t// Right text (ratio + %)\n\tcanvas.Append(svg.Text{\n\t\tX:    canvasWidth - paddingH,\n\t\tY:    canvasHeight / 2,\n\t\tText: rightText,\n\t\tFill: \"#000000\",\n\t\tAttr: svg.BaseAttrs{\n\t\t\tStyle: ufmt.Sprintf(\n\t\t\t\t\"font-family:'Inter var',sans-serif;font-size:%dpx;dominant-baseline:middle;text-anchor:end;\",\n\t\t\t\tfontSize,\n\t\t\t),\n\t\t},\n\t})\n\n\treturn canvas.Render(\"Jauge\")\n}\n"},{"name":"gauge_test.gno","body":"package gauge\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestRender(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tvalue        int\n\t\ttotal        int\n\t\tlabel        string\n\t\tcolor        string\n\t\tpercentOnly  bool\n\t\twidth        int\n\t\tcanvasHeight int\n\t\tfontSize     int\n\t\tpaddingH     int\n\t\twantSubstr   string\n\t}{\n\t\t{\n\t\t\tname:         \"fails when value \u003c= 0\",\n\t\t\tvalue:        0,\n\t\t\ttotal:        5,\n\t\t\tlabel:        \"Yes\",\n\t\t\tcolor:        \"#a6da95\",\n\t\t\tpercentOnly:  false,\n\t\t\twidth:        100,\n\t\t\tcanvasHeight: 30,\n\t\t\tfontSize:     16,\n\t\t\tpaddingH:     6,\n\t\t\twantSubstr:   \"jauge fails: value must be greater than 0\",\n\t\t},\n\t\t{\n\t\t\tname:         \"fails when total \u003c= 0\",\n\t\t\tvalue:        5,\n\t\t\ttotal:        0,\n\t\t\tlabel:        \"Yes\",\n\t\t\tcolor:        \"#a6da95\",\n\t\t\tpercentOnly:  false,\n\t\t\twidth:        100,\n\t\t\tcanvasHeight: 30,\n\t\t\tfontSize:     16,\n\t\t\tpaddingH:     6,\n\t\t\twantSubstr:   \"jauge fails: total must be greater than 0\",\n\t\t},\n\t\t{\n\t\t\tname:         \"fails when value \u003e total\",\n\t\t\tvalue:        11,\n\t\t\ttotal:        10,\n\t\t\tlabel:        \"Yes\",\n\t\t\tcolor:        \"#a6da95\",\n\t\t\tpercentOnly:  false,\n\t\t\twidth:        100,\n\t\t\tcanvasHeight: 30,\n\t\t\tfontSize:     16,\n\t\t\tpaddingH:     6,\n\t\t\twantSubstr:   \"jauge fails: value cannot be greater than total\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := Render(tt.value, tt.total, tt.label, tt.color, Config{tt.percentOnly, tt.width, tt.canvasHeight, tt.fontSize, tt.paddingH})\n\t\t\tif !strings.Contains(got, tt.wantSubstr) {\n\t\t\t\tt.Errorf(\"Render() = %q, want substring %q\", got, tt.wantSubstr)\n\t\t\t}\n\t\t})\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/samcrew/gauge\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1kfd9f5zlvcvy6aammcmqswa7cyjpu2nyt9qfen\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"aMDbIqGonP1WZK4TKJNIuLJ407nVFRm2yBa/JPexdXAgIOZVwUkbMOqjHFMylLmehuuBiCe29b7l/C/bkLo0DQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"keccak256","path":"gno.land/p/samcrew/keccak256","files":[{"name":"README.md","body":"# `keccak256` \n\nKeccak-256 hash (32‑byte output). Port of Go's x/crypto/sha3 (legacy Keccak variant).\n\n## Usage\n```go\ndata := []byte(\"hello world\")\ndigest := keccak256.Hash(data) // [32]byte\n\nh := keccak256.NewLegacyKeccak256() // streaming\nh.Write([]byte(\"hello \"))\nh.Write([]byte(\"world\"))\nfull := h.Sum(nil) // []byte len 32\n```\n\n## API\n```go\nfunc Hash(data []byte) [32]byte\nfunc NewLegacyKeccak256() hash.Hash\n```\n\n`Hash` = one-shot helper. `NewLegacyKeccak256` = streaming (implements hash.Hash).\n\n## Notes\n- Fixed size: 32 bytes (256 bits).\n- Uses legacy Keccak padding (differs from finalized SHA3-256 padding).\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/samcrew/keccak256\"\ngno = \"0.9\"\n"},{"name":"hashes.gno","body":"package keccak256\n\n// This file provides functions for creating instances of the SHA-3\n// and SHAKE hash functions, as well as utility functions for hashing\n// bytes.\n\nimport (\n\t\"hash\"\n)\n\nconst (\n\tdsbyteSHA3   = 0b00000110\n\tdsbyteKeccak = 0b00000001\n\tdsbyteShake  = 0b00011111\n\tdsbyteCShake = 0b00000100\n\n\t// rateK[c] is the rate in bytes for Keccak[c] where c is the capacity in\n\t// bits. Given the sponge size is 1600 bits, the rate is 1600 - c bits.\n\trateK256  = (1600 - 256) / 8\n\trateK448  = (1600 - 448) / 8\n\trateK512  = (1600 - 512) / 8\n\trateK768  = (1600 - 768) / 8\n\trateK1024 = (1600 - 1024) / 8\n)\n\n// NewLegacyKeccak256 creates a new Keccak-256 hash.\n//\n// Only use this function if you require compatibility with an existing cryptosystem\n// that uses non-standard padding. All other users should use New256 instead.\nfunc NewLegacyKeccak256() hash.Hash {\n\treturn \u0026state{rate: rateK512, outputLen: 32, dsbyte: dsbyteKeccak}\n}\n\nfunc Hash(data []byte) [32]byte {\n\tvar res [32]byte\n\tkeccak256 := NewLegacyKeccak256()\n\tkeccak256.Write(data)\n\tcopy(res[:], keccak256.Sum(nil))\n\n\treturn res\n}\n"},{"name":"keccakf.gno","body":"package keccak256\n\nimport \"math/bits\"\n\n// rc stores the round constants for use in the ι step.\nvar rc = [24]uint64{\n\t0x0000000000000001,\n\t0x0000000000008082,\n\t0x800000000000808A,\n\t0x8000000080008000,\n\t0x000000000000808B,\n\t0x0000000080000001,\n\t0x8000000080008081,\n\t0x8000000000008009,\n\t0x000000000000008A,\n\t0x0000000000000088,\n\t0x0000000080008009,\n\t0x000000008000000A,\n\t0x000000008000808B,\n\t0x800000000000008B,\n\t0x8000000000008089,\n\t0x8000000000008003,\n\t0x8000000000008002,\n\t0x8000000000000080,\n\t0x000000000000800A,\n\t0x800000008000000A,\n\t0x8000000080008081,\n\t0x8000000000008080,\n\t0x0000000080000001,\n\t0x8000000080008008,\n}\n\n// keccakF1600 applies the Keccak permutation to a 1600b-wide\n// state represented as a slice of 25 uint64s.\nfunc keccakF1600(a *[25]uint64) {\n\t// Implementation translated from Keccak-inplace.c\n\t// in the keccak reference code.\n\tvar t, bc0, bc1, bc2, bc3, bc4, d0, d1, d2, d3, d4 uint64\n\n\tfor i := 0; i \u003c 24; i += 4 {\n\t\t// Combines the 5 steps in each round into 2 steps.\n\t\t// Unrolls 4 rounds per loop and spreads some steps across rounds.\n\n\t\t// Round 1\n\t\tbc0 = a[0] ^ a[5] ^ a[10] ^ a[15] ^ a[20]\n\t\tbc1 = a[1] ^ a[6] ^ a[11] ^ a[16] ^ a[21]\n\t\tbc2 = a[2] ^ a[7] ^ a[12] ^ a[17] ^ a[22]\n\t\tbc3 = a[3] ^ a[8] ^ a[13] ^ a[18] ^ a[23]\n\t\tbc4 = a[4] ^ a[9] ^ a[14] ^ a[19] ^ a[24]\n\t\td0 = bc4 ^ (bc1\u003c\u003c1 | bc1\u003e\u003e63)\n\t\td1 = bc0 ^ (bc2\u003c\u003c1 | bc2\u003e\u003e63)\n\t\td2 = bc1 ^ (bc3\u003c\u003c1 | bc3\u003e\u003e63)\n\t\td3 = bc2 ^ (bc4\u003c\u003c1 | bc4\u003e\u003e63)\n\t\td4 = bc3 ^ (bc0\u003c\u003c1 | bc0\u003e\u003e63)\n\n\t\tbc0 = a[0] ^ d0\n\t\tt = a[6] ^ d1\n\t\tbc1 = bits.RotateLeft64(t, 44)\n\t\tt = a[12] ^ d2\n\t\tbc2 = bits.RotateLeft64(t, 43)\n\t\tt = a[18] ^ d3\n\t\tbc3 = bits.RotateLeft64(t, 21)\n\t\tt = a[24] ^ d4\n\t\tbc4 = bits.RotateLeft64(t, 14)\n\t\ta[0] = bc0 ^ (bc2 \u0026^ bc1) ^ rc[i]\n\t\ta[6] = bc1 ^ (bc3 \u0026^ bc2)\n\t\ta[12] = bc2 ^ (bc4 \u0026^ bc3)\n\t\ta[18] = bc3 ^ (bc0 \u0026^ bc4)\n\t\ta[24] = bc4 ^ (bc1 \u0026^ bc0)\n\n\t\tt = a[10] ^ d0\n\t\tbc2 = bits.RotateLeft64(t, 3)\n\t\tt = a[16] ^ d1\n\t\tbc3 = bits.RotateLeft64(t, 45)\n\t\tt = a[22] ^ d2\n\t\tbc4 = bits.RotateLeft64(t, 61)\n\t\tt = a[3] ^ d3\n\t\tbc0 = bits.RotateLeft64(t, 28)\n\t\tt = a[9] ^ d4\n\t\tbc1 = bits.RotateLeft64(t, 20)\n\t\ta[10] = bc0 ^ (bc2 \u0026^ bc1)\n\t\ta[16] = bc1 ^ (bc3 \u0026^ bc2)\n\t\ta[22] = bc2 ^ (bc4 \u0026^ bc3)\n\t\ta[3] = bc3 ^ (bc0 \u0026^ bc4)\n\t\ta[9] = bc4 ^ (bc1 \u0026^ bc0)\n\n\t\tt = a[20] ^ d0\n\t\tbc4 = bits.RotateLeft64(t, 18)\n\t\tt = a[1] ^ d1\n\t\tbc0 = bits.RotateLeft64(t, 1)\n\t\tt = a[7] ^ d2\n\t\tbc1 = bits.RotateLeft64(t, 6)\n\t\tt = a[13] ^ d3\n\t\tbc2 = bits.RotateLeft64(t, 25)\n\t\tt = a[19] ^ d4\n\t\tbc3 = bits.RotateLeft64(t, 8)\n\t\ta[20] = bc0 ^ (bc2 \u0026^ bc1)\n\t\ta[1] = bc1 ^ (bc3 \u0026^ bc2)\n\t\ta[7] = bc2 ^ (bc4 \u0026^ bc3)\n\t\ta[13] = bc3 ^ (bc0 \u0026^ bc4)\n\t\ta[19] = bc4 ^ (bc1 \u0026^ bc0)\n\n\t\tt = a[5] ^ d0\n\t\tbc1 = bits.RotateLeft64(t, 36)\n\t\tt = a[11] ^ d1\n\t\tbc2 = bits.RotateLeft64(t, 10)\n\t\tt = a[17] ^ d2\n\t\tbc3 = bits.RotateLeft64(t, 15)\n\t\tt = a[23] ^ d3\n\t\tbc4 = bits.RotateLeft64(t, 56)\n\t\tt = a[4] ^ d4\n\t\tbc0 = bits.RotateLeft64(t, 27)\n\t\ta[5] = bc0 ^ (bc2 \u0026^ bc1)\n\t\ta[11] = bc1 ^ (bc3 \u0026^ bc2)\n\t\ta[17] = bc2 ^ (bc4 \u0026^ bc3)\n\t\ta[23] = bc3 ^ (bc0 \u0026^ bc4)\n\t\ta[4] = bc4 ^ (bc1 \u0026^ bc0)\n\n\t\tt = a[15] ^ d0\n\t\tbc3 = bits.RotateLeft64(t, 41)\n\t\tt = a[21] ^ d1\n\t\tbc4 = bits.RotateLeft64(t, 2)\n\t\tt = a[2] ^ d2\n\t\tbc0 = bits.RotateLeft64(t, 62)\n\t\tt = a[8] ^ d3\n\t\tbc1 = bits.RotateLeft64(t, 55)\n\t\tt = a[14] ^ d4\n\t\tbc2 = bits.RotateLeft64(t, 39)\n\t\ta[15] = bc0 ^ (bc2 \u0026^ bc1)\n\t\ta[21] = bc1 ^ (bc3 \u0026^ bc2)\n\t\ta[2] = bc2 ^ (bc4 \u0026^ bc3)\n\t\ta[8] = bc3 ^ (bc0 \u0026^ bc4)\n\t\ta[14] = bc4 ^ (bc1 \u0026^ bc0)\n\n\t\t// Round 2\n\t\tbc0 = a[0] ^ a[5] ^ a[10] ^ a[15] ^ a[20]\n\t\tbc1 = a[1] ^ a[6] ^ a[11] ^ a[16] ^ a[21]\n\t\tbc2 = a[2] ^ a[7] ^ a[12] ^ a[17] ^ a[22]\n\t\tbc3 = a[3] ^ a[8] ^ a[13] ^ a[18] ^ a[23]\n\t\tbc4 = a[4] ^ a[9] ^ a[14] ^ a[19] ^ a[24]\n\t\td0 = bc4 ^ (bc1\u003c\u003c1 | bc1\u003e\u003e63)\n\t\td1 = bc0 ^ (bc2\u003c\u003c1 | bc2\u003e\u003e63)\n\t\td2 = bc1 ^ (bc3\u003c\u003c1 | bc3\u003e\u003e63)\n\t\td3 = bc2 ^ (bc4\u003c\u003c1 | bc4\u003e\u003e63)\n\t\td4 = bc3 ^ (bc0\u003c\u003c1 | bc0\u003e\u003e63)\n\n\t\tbc0 = a[0] ^ d0\n\t\tt = a[16] ^ d1\n\t\tbc1 = bits.RotateLeft64(t, 44)\n\t\tt = a[7] ^ d2\n\t\tbc2 = bits.RotateLeft64(t, 43)\n\t\tt = a[23] ^ d3\n\t\tbc3 = bits.RotateLeft64(t, 21)\n\t\tt = a[14] ^ d4\n\t\tbc4 = bits.RotateLeft64(t, 14)\n\t\ta[0] = bc0 ^ (bc2 \u0026^ bc1) ^ rc[i+1]\n\t\ta[16] = bc1 ^ (bc3 \u0026^ bc2)\n\t\ta[7] = bc2 ^ (bc4 \u0026^ bc3)\n\t\ta[23] = bc3 ^ (bc0 \u0026^ bc4)\n\t\ta[14] = bc4 ^ (bc1 \u0026^ bc0)\n\n\t\tt = a[20] ^ d0\n\t\tbc2 = bits.RotateLeft64(t, 3)\n\t\tt = a[11] ^ d1\n\t\tbc3 = bits.RotateLeft64(t, 45)\n\t\tt = a[2] ^ d2\n\t\tbc4 = bits.RotateLeft64(t, 61)\n\t\tt = a[18] ^ d3\n\t\tbc0 = bits.RotateLeft64(t, 28)\n\t\tt = a[9] ^ d4\n\t\tbc1 = bits.RotateLeft64(t, 20)\n\t\ta[20] = bc0 ^ (bc2 \u0026^ bc1)\n\t\ta[11] = bc1 ^ (bc3 \u0026^ bc2)\n\t\ta[2] = bc2 ^ (bc4 \u0026^ bc3)\n\t\ta[18] = bc3 ^ (bc0 \u0026^ bc4)\n\t\ta[9] = bc4 ^ (bc1 \u0026^ bc0)\n\n\t\tt = a[15] ^ d0\n\t\tbc4 = bits.RotateLeft64(t, 18)\n\t\tt = a[6] ^ d1\n\t\tbc0 = bits.RotateLeft64(t, 1)\n\t\tt = a[22] ^ d2\n\t\tbc1 = bits.RotateLeft64(t, 6)\n\t\tt = a[13] ^ d3\n\t\tbc2 = bits.RotateLeft64(t, 25)\n\t\tt = a[4] ^ d4\n\t\tbc3 = bits.RotateLeft64(t, 8)\n\t\ta[15] = bc0 ^ (bc2 \u0026^ bc1)\n\t\ta[6] = bc1 ^ (bc3 \u0026^ bc2)\n\t\ta[22] = bc2 ^ (bc4 \u0026^ bc3)\n\t\ta[13] = bc3 ^ (bc0 \u0026^ bc4)\n\t\ta[4] = bc4 ^ (bc1 \u0026^ bc0)\n\n\t\tt = a[10] ^ d0\n\t\tbc1 = bits.RotateLeft64(t, 36)\n\t\tt = a[1] ^ d1\n\t\tbc2 = bits.RotateLeft64(t, 10)\n\t\tt = a[17] ^ d2\n\t\tbc3 = bits.RotateLeft64(t, 15)\n\t\tt = a[8] ^ d3\n\t\tbc4 = bits.RotateLeft64(t, 56)\n\t\tt = a[24] ^ d4\n\t\tbc0 = bits.RotateLeft64(t, 27)\n\t\ta[10] = bc0 ^ (bc2 \u0026^ bc1)\n\t\ta[1] = bc1 ^ (bc3 \u0026^ bc2)\n\t\ta[17] = bc2 ^ (bc4 \u0026^ bc3)\n\t\ta[8] = bc3 ^ (bc0 \u0026^ bc4)\n\t\ta[24] = bc4 ^ (bc1 \u0026^ bc0)\n\n\t\tt = a[5] ^ d0\n\t\tbc3 = bits.RotateLeft64(t, 41)\n\t\tt = a[21] ^ d1\n\t\tbc4 = bits.RotateLeft64(t, 2)\n\t\tt = a[12] ^ d2\n\t\tbc0 = bits.RotateLeft64(t, 62)\n\t\tt = a[3] ^ d3\n\t\tbc1 = bits.RotateLeft64(t, 55)\n\t\tt = a[19] ^ d4\n\t\tbc2 = bits.RotateLeft64(t, 39)\n\t\ta[5] = bc0 ^ (bc2 \u0026^ bc1)\n\t\ta[21] = bc1 ^ (bc3 \u0026^ bc2)\n\t\ta[12] = bc2 ^ (bc4 \u0026^ bc3)\n\t\ta[3] = bc3 ^ (bc0 \u0026^ bc4)\n\t\ta[19] = bc4 ^ (bc1 \u0026^ bc0)\n\n\t\t// Round 3\n\t\tbc0 = a[0] ^ a[5] ^ a[10] ^ a[15] ^ a[20]\n\t\tbc1 = a[1] ^ a[6] ^ a[11] ^ a[16] ^ a[21]\n\t\tbc2 = a[2] ^ a[7] ^ a[12] ^ a[17] ^ a[22]\n\t\tbc3 = a[3] ^ a[8] ^ a[13] ^ a[18] ^ a[23]\n\t\tbc4 = a[4] ^ a[9] ^ a[14] ^ a[19] ^ a[24]\n\t\td0 = bc4 ^ (bc1\u003c\u003c1 | bc1\u003e\u003e63)\n\t\td1 = bc0 ^ (bc2\u003c\u003c1 | bc2\u003e\u003e63)\n\t\td2 = bc1 ^ (bc3\u003c\u003c1 | bc3\u003e\u003e63)\n\t\td3 = bc2 ^ (bc4\u003c\u003c1 | bc4\u003e\u003e63)\n\t\td4 = bc3 ^ (bc0\u003c\u003c1 | bc0\u003e\u003e63)\n\n\t\tbc0 = a[0] ^ d0\n\t\tt = a[11] ^ d1\n\t\tbc1 = bits.RotateLeft64(t, 44)\n\t\tt = a[22] ^ d2\n\t\tbc2 = bits.RotateLeft64(t, 43)\n\t\tt = a[8] ^ d3\n\t\tbc3 = bits.RotateLeft64(t, 21)\n\t\tt = a[19] ^ d4\n\t\tbc4 = bits.RotateLeft64(t, 14)\n\t\ta[0] = bc0 ^ (bc2 \u0026^ bc1) ^ rc[i+2]\n\t\ta[11] = bc1 ^ (bc3 \u0026^ bc2)\n\t\ta[22] = bc2 ^ (bc4 \u0026^ bc3)\n\t\ta[8] = bc3 ^ (bc0 \u0026^ bc4)\n\t\ta[19] = bc4 ^ (bc1 \u0026^ bc0)\n\n\t\tt = a[15] ^ d0\n\t\tbc2 = bits.RotateLeft64(t, 3)\n\t\tt = a[1] ^ d1\n\t\tbc3 = bits.RotateLeft64(t, 45)\n\t\tt = a[12] ^ d2\n\t\tbc4 = bits.RotateLeft64(t, 61)\n\t\tt = a[23] ^ d3\n\t\tbc0 = bits.RotateLeft64(t, 28)\n\t\tt = a[9] ^ d4\n\t\tbc1 = bits.RotateLeft64(t, 20)\n\t\ta[15] = bc0 ^ (bc2 \u0026^ bc1)\n\t\ta[1] = bc1 ^ (bc3 \u0026^ bc2)\n\t\ta[12] = bc2 ^ (bc4 \u0026^ bc3)\n\t\ta[23] = bc3 ^ (bc0 \u0026^ bc4)\n\t\ta[9] = bc4 ^ (bc1 \u0026^ bc0)\n\n\t\tt = a[5] ^ d0\n\t\tbc4 = bits.RotateLeft64(t, 18)\n\t\tt = a[16] ^ d1\n\t\tbc0 = bits.RotateLeft64(t, 1)\n\t\tt = a[2] ^ d2\n\t\tbc1 = bits.RotateLeft64(t, 6)\n\t\tt = a[13] ^ d3\n\t\tbc2 = bits.RotateLeft64(t, 25)\n\t\tt = a[24] ^ d4\n\t\tbc3 = bits.RotateLeft64(t, 8)\n\t\ta[5] = bc0 ^ (bc2 \u0026^ bc1)\n\t\ta[16] = bc1 ^ (bc3 \u0026^ bc2)\n\t\ta[2] = bc2 ^ (bc4 \u0026^ bc3)\n\t\ta[13] = bc3 ^ (bc0 \u0026^ bc4)\n\t\ta[24] = bc4 ^ (bc1 \u0026^ bc0)\n\n\t\tt = a[20] ^ d0\n\t\tbc1 = bits.RotateLeft64(t, 36)\n\t\tt = a[6] ^ d1\n\t\tbc2 = bits.RotateLeft64(t, 10)\n\t\tt = a[17] ^ d2\n\t\tbc3 = bits.RotateLeft64(t, 15)\n\t\tt = a[3] ^ d3\n\t\tbc4 = bits.RotateLeft64(t, 56)\n\t\tt = a[14] ^ d4\n\t\tbc0 = bits.RotateLeft64(t, 27)\n\t\ta[20] = bc0 ^ (bc2 \u0026^ bc1)\n\t\ta[6] = bc1 ^ (bc3 \u0026^ bc2)\n\t\ta[17] = bc2 ^ (bc4 \u0026^ bc3)\n\t\ta[3] = bc3 ^ (bc0 \u0026^ bc4)\n\t\ta[14] = bc4 ^ (bc1 \u0026^ bc0)\n\n\t\tt = a[10] ^ d0\n\t\tbc3 = bits.RotateLeft64(t, 41)\n\t\tt = a[21] ^ d1\n\t\tbc4 = bits.RotateLeft64(t, 2)\n\t\tt = a[7] ^ d2\n\t\tbc0 = bits.RotateLeft64(t, 62)\n\t\tt = a[18] ^ d3\n\t\tbc1 = bits.RotateLeft64(t, 55)\n\t\tt = a[4] ^ d4\n\t\tbc2 = bits.RotateLeft64(t, 39)\n\t\ta[10] = bc0 ^ (bc2 \u0026^ bc1)\n\t\ta[21] = bc1 ^ (bc3 \u0026^ bc2)\n\t\ta[7] = bc2 ^ (bc4 \u0026^ bc3)\n\t\ta[18] = bc3 ^ (bc0 \u0026^ bc4)\n\t\ta[4] = bc4 ^ (bc1 \u0026^ bc0)\n\n\t\t// Round 4\n\t\tbc0 = a[0] ^ a[5] ^ a[10] ^ a[15] ^ a[20]\n\t\tbc1 = a[1] ^ a[6] ^ a[11] ^ a[16] ^ a[21]\n\t\tbc2 = a[2] ^ a[7] ^ a[12] ^ a[17] ^ a[22]\n\t\tbc3 = a[3] ^ a[8] ^ a[13] ^ a[18] ^ a[23]\n\t\tbc4 = a[4] ^ a[9] ^ a[14] ^ a[19] ^ a[24]\n\t\td0 = bc4 ^ (bc1\u003c\u003c1 | bc1\u003e\u003e63)\n\t\td1 = bc0 ^ (bc2\u003c\u003c1 | bc2\u003e\u003e63)\n\t\td2 = bc1 ^ (bc3\u003c\u003c1 | bc3\u003e\u003e63)\n\t\td3 = bc2 ^ (bc4\u003c\u003c1 | bc4\u003e\u003e63)\n\t\td4 = bc3 ^ (bc0\u003c\u003c1 | bc0\u003e\u003e63)\n\n\t\tbc0 = a[0] ^ d0\n\t\tt = a[1] ^ d1\n\t\tbc1 = bits.RotateLeft64(t, 44)\n\t\tt = a[2] ^ d2\n\t\tbc2 = bits.RotateLeft64(t, 43)\n\t\tt = a[3] ^ d3\n\t\tbc3 = bits.RotateLeft64(t, 21)\n\t\tt = a[4] ^ d4\n\t\tbc4 = bits.RotateLeft64(t, 14)\n\t\ta[0] = bc0 ^ (bc2 \u0026^ bc1) ^ rc[i+3]\n\t\ta[1] = bc1 ^ (bc3 \u0026^ bc2)\n\t\ta[2] = bc2 ^ (bc4 \u0026^ bc3)\n\t\ta[3] = bc3 ^ (bc0 \u0026^ bc4)\n\t\ta[4] = bc4 ^ (bc1 \u0026^ bc0)\n\n\t\tt = a[5] ^ d0\n\t\tbc2 = bits.RotateLeft64(t, 3)\n\t\tt = a[6] ^ d1\n\t\tbc3 = bits.RotateLeft64(t, 45)\n\t\tt = a[7] ^ d2\n\t\tbc4 = bits.RotateLeft64(t, 61)\n\t\tt = a[8] ^ d3\n\t\tbc0 = bits.RotateLeft64(t, 28)\n\t\tt = a[9] ^ d4\n\t\tbc1 = bits.RotateLeft64(t, 20)\n\t\ta[5] = bc0 ^ (bc2 \u0026^ bc1)\n\t\ta[6] = bc1 ^ (bc3 \u0026^ bc2)\n\t\ta[7] = bc2 ^ (bc4 \u0026^ bc3)\n\t\ta[8] = bc3 ^ (bc0 \u0026^ bc4)\n\t\ta[9] = bc4 ^ (bc1 \u0026^ bc0)\n\n\t\tt = a[10] ^ d0\n\t\tbc4 = bits.RotateLeft64(t, 18)\n\t\tt = a[11] ^ d1\n\t\tbc0 = bits.RotateLeft64(t, 1)\n\t\tt = a[12] ^ d2\n\t\tbc1 = bits.RotateLeft64(t, 6)\n\t\tt = a[13] ^ d3\n\t\tbc2 = bits.RotateLeft64(t, 25)\n\t\tt = a[14] ^ d4\n\t\tbc3 = bits.RotateLeft64(t, 8)\n\t\ta[10] = bc0 ^ (bc2 \u0026^ bc1)\n\t\ta[11] = bc1 ^ (bc3 \u0026^ bc2)\n\t\ta[12] = bc2 ^ (bc4 \u0026^ bc3)\n\t\ta[13] = bc3 ^ (bc0 \u0026^ bc4)\n\t\ta[14] = bc4 ^ (bc1 \u0026^ bc0)\n\n\t\tt = a[15] ^ d0\n\t\tbc1 = bits.RotateLeft64(t, 36)\n\t\tt = a[16] ^ d1\n\t\tbc2 = bits.RotateLeft64(t, 10)\n\t\tt = a[17] ^ d2\n\t\tbc3 = bits.RotateLeft64(t, 15)\n\t\tt = a[18] ^ d3\n\t\tbc4 = bits.RotateLeft64(t, 56)\n\t\tt = a[19] ^ d4\n\t\tbc0 = bits.RotateLeft64(t, 27)\n\t\ta[15] = bc0 ^ (bc2 \u0026^ bc1)\n\t\ta[16] = bc1 ^ (bc3 \u0026^ bc2)\n\t\ta[17] = bc2 ^ (bc4 \u0026^ bc3)\n\t\ta[18] = bc3 ^ (bc0 \u0026^ bc4)\n\t\ta[19] = bc4 ^ (bc1 \u0026^ bc0)\n\n\t\tt = a[20] ^ d0\n\t\tbc3 = bits.RotateLeft64(t, 41)\n\t\tt = a[21] ^ d1\n\t\tbc4 = bits.RotateLeft64(t, 2)\n\t\tt = a[22] ^ d2\n\t\tbc0 = bits.RotateLeft64(t, 62)\n\t\tt = a[23] ^ d3\n\t\tbc1 = bits.RotateLeft64(t, 55)\n\t\tt = a[24] ^ d4\n\t\tbc2 = bits.RotateLeft64(t, 39)\n\t\ta[20] = bc0 ^ (bc2 \u0026^ bc1)\n\t\ta[21] = bc1 ^ (bc3 \u0026^ bc2)\n\t\ta[22] = bc2 ^ (bc4 \u0026^ bc3)\n\t\ta[23] = bc3 ^ (bc0 \u0026^ bc4)\n\t\ta[24] = bc4 ^ (bc1 \u0026^ bc0)\n\t}\n}\n"},{"name":"sha3.gno","body":"// Copyright 2014 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 keccak256 was ported from x/crypto/sha3\n// the original source can be found here:\n// https://cs.opensource.google/go/x/crypto/+/master:sha3/sha3.go\npackage keccak256\n\nimport (\n\t\"crypto/subtle\"\n\t\"encoding/binary\"\n\t\"errors\"\n)\n\n// spongeDirection indicates the direction bytes are flowing through the sponge.\ntype spongeDirection int\n\nconst (\n\t// spongeAbsorbing indicates that the sponge is absorbing input.\n\tspongeAbsorbing spongeDirection = iota\n\t// spongeSqueezing indicates that the sponge is being squeezed.\n\tspongeSqueezing\n)\n\ntype state struct {\n\ta [1600 / 8]byte // main state of the hash\n\n\t// a[n:rate] is the buffer. If absorbing, it's the remaining space to XOR\n\t// into before running the permutation. If squeezing, it's the remaining\n\t// output to produce before running the permutation.\n\tn, rate int\n\n\t// dsbyte contains the \"domain separation\" bits and the first bit of\n\t// the padding. Sections 6.1 and 6.2 of [1] separate the outputs of the\n\t// SHA-3 and SHAKE functions by appending bitstrings to the message.\n\t// Using a little-endian bit-ordering convention, these are \"01\" for SHA-3\n\t// and \"1111\" for SHAKE, or 00000010b and 00001111b, respectively. Then the\n\t// padding rule from section 5.1 is applied to pad the message to a multiple\n\t// of the rate, which involves adding a \"1\" bit, zero or more \"0\" bits, and\n\t// a final \"1\" bit. We merge the first \"1\" bit from the padding into dsbyte,\n\t// giving 00000110b (0x06) and 00011111b (0x1f).\n\t// [1] http://csrc.nist.gov/publications/drafts/fips-202/fips_202_draft.pdf\n\t//     \"Draft FIPS 202: SHA-3 Standard: Permutation-Based Hash and\n\t//      Extendable-Output Functions (May 2014)\"\n\tdsbyte byte\n\n\toutputLen int             // the default output size in bytes\n\tstate     spongeDirection // whether the sponge is absorbing or squeezing\n}\n\n// BlockSize returns the rate of sponge underlying this hash function.\nfunc (d *state) BlockSize() int { return d.rate }\n\n// Size returns the output size of the hash function in bytes.\nfunc (d *state) Size() int { return d.outputLen }\n\n// Reset clears the internal state by zeroing the sponge state and\n// the buffer indexes, and setting Sponge.state to absorbing.\nfunc (d *state) Reset() {\n\t// Zero the permutation's state.\n\tfor i := range d.a {\n\t\td.a[i] = 0\n\t}\n\td.state = spongeAbsorbing\n\td.n = 0\n}\n\nfunc (d *state) clone() *state {\n\tret := *d\n\treturn \u0026ret\n}\n\n// permute applies the KeccakF-1600 permutation without unsafe.\nfunc (d *state) permute() {\n\tvar a [25]uint64\n\n\t// Always load in little-endian order\n\tfor i := range a {\n\t\ta[i] = binary.LittleEndian.Uint64(d.a[i*8:])\n\t}\n\n\tkeccakF1600(\u0026a)\n\td.n = 0\n\n\t// Always store in little-endian order\n\tfor i := range a {\n\t\tbinary.LittleEndian.PutUint64(d.a[i*8:], a[i])\n\t}\n}\n\n// pads appends the domain separation bits in dsbyte, applies\n// the multi-bitrate 10..1 padding rule, and permutes the state.\nfunc (d *state) padAndPermute() {\n\t// Pad with this instance's domain-separator bits. We know that there's\n\t// at least one byte of space in the sponge because, if it were full,\n\t// permute would have been called to empty it. dsbyte also contains the\n\t// first one bit for the padding. See the comment in the state struct.\n\td.a[d.n] ^= d.dsbyte\n\t// This adds the final one bit for the padding. Because of the way that\n\t// bits are numbered from the LSB upwards, the final bit is the MSB of\n\t// the last byte.\n\td.a[d.rate-1] ^= 0x80\n\t// Apply the permutation\n\td.permute()\n\td.state = spongeSqueezing\n}\n\n// Write absorbs more data into the hash's state. It panics if any\n// output has already been read.\nfunc (d *state) Write(p []byte) (n int, err error) {\n\tif d.state != spongeAbsorbing {\n\t\tpanic(\"sha3: Write after Read\")\n\t}\n\n\tn = len(p)\n\tfor len(p) \u003e 0 {\n\t\tx := subtle.XORBytesUnsafe(d.a[d.n:d.rate], d.a[d.n:d.rate], p)\n\t\td.n += x\n\t\tp = p[x:]\n\n\t\t// If the sponge is full, apply the permutation.\n\t\tif d.n == d.rate {\n\t\t\td.permute()\n\t\t}\n\t}\n\n\treturn\n}\n\n// Read squeezes an arbitrary number of bytes from the sponge.\nfunc (d *state) Read(out []byte) (n int, err error) {\n\t// If we're still absorbing, pad and apply the permutation.\n\tif d.state == spongeAbsorbing {\n\t\td.padAndPermute()\n\t}\n\n\tn = len(out)\n\n\t// Now, do the squeezing.\n\tfor len(out) \u003e 0 {\n\t\t// Apply the permutation if we've squeezed the sponge dry.\n\t\tif d.n == d.rate {\n\t\t\td.permute()\n\t\t}\n\n\t\tx := copy(out, d.a[d.n:d.rate])\n\t\td.n += x\n\t\tout = out[x:]\n\t}\n\n\treturn\n}\n\n// Sum applies padding to the hash state and then squeezes out the desired\n// number of output bytes. It panics if any output has already been read.\nfunc (d *state) Sum(in []byte) []byte {\n\tif d.state != spongeAbsorbing {\n\t\tpanic(\"sha3: Sum after Read\")\n\t}\n\n\t// Make a copy of the original hash so that caller can keep writing\n\t// and summing.\n\tdup := d.clone()\n\thash := make([]byte, dup.outputLen, 64) // explicit cap to allow stack allocation\n\tdup.Read(hash)\n\treturn append(in, hash...)\n}\n\nconst (\n\tmagicSHA3   = \"sha\\x08\"\n\tmagicShake  = \"sha\\x09\"\n\tmagicCShake = \"sha\\x0a\"\n\tmagicKeccak = \"sha\\x0b\"\n\t// magic || rate || main state || n || sponge direction\n\tmarshaledSize = len(magicSHA3) + 1 + 200 + 1 + 1\n)\n\nfunc (d *state) MarshalBinary() ([]byte, error) {\n\treturn d.AppendBinary(make([]byte, 0, marshaledSize))\n}\n\nfunc (d *state) AppendBinary(b []byte) ([]byte, error) {\n\tswitch d.dsbyte {\n\tcase dsbyteSHA3:\n\t\tb = append(b, magicSHA3...)\n\tcase dsbyteShake:\n\t\tb = append(b, magicShake...)\n\tcase dsbyteCShake:\n\t\tb = append(b, magicCShake...)\n\tcase dsbyteKeccak:\n\t\tb = append(b, magicKeccak...)\n\tdefault:\n\t\tpanic(\"unknown dsbyte\")\n\t}\n\t// rate is at most 168, and n is at most rate.\n\tb = append(b, byte(d.rate))\n\tb = append(b, d.a[:]...)\n\tb = append(b, byte(d.n), byte(d.state))\n\treturn b, nil\n}\n\nfunc (d *state) UnmarshalBinary(b []byte) error {\n\tif len(b) != marshaledSize {\n\t\treturn errors.New(\"sha3: invalid hash state\")\n\t}\n\n\tmagic := string(b[:len(magicSHA3)])\n\tb = b[len(magicSHA3):]\n\tswitch {\n\tcase magic == magicSHA3 \u0026\u0026 d.dsbyte == dsbyteSHA3:\n\tcase magic == magicShake \u0026\u0026 d.dsbyte == dsbyteShake:\n\tcase magic == magicCShake \u0026\u0026 d.dsbyte == dsbyteCShake:\n\tcase magic == magicKeccak \u0026\u0026 d.dsbyte == dsbyteKeccak:\n\tdefault:\n\t\treturn errors.New(\"sha3: invalid hash state identifier\")\n\t}\n\n\trate := int(b[0])\n\tb = b[1:]\n\tif rate != d.rate {\n\t\treturn errors.New(\"sha3: invalid hash state function\")\n\t}\n\n\tcopy(d.a[:], b)\n\tb = b[len(d.a):]\n\n\tn, state := int(b[0]), spongeDirection(b[1])\n\tif n \u003e d.rate {\n\t\treturn errors.New(\"sha3: invalid hash state\")\n\t}\n\td.n = n\n\tif state != spongeAbsorbing \u0026\u0026 state != spongeSqueezing {\n\t\treturn errors.New(\"sha3: invalid hash state\")\n\t}\n\td.state = state\n\n\treturn nil\n}\n"},{"name":"sha3_test.gno","body":"// Copyright 2014 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\npackage keccak256\n\n// Tests include all the ShortMsgKATs provided by the Keccak team at\n// https://github.com/gvanas/KeccakCodePackage\n//\n// They only include the zero-bit case of the bitwise testvectors\n// published by NIST in the draft of FIPS-202.\n\nimport (\n\t\"bytes\"\n\t\"encoding/hex\"\n\t\"hash\"\n\t\"math/rand\"\n\t\"testing\"\n)\n\nconst (\n\ttestString  = \"brekeccakkeccak koax koax\"\n\tkatFilename = \"testdata/keccakKats.json.deflate\"\n)\n\n// testDigests contains functions returning hash.Hash instances\n// with output-length equal to the KAT length for SHA-3, Keccak\n// and SHAKE instances.\nvar testDigests = map[string]func() hash.Hash{\n\t\"Keccak-256\": NewLegacyKeccak256,\n}\n\n// decodeHex converts a hex-encoded string into a raw byte string.\nfunc decodeHex(s string) []byte {\n\tb, err := hex.DecodeString(s)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn b\n}\n\n// structs used to marshal JSON test-cases.\ntype KeccakKats struct {\n\tKats map[string][]struct {\n\t\tDigest  string `json:\"digest\"`\n\t\tLength  int64  `json:\"length\"`\n\t\tMessage string `json:\"message\"`\n\n\t\t// Defined only for cSHAKE\n\t\tN string `json:\"N\"`\n\t\tS string `json:\"S\"`\n\t}\n}\n\n// TestKeccakKats tests the SHA-3 and Shake implementations against all the\n// ShortMsgKATs from https://github.com/gvanas/KeccakCodePackage\n// (The testvectors are stored in keccakKats.json.deflate due to their length.)\n//XXX Do not use \"compress/flate\"\n/*func TestKeccakKats(t *testing.T) {\n\t// Read the KATs.\n\tdeflated, err := os.Open(katFilename)\n\tif err != nil {\n\t\tt.Errorf(\"error opening %s: %s\", katFilename, err)\n\t}\n\tfile := flate.NewReader(deflated)\n\tdec := json.NewDecoder(file)\n\tvar katSet KeccakKats\n\terr = dec.Decode(\u0026katSet)\n\tif err != nil {\n\t\tt.Errorf(\"error decoding KATs: %s\", err)\n\t}\n\n\tfor algo, function := range testDigests {\n\t\td := function()\n\t\tfor _, kat := range katSet.Kats[algo] {\n\t\t\td.Reset()\n\t\t\tin, err := hex.DecodeString(kat.Message)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"error decoding KAT: %s\", err)\n\t\t\t}\n\t\t\td.Write(in[:kat.Length/8])\n\t\t\tgot := strings.ToUpper(hex.EncodeToString(d.Sum(nil)))\n\t\t\tif got != kat.Digest {\n\t\t\t\tt.Errorf(\"function=%s, length=%d\\nmessage:\\n %s\\ngot:\\n  %s\\nwanted:\\n %s\",\n\t\t\t\t\talgo, kat.Length, kat.Message, got, kat.Digest)\n\t\t\t\tt.Logf(\"wanted %+v\", kat)\n\t\t\t\tt.FailNow()\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t}\n\n\tfor algo, v := range testShakes {\n\t\tfor _, kat := range katSet.Kats[algo] {\n\t\t\tN, err := hex.DecodeString(kat.N)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"error decoding KAT: %s\", err)\n\t\t\t}\n\n\t\t\tS, err := hex.DecodeString(kat.S)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"error decoding KAT: %s\", err)\n\t\t\t}\n\t\t\td := v.constructor(N, S)\n\t\t\tin, err := hex.DecodeString(kat.Message)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"error decoding KAT: %s\", err)\n\t\t\t}\n\n\t\t\td.Write(in[:kat.Length/8])\n\t\t\tout := make([]byte, len(kat.Digest)/2)\n\t\t\td.Read(out)\n\t\t\tgot := strings.ToUpper(hex.EncodeToString(out))\n\t\t\tif got != kat.Digest {\n\t\t\t\tt.Errorf(\"function=%s, length=%d N:%s\\n S:%s\\nmessage:\\n %s \\ngot:\\n  %s\\nwanted:\\n %s\",\n\t\t\t\t\talgo, kat.Length, kat.N, kat.S, kat.Message, got, kat.Digest)\n\t\t\t\tt.Logf(\"wanted %+v\", kat)\n\t\t\t\tt.FailNow()\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t}\n}*/\n\n// TestKeccak does a basic test of the non-standardized Keccak hash functions.\nfunc TestKeccak(t *testing.T) {\n\ttests := []struct {\n\t\tfn   func() hash.Hash\n\t\tdata []byte\n\t\twant string\n\t}{\n\n\t\t{\n\t\t\tNewLegacyKeccak256,\n\t\t\t[]byte(\"There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text\"),\n\t\t\t\"39670cbdfbfc25519e9834899e13569e5f5802b77df3d8259961f713ad09745d\",\n\t\t},\n\t\t{\n\t\t\tNewLegacyKeccak256,\n\t\t\t[]byte(\"abc\"),\n\t\t\t\"4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45\",\n\t\t},\n\t\t{\n\t\t\tNewLegacyKeccak256,\n\t\t\t[]byte(\"916889520661998333947422699691\"),\n\t\t\t\"c5b223cc231dd56389ca19435758fba38e79f3c461d989d818ceb96595d310d1\",\n\t\t},\n\t\t{\n\t\t\tNewLegacyKeccak256,\n\t\t\t[]byte(\"6R1Q2KvQNGxI4OtUGDS1rSCbJXkp1H\"),\n\t\t\t\"8e94c6d49ca23597ee1d4a317b17a85ae38f6f3241e11c3ace4abe756287839a\",\n\t\t},\n\t\t{\n\t\t\tNewLegacyKeccak256,\n\t\t\t[]byte(\"SOMERANDOMSTRING\"),\n\t\t\t\"59f47b8dc1ffdfb716d2fcb313c070040f6049bd23c321c6cb8adb3f79eb720f\",\n\t\t},\n\t\t{\n\t\t\tNewLegacyKeccak256,\n\t\t\t[]byte(\"Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC\"),\n\t\t\t\"da5293ad50619d7fdac8b4c8dc2f80e0cd40b6571be14740229d0a1d6e2fc232\",\n\t\t},\n\t\t{\n\t\t\tNewLegacyKeccak256,\n\t\t\t[]byte(\"There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which looks reasonable. The generated Lorem Ipsum is therefore always free from repetition, injected humour, or non-characteristic words etc.\"),\n\t\t\t\"a84506d23b7246dc18805c8bf60316fb6f677934a546ee8d8650495929f21eef\",\n\t\t},\n\t}\n\n\tfor _, u := range tests {\n\t\th := u.fn()\n\t\th.Write(u.data)\n\t\tgot := h.Sum(nil)\n\t\twant := decodeHex(u.want)\n\t\tif !bytes.Equal(got, want) {\n\t\t\tt.Errorf(\"unexpected hash for size %d: got '%x' want '%s'\", h.Size()*8, got, u.want)\n\t\t}\n\t}\n}\n\nfunc TestHash(t *testing.T) {\n\ttests := []struct {\n\t\tdata []byte\n\t\twant string\n\t}{\n\n\t\t{\n\n\t\t\t[]byte(\"There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text\"),\n\t\t\t\"39670cbdfbfc25519e9834899e13569e5f5802b77df3d8259961f713ad09745d\",\n\t\t},\n\t\t{\n\n\t\t\t[]byte(\"abc\"),\n\t\t\t\"4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45\",\n\t\t},\n\t\t{\n\n\t\t\t[]byte(\"916889520661998333947422699691\"),\n\t\t\t\"c5b223cc231dd56389ca19435758fba38e79f3c461d989d818ceb96595d310d1\",\n\t\t},\n\t\t{\n\n\t\t\t[]byte(\"6R1Q2KvQNGxI4OtUGDS1rSCbJXkp1H\"),\n\t\t\t\"8e94c6d49ca23597ee1d4a317b17a85ae38f6f3241e11c3ace4abe756287839a\",\n\t\t},\n\t\t{\n\n\t\t\t[]byte(\"SOMERANDOMSTRING\"),\n\t\t\t\"59f47b8dc1ffdfb716d2fcb313c070040f6049bd23c321c6cb8adb3f79eb720f\",\n\t\t},\n\t\t{\n\n\t\t\t[]byte(\"Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC\"),\n\t\t\t\"da5293ad50619d7fdac8b4c8dc2f80e0cd40b6571be14740229d0a1d6e2fc232\",\n\t\t},\n\t\t{\n\n\t\t\t[]byte(\"There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which looks reasonable. The generated Lorem Ipsum is therefore always free from repetition, injected humour, or non-characteristic words etc.\"),\n\t\t\t\"a84506d23b7246dc18805c8bf60316fb6f677934a546ee8d8650495929f21eef\",\n\t\t},\n\t}\n\n\tfor _, u := range tests {\n\t\tgot := Hash(u.data)\n\t\twant := decodeHex(u.want)\n\t\tif !bytes.Equal(got[:], want) {\n\t\t\tt.Errorf(\"unexpected hash: got '%x' want '%s'\", got, u.want)\n\t\t}\n\t}\n}\n\n// sequentialBytes produces a buffer of size consecutive bytes 0x00, 0x01, ..., used for testing.\n//\n// The alignment of each slice is intentionally randomized to detect alignment\n// issues in the implementation. See https://golang.org/issue/37644.\n// Ideally, the compiler should fuzz the alignment itself.\n// (See https://golang.org/issue/35128.)\nfunc sequentialBytes(size int) []byte {\n\talignmentOffset := rand.IntN(8)\n\tresult := make([]byte, size+alignmentOffset)[alignmentOffset:]\n\tfor i := range result {\n\t\tresult[i] = byte(i)\n\t}\n\treturn result\n}\n\n/*func TestMarshalUnmarshal(t *testing.T) {\n\tt.Run(\"Keccak-256\", func(t *testing.T) { testMarshalUnmarshal(t, NewLegacyKeccak256()) })\n}*/\n\n// TODO(filippo): move this to crypto/internal/cryptotest.\n//XX: remove rand.Read\n/*func testMarshalUnmarshal(t *testing.T, h hash.Hash) {\n\tbuf := make([]byte, 200)\n\trand.Read(buf)\n\tn := rand.IntN(200)\n\th.Write(buf)\n\twant := h.Sum(nil)\n\th.Reset()\n\th.Write(buf[:n])\n\tb, err := h.(encoding.BinaryMarshaler).MarshalBinary()\n\tif err != nil {\n\t\tt.Errorf(\"MarshalBinary: %v\", err)\n\t}\n\th.Write(bytes.Repeat([]byte{0}, 200))\n\tif err := h.(encoding.BinaryUnmarshaler).UnmarshalBinary(b); err != nil {\n\t\tt.Errorf(\"UnmarshalBinary: %v\", err)\n\t}\n\th.Write(buf[n:])\n\tgot := h.Sum(nil)\n\tif !bytes.Equal(got, want) {\n\t\tt.Errorf(\"got %x, want %x\", got, want)\n\t}\n}*/\n\n// BenchmarkPermutationFunction measures the speed of the permutation function\n// with no input data.\nfunc BenchmarkPermutationFunction(b *testing.B) {\n\tb.SetBytes(int64(200))\n\tvar lanes [25]uint64\n\tfor i := 0; i \u003c b.N; i++ {\n\t\tkeccakF1600(\u0026lanes)\n\t}\n}\n\n// benchmarkHash tests the speed to hash num buffers of buflen each.\nfunc benchmarkHash(b *testing.B, h hash.Hash, size, num int) {\n\tb.StopTimer()\n\th.Reset()\n\tdata := sequentialBytes(size)\n\tb.SetBytes(int64(size * num))\n\tb.StartTimer()\n\n\tvar state []byte\n\tfor i := 0; i \u003c b.N; i++ {\n\t\tfor j := 0; j \u003c num; j++ {\n\t\t\th.Write(data)\n\t\t}\n\t\tstate = h.Sum(state[:0])\n\t}\n\tb.StopTimer()\n\th.Reset()\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"NehXzM/7gtDkyXOnf8YUhokBbOtyyW0t6XflvkExstU6iTeJASfUI8+nboFxlKwlaifOVNN60a6mcJRnFUmx/g=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l","package":{"name":"genesis","path":"gno.land/p/sys/genesis","files":[{"name":"consts.gno","body":"// Package genesis provides a way to store and access dynamic variables that are\n// set during the genesis or block0 initialization phase. This package\n// demonstrates an important aspect of p/ packages in Gno: while they are\n// pure packages, they can have mutable state during initialization (init phase)\n// before becoming read-only.\n//\n// Future improvements:\n// When Gno supports something similar to \"go build -X\", this package could be\n// enhanced to accept variables from the CLI or environment variables (e.g., in\n// gnodev context). For now, it only supports initialization-time operations\n// that make sense at the beginning of the system's lifecycle.\npackage genesis\n\nimport (\n\t\"chain/runtime\"\n\t\"time\"\n)\n\nvar (\n\t// Time is the time of the genesis block.\n\tTime = time.Now()\n\n\t// Height is the height of the genesis block (usually 0).\n\tHeight = runtime.ChainHeight()\n\n\t// Domain is the domain of the chain.\n\tDomain = runtime.ChainDomain()\n\n\t// XXX: TZ\n\t// XXX: Supply = chain.Coins{{\"ugnot\", banker.NewReadonlyBanker().TotalSupply(\"ugnot\")}}\n)\n\n// Uptime returns the uptime of the chain.\nfunc Uptime() time.Duration {\n\treturn time.Since(Time)\n}\n\n// Upheight returns the height of the chain.\nfunc Upheight() int64 {\n\treturn runtime.ChainHeight() - Height\n}\n"},{"name":"consts_test.gno","body":"package genesis_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/sys/genesis\"\n)\n\nfunc TestGenesisConstants(t *testing.T) {\n\t// Test that Time is not zero\n\tif genesis.Time.IsZero() {\n\t\tt.Error(\"Time should not be zero\")\n\t}\n\n\t// Test that Height is not zero\n\tif genesis.Height == 0 {\n\t\tt.Error(\"Height should not be zero\")\n\t}\n\n\t// Test that Domain is not empty\n\tif genesis.Domain == \"\" {\n\t\tt.Error(\"Domain should not be empty\")\n\t}\n\n\t// Test that Uptime is not zero\n\tif genesis.Uptime() != 0 {\n\t\tt.Error(\"Uptime should be zero\")\n\t}\n\n\t// Test that Upheight is not zero\n\tif genesis.Upheight() != 0 {\n\t\tt.Error(\"Upheight should be zero\")\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/sys/genesis\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"Wnyy4QYPZgPn+pJNrWMrE7HociOuXcnXIlGD8Gdl0ls3o5FUlHbK06Mrd/qvZx5zbiilhxk3UyWmF+n2Ajer7g=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"seal","path":"gno.land/p/test/seal","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/test/seal\"\ngno = \"0.9\"\n"},{"name":"seal.gno","body":"// Package seal documents whether Gno's sealed-interface pattern\n// (unexported marker method on an interface) provides a security\n// boundary. CONCLUSION: it does NOT. See the filetests for proof.\n//\n// Search terms: sealed interface, embedding bypass, unexported marker\n// method, capability-token leak, isSealed bypass, cur leak via\n// embedding, rlm leak via interface, defense in depth, canonical-impl\n// allowlist, IsCanonical pattern, allowancesender, Teller seal,\n// Banker seal.\n//\n// Filetests (read these before reaching for the sealed-marker\n// pattern as a security control):\n//   - z_seal_violation_filetest.gno — direct redeclaration of the\n//     unexported marker in a foreign package FAILS. (The original\n//     intended property survives.)\n//   - z_seal_embedding_filetest.gno — concrete-type embedding\n//     BYPASSES the seal when the legitimate impl is exported. The\n//     promoted unexported marker retains its origin package's\n//     identity and satisfies the interface.\n//   - z_seal_iface_embedding_filetest.gno — embedding the interface\n//     value itself BYPASSES the seal regardless of whether the\n//     legitimate concrete impl is exported. This is the realistic\n//     Teller/Banker attack: hold a legit Teller obtained from\n//     Token.CallerTeller(), embed it, shadow Transfer to capture rlm.\n//\n// Implication for the cur-leak migration: don't seal as a defense.\n// Use Reader/Writer split, Authority-pattern with private concrete,\n// or canonical-impl allowlist (see migration guide).\npackage seal\n\n// Sealed is intended to be implementable only from within the seal\n// package. The unexported isSealed() method is the seal — any foreign\n// type that declares an `isSealed()` method declares\n// `r/foreign.isSealed`, not `p/test/seal.isSealed`, so structural\n// satisfaction fails.\ntype Sealed interface {\n\tHello() string\n\tisSealed()\n}\n\n// Impl is the legitimate in-package implementation of Sealed.\ntype Impl struct {\n\tName string\n}\n\nfunc NewImpl(name string) *Impl {\n\treturn \u0026Impl{Name: name}\n}\n\nfunc (i *Impl) Hello() string { return \"hello, \" + i.Name }\nfunc (i *Impl) isSealed()     {}\n\n// Accept demonstrates a consumer that takes a Sealed value. In real\n// code this is the surface a cur-leaking interface would expose; the\n// security property of sealing is that Accept can only ever receive a\n// package-internal *Impl, not an attacker-supplied type.\nfunc Accept(s Sealed) string {\n\treturn s.Hello()\n}\n"},{"name":"z_seal_embedding_filetest.gno","body":"// PKGPATH: gno.land/r/test/sealembed\npackage sealembed\n\n// Embedding-bypass test: if a foreign package can satisfy a sealed\n// interface by *embedding* the legitimate in-package implementation,\n// then the seal does not defend against the actual cur-leak threat\n// model. The attacker would obtain a legitimate *seal.Impl, embed it\n// in their own struct, and shadow whatever methods they want to\n// control — including methods that take rlm in a real-world Teller\n// or Banker interface.\n//\n// RESULT: Gno mirrors Go promotion semantics — the test compiles\n// and runs. The seal is BYPASSABLE via embedding when the\n// implementation type is exported. The attacker's Hello() runs\n// while the embedded *seal.Impl supplies the promoted isSealed().\n// See `// Output:` below for the attacker-controlled string.\n//\n// IMPLICATION for the cur-leak migration: sealing alone is NOT\n// sufficient for Teller/Banker if their concrete impls are\n// exported. `treasury.CoinsBanker` is exported (banker_coins.gno);\n// `grc20.fnTeller` is unexported, which blocks concrete embedding\n// but not interface-value embedding — see\n// z_seal_iface_embedding_filetest.gno.\n\nimport (\n\t\"gno.land/p/test/seal\"\n)\n\n// foreignImpl embeds *seal.Impl. The promoted isSealed() method\n// satisfies the seal's package-qualified requirement IF Gno follows\n// Go's promotion rules.\ntype foreignImpl struct {\n\t*seal.Impl\n}\n\n// Shadow Hello() — this is the attack: foreign code controls what\n// the \"sealed\" interface returns, while the seal check is satisfied\n// via the embedded type's promoted isSealed().\nfunc (f *foreignImpl) Hello() string { return \"evil from r/test/sealembed\" }\n\nfunc main() {\n\tlegit := seal.NewImpl(\"legit\")\n\tevil := \u0026foreignImpl{Impl: legit}\n\tvar s seal.Sealed = evil\n\tprintln(s.Hello())\n}\n\n// Output:\n// evil from r/test/sealembed\n"},{"name":"z_seal_iface_embedding_filetest.gno","body":"// PKGPATH: gno.land/r/test/sealifaceembed\npackage sealifaceembed\n\n// Interface-embedding bypass: even when the legitimate concrete impl\n// is unexported (e.g., grc20.fnTeller), a foreign realm can still\n// satisfy a sealed interface by embedding the *interface itself* in\n// a struct. The embedded interface value carries the package-\n// qualified isSealed() forward; the struct's method set includes\n// the promoted methods. Shadowing the methods the attacker cares\n// about (e.g., Transfer that takes rlm) gives them full cur-leak\n// despite the seal.\n//\n// This is the realistic Teller/Banker attack: an attacker realm\n// obtains a legitimate seal.Sealed value from a public factory\n// (e.g., Token.CallerTeller()), stores it in an embedded interface\n// field, and shadows the rlm-typed methods.\n\nimport (\n\t\"gno.land/p/test/seal\"\n)\n\n// evilWrap embeds the sealed interface directly — no concrete type\n// reference needed. If embedded-interface promotion satisfies the\n// seal, this test compiles and the attack surface is real even when\n// the legitimate concrete impl is unexported.\ntype evilWrap struct {\n\tseal.Sealed\n}\n\n// Shadow Hello() with attacker-controlled behavior.\nfunc (e *evilWrap) Hello() string { return \"evil via interface embedding\" }\n\nfunc main() {\n\tlegit := seal.NewImpl(\"legit\")\n\tevil := \u0026evilWrap{Sealed: legit}\n\tvar s seal.Sealed = evil\n\tprintln(s.Hello())\n}\n\n// Output:\n// evil via interface embedding\n"},{"name":"z_seal_rlm_shadow_filetest.gno","body":"// PKGPATH: gno.land/r/test/sealrlmshadow\npackage sealrlmshadow\n\n// Realistic Teller/Banker attack shape: the prior embedding filetests\n// shadow Hello() string, which proves embedding-bypass in principle.\n// This filetest shadows a method whose signature takes `(_ int, rlm\n// realm, ...)` — the actual shape of grc20.Teller.Transfer,\n// treasury.Banker.Send, and similar rlm-leaking methods — to confirm\n// the attacker really can capture cur via the embedding bypass.\n//\n// Search terms: rlm shadow, capture cur via embedding, Teller attack\n// proof, sealed interface embedding bypass for rlm-typed methods.\n\nimport (\n\t\"gno.land/p/test/seal\"\n)\n\n// Capturable is a Sealed-shaped interface with an rlm-typed method,\n// matching the Teller/Banker pattern (sealed marker + a method that\n// takes rlm).\ntype Capturable interface {\n\tseal.Sealed\n\tDoWithRealm(_ int, rlm realm) string\n}\n\n// evilImpl embeds *seal.Impl — gains the promoted isSealed() marker\n// satisfying the seal, plus the promoted Hello() — then adds its\n// own DoWithRealm that captures rlm. This is the realistic Teller\n// attack shape: attacker holds a legit Sealed value, embeds it,\n// adds a method that takes rlm and stashes it for later use.\ntype evilImpl struct {\n\t*seal.Impl\n}\n\nvar capturedRealmPkgPath string\n\nfunc (e *evilImpl) DoWithRealm(_ int, rlm realm) string {\n\t// rlm captured here — attacker can later use this stash to act\n\t// AS the realm whose cur was captured (A/B-class leak).\n\tcapturedRealmPkgPath = rlm.PkgPath()\n\treturn \"evil captured: \" + capturedRealmPkgPath\n}\n\n// attack runs in a crossing frame so cur is available.\nfunc attack(cur realm) {\n\tevil := \u0026evilImpl{Impl: seal.NewImpl(\"inner\")}\n\t// Assignment SUCCEEDS — *evilImpl satisfies Capturable via the\n\t// embedded *seal.Impl's promoted isSealed() and Hello(), plus\n\t// the attacker-defined DoWithRealm.\n\tvar c Capturable = evil\n\t// Caller hands their cur to what they believe is a sealed,\n\t// in-package Capturable. Attacker's DoWithRealm captures rlm.\n\tprintln(c.DoWithRealm(0, cur))\n\tprintln(\"stash holds:\", capturedRealmPkgPath)\n}\n\nfunc main(cur realm) {\n\tattack(cross(cur))\n}\n\n// Output:\n// evil captured: gno.land/r/test/sealrlmshadow\n// stash holds: gno.land/r/test/sealrlmshadow\n"},{"name":"z_seal_violation_filetest.gno","body":"// PKGPATH: gno.land/r/test/sealviolation\npackage sealviolation\n\n// Regression test: verify that an interface declared in another\n// package with an unexported method cannot be satisfied by a type\n// declared in this package. If this filetest compiles, Gno's\n// sealed-interface semantics differ from Go's and the cur-leak\n// mitigation strategy (sealing /p/ interfaces like grc20.Teller via\n// an unexported method) does NOT work.\n//\n// The expected behavior is a preprocess/typecheck error at the\n// assignment line: foreignImpl's isSealed() method belongs to\n// gno.land/r/test/sealviolation, not gno.land/p/test/seal, so it\n// doesn't satisfy seal.Sealed's seal.isSealed() requirement.\n\nimport (\n\t\"gno.land/p/test/seal\"\n)\n\ntype foreignImpl struct{}\n\nfunc (f *foreignImpl) Hello() string { return \"evil\" }\n\n// This method is named isSealed() but it belongs to package\n// sealviolation, not package seal. Structural interface matching\n// should look for *seal.isSealed* on the type, not just any method\n// named isSealed.\nfunc (f *foreignImpl) isSealed() {}\n\nfunc main() {\n\tvar s seal.Sealed = \u0026foreignImpl{} // should be a compile-time error\n\tprintln(s.Hello())\n}\n\n// Error:\n// gno.land/r/test/sealviolation/z_seal_violation.gno:31:6-36: *gno.land/r/test/sealviolation.foreignImpl does not implement gno.land/p/test/seal.Sealed (missing method isSealed)\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"8ycAHtN3jjUCDgqeHUBadBYSL3K1Sv125rWzwFBP8l85PIkpQ8eqYggFS+6XC6ZuqkFobsEptcCZV+VPFjivPQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"accesscontrol","path":"gno.land/p/thox/accesscontrol","files":[{"name":"accesscontrol.gno","body":"package accesscontrol\n\nimport (\n\t\"chain\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/ownable/v0\"\n)\n\nconst (\n\tRoleCreatedEvent   = \"RoleCreated\"\n\tRoleGrantedEvent   = \"RoleGranted\"\n\tRoleRevokedEvent   = \"RoleRevoked\"\n\tRoleRenouncedEvent = \"RoleRenounced\"\n\tRoleSetEvent       = \"RoleSet\"\n)\n\n// Role struct to store role information\ntype Role struct {\n\tName    string\n\tHolders *avl.Tree // address -\u003e struct{}\n\tOwnable *ownable.Ownable\n}\n\n// Roles struct to store all Roles information\ntype Roles struct {\n\tRoles       []*Role\n\tUserToRoles avl.Tree // address -\u003e []*Role\n\tOwnable     *ownable.Ownable\n}\n\nfunc validRoleName(name string) error {\n\tif len(name) \u003e 30 || name == \"\" {\n\t\treturn ErrNameRole\n\t}\n\treturn nil\n}\n\n// NewRole creates a new instance of Role\nfunc NewRole(name string, admin address) (*Role, error) {\n\tif err := validRoleName(name); err != nil {\n\t\treturn nil, ErrNameRole\n\t}\n\n\treturn \u0026Role{\n\t\tName:    name,\n\t\tHolders: avl.NewTree(),\n\t\tOwnable: ownable.NewWithAddress(admin),\n\t}, nil\n}\n\n// CreateRole create a new role within the realm. rlm must be the\n// caller's own captured cur; rlm.Previous().Address() must equal the\n// owner of the Roles container.\nfunc (rs *Roles) CreateRole(_ int, rlm realm, name string) (*Role, error) {\n\tif err := validRoleName(name); err != nil {\n\t\treturn nil, ErrNameRole\n\t}\n\n\tif !rlm.IsCurrent() {\n\t\treturn nil, ErrNotOwner\n\t}\n\tif !rs.Ownable.OwnedBy(rlm.Previous().Address()) {\n\t\treturn nil, ErrNotOwner\n\t}\n\n\tfor _, role := range rs.Roles {\n\t\tif role.Name == name {\n\t\t\treturn nil, ErrRoleSameName\n\t\t}\n\t}\n\n\trole, err := NewRole(name, rs.Ownable.Owner())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trs.Roles = append(rs.Roles, role)\n\n\tchain.Emit(\n\t\tRoleCreatedEvent,\n\t\t\"roleName\", name,\n\t\t\"sender\", rs.Ownable.Owner().String(),\n\t)\n\n\treturn role, nil\n}\n\n// HasAccount check if an account has a specific role\nfunc (r *Role) HasAccount(account address) bool {\n\treturn r.Holders.Has(account.String())\n}\n\n// FindRole searches for a role by its name\nfunc (rs *Roles) FindRole(name string) (*Role, error) {\n\tfor _, role := range rs.Roles {\n\t\tif role.Name == name {\n\t\t\treturn role, nil\n\t\t}\n\t}\n\n\treturn nil, ErrRoleNotFound\n}\n\n// GrantRole grants a role to an account. rlm must be the caller's own\n// captured cur; rlm.Previous().Address() must equal the role's\n// admin/owner.\nfunc (rs *Roles) GrantRole(_ int, rlm realm, name string, account address) error {\n\tr, err := rs.FindRole(name)\n\tif err != nil {\n\t\treturn ErrRoleNotFound\n\t}\n\n\tif !rlm.IsCurrent() {\n\t\treturn ErrNotOwner\n\t}\n\tcaller := rlm.Previous().Address()\n\tif !r.Ownable.OwnedBy(caller) {\n\t\treturn ErrNotOwner\n\t}\n\n\tr.Holders.Set(account.String(), struct{}{})\n\n\t// Add in UserToRoles\n\troles := rs.UserToRoles.Get(account.String())\n\tif roles == nil {\n\t\troles = []*Role{}\n\t}\n\troles = append(roles.([]*Role), r)\n\trs.UserToRoles.Set(account.String(), roles)\n\n\tchain.Emit(\n\t\tRoleGrantedEvent,\n\t\t\"roleName\", r.Name,\n\t\t\"account\", account.String(),\n\t\t\"sender\", caller.String(),\n\t)\n\n\treturn nil\n}\n\n// RevokeRole revokes a role from an account. rlm must be the caller's\n// own captured cur; rlm.Previous().Address() must equal the role's\n// admin/owner.\nfunc (rs *Roles) RevokeRole(_ int, rlm realm, name string, account address) error {\n\tr, err := rs.FindRole(name)\n\tif err != nil {\n\t\treturn ErrRoleNotFound\n\t}\n\n\tif !rlm.IsCurrent() {\n\t\treturn ErrNotOwner\n\t}\n\tcaller := rlm.Previous().Address()\n\tif !r.Ownable.OwnedBy(caller) {\n\t\treturn ErrNotOwner\n\t}\n\n\tr.Holders.Remove(account.String())\n\n\t// Remove in UserToRoles\n\troles := rs.UserToRoles.Get(account.String())\n\tif roles != nil {\n\t\tupdatedRoles := []*Role{}\n\t\tfor _, role := range roles.([]*Role) {\n\t\t\tif role != r {\n\t\t\t\tupdatedRoles = append(updatedRoles, role)\n\t\t\t}\n\t\t}\n\t\trs.UserToRoles.Set(account.String(), updatedRoles)\n\t}\n\n\tchain.Emit(\n\t\tRoleRevokedEvent,\n\t\t\"roleName\", r.Name,\n\t\t\"account\", account.String(),\n\t\t\"sender\", caller.String(),\n\t)\n\n\treturn nil\n}\n\n// RenounceRole allows an account to renounce a role it holds. rlm must\n// be the caller's own captured cur; the renouncer is\n// rlm.Previous().Address(). Closing the forgery here matters: without\n// it, any external realm could force a victim to renounce a role by\n// passing the victim's address as caller.\nfunc (rs *Roles) RenounceRole(_ int, rlm realm, name string) error {\n\tr, err := rs.FindRole(name)\n\tif err != nil {\n\t\treturn ErrRoleNotFound\n\t}\n\n\tif !rlm.IsCurrent() {\n\t\treturn ErrAccountNotRole\n\t}\n\tcaller := rlm.Previous().Address()\n\tif !r.HasAccount(caller) {\n\t\treturn ErrAccountNotRole\n\t}\n\n\tr.Holders.Remove(caller.String())\n\n\tchain.Emit(\n\t\tRoleRenouncedEvent,\n\t\t\"roleName\", r.Name,\n\t\t\"account\", caller.String(),\n\t\t\"sender\", caller.String(),\n\t)\n\n\treturn nil\n}\n\n// SetRoleAdmin transfers the ownership of the role to a new\n// administrator. rlm must be the caller's own captured cur and must be\n// the current admin (delegated to ownable.TransferOwnership which\n// re-checks via rlm.Previous().Address()).\nfunc (r *Role) SetRoleAdmin(_ int, rlm realm, admin address) error {\n\tif err := r.Ownable.TransferOwnership(0, rlm, admin); err != nil {\n\t\treturn err\n\t}\n\n\tchain.Emit(\n\t\tRoleSetEvent,\n\t\t\"roleName\", r.Name,\n\t\t\"newAdminRole\", r.Ownable.Owner().String(),\n\t)\n\n\treturn nil\n}\n"},{"name":"accesscontrol_test.gno","body":"package accesscontrol\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/ownable/v0\"\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nvar (\n\tadmin    = testutils.TestAddress(\"admin1\")\n\tnewAdmin = testutils.TestAddress(\"admin2\")\n\tuser1    = testutils.TestAddress(\"user1\")\n\tuser2    = testutils.TestAddress(\"user2\")\n\n\troleName = \"TestRole\"\n)\n\nfunc initSetup(adminAddr address) *Roles {\n\treturn \u0026Roles{\n\t\tRoles:   []*Role{},\n\t\tOwnable: ownable.NewWithAddress(adminAddr),\n\t}\n}\n\n// asAdmin / asUser1 / asUser2 wrap the migrated (_ int, rlm realm)\n// API in a crossing closure under SetRealm(NewUserRealm(addr)) so\n// rlm.Previous().Address() inside the callee resolves to addr.\n\nfunc TestCreateRole(cur realm, t *testing.T) {\n\troles := initSetup(admin)\n\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\tvar (\n\t\trole *Role\n\t\terr  error\n\t)\n\tfunc(cur realm) { role, err = roles.CreateRole(0, cur, roleName) }(cross(cur))\n\tuassert.NoError(t, err)\n\tuassert.NotNil(t, role, \"role should not be nil\")\n\tuassert.Equal(t, role.Name, roleName)\n\n\tfunc(cur realm) { _, err = roles.CreateRole(0, cur, roleName) }(cross(cur))\n\tuassert.Error(t, err, \"should fail on duplicate role creation\")\n}\n\nfunc TestGrantRole(cur realm, t *testing.T) {\n\troles := initSetup(admin)\n\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\tvar err error\n\tfunc(cur realm) { _, err = roles.CreateRole(0, cur, roleName) }(cross(cur))\n\tuassert.NoError(t, err)\n\n\tfunc(cur realm) { err = roles.GrantRole(0, cur, roleName, user1) }(cross(cur))\n\tuassert.NoError(t, err)\n\n\trole, err := roles.FindRole(roleName)\n\tuassert.NoError(t, err)\n\tuassert.True(t, role.HasAccount(user1), \"user1 should have the TestRole\")\n\n\trolesList := roles.UserToRoles.Get(user1.String())\n\tuassert.NotNil(t, rolesList, \"user1 should be in UserToRoles\")\n\tuassert.True(t, containsRole(rolesList.([]*Role), role), \"UserToRoles should contain TestRole for user1\")\n}\n\nfunc TestGrantRoleByNonOwner(cur realm, t *testing.T) {\n\troles := initSetup(admin)\n\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\tvar err error\n\tfunc(cur realm) { _, err = roles.CreateRole(0, cur, roleName) }(cross(cur))\n\tuassert.NoError(t, err)\n\n\t// user2 is not the role admin (admin is); GrantRole should fail.\n\ttesting.SetRealm(testing.NewUserRealm(user2))\n\tfunc(cur realm) { err = roles.GrantRole(0, cur, roleName, user1) }(cross(cur))\n\tuassert.Error(t, err, \"non-owner should not be able to grant roles\")\n}\n\nfunc TestRevokeRole(cur realm, t *testing.T) {\n\troles := initSetup(admin)\n\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\tvar err error\n\tfunc(cur realm) { _, err = roles.CreateRole(0, cur, roleName) }(cross(cur))\n\tuassert.NoError(t, err)\n\tfunc(cur realm) { err = roles.GrantRole(0, cur, roleName, user1) }(cross(cur))\n\tuassert.NoError(t, err)\n\n\tfunc(cur realm) { err = roles.RevokeRole(0, cur, roleName, user1) }(cross(cur))\n\tuassert.NoError(t, err)\n\n\trole, err := roles.FindRole(roleName)\n\tuassert.NoError(t, err)\n\tuassert.False(t, role.HasAccount(user1), \"user1 should no longer have the TestRole\")\n\n\trolesList := roles.UserToRoles.Get(user1.String())\n\tif rolesList != nil {\n\t\tuassert.False(t, containsRole(rolesList.([]*Role), role), \"UserToRoles should not contain TestRole for user1 after revocation\")\n\t}\n}\n\nfunc TestRenounceRole(cur realm, t *testing.T) {\n\troles := initSetup(admin)\n\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\tvar err error\n\tfunc(cur realm) { _, err = roles.CreateRole(0, cur, roleName) }(cross(cur))\n\tuassert.NoError(t, err)\n\tfunc(cur realm) { err = roles.GrantRole(0, cur, roleName, user1) }(cross(cur))\n\tuassert.NoError(t, err)\n\n\t// user1 renounces their own role — must be in user1's context.\n\ttesting.SetRealm(testing.NewUserRealm(user1))\n\tfunc(cur realm) { err = roles.RenounceRole(0, cur, roleName) }(cross(cur))\n\tuassert.NoError(t, err)\n\n\trole, err := roles.FindRole(roleName)\n\tuassert.NoError(t, err)\n\tuassert.False(t, role.HasAccount(user1), \"user1 should have renounced the TestRole\")\n}\n\nfunc TestRenounceRoleByNonHolder(cur realm, t *testing.T) {\n\t// Regression for the address-parameter forgery: previously\n\t// RenounceRole(victim) could be called by any realm to force a\n\t// victim to renounce. After the IsCurrent + rlm.Previous() fix,\n\t// the renouncer is the principal — an attacker cannot strip\n\t// someone else's role.\n\troles := initSetup(admin)\n\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\tvar err error\n\tfunc(cur realm) { _, err = roles.CreateRole(0, cur, roleName) }(cross(cur))\n\tuassert.NoError(t, err)\n\tfunc(cur realm) { err = roles.GrantRole(0, cur, roleName, user1) }(cross(cur))\n\tuassert.NoError(t, err)\n\n\t// user2 tries to \"renounce\" user1's role — must fail because user2\n\t// does not hold the role.\n\ttesting.SetRealm(testing.NewUserRealm(user2))\n\tfunc(cur realm) { err = roles.RenounceRole(0, cur, roleName) }(cross(cur))\n\tuassert.Error(t, err, \"non-holder must not be able to renounce\")\n\n\trole, err := roles.FindRole(roleName)\n\tuassert.NoError(t, err)\n\tuassert.True(t, role.HasAccount(user1), \"user1 must still hold the role\")\n}\n\nfunc TestSetRoleAdmin(cur realm, t *testing.T) {\n\troles := initSetup(admin)\n\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\tvar (\n\t\trole *Role\n\t\terr  error\n\t)\n\tfunc(cur realm) { role, err = roles.CreateRole(0, cur, roleName) }(cross(cur))\n\tuassert.NoError(t, err)\n\n\tfunc(cur realm) { err = role.SetRoleAdmin(0, cur, newAdmin) }(cross(cur))\n\tuassert.NoError(t, err, \"admin change should succeed\")\n\n\tuassert.Equal(t, role.Ownable.Owner(), newAdmin, \"the new admin should be newAdmin\")\n\tuassert.NotEqual(t, role.Ownable.Owner(), admin, \"the old admin should no longer be the owner\")\n}\n\nfunc TestCreateRoleInvalidName(cur realm, t *testing.T) {\n\troles := initSetup(admin)\n\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\tvar err error\n\tfunc(cur realm) { _, err = roles.CreateRole(0, cur, \"\") }(cross(cur))\n\tuassert.Error(t, err, \"should fail on empty role name\")\n\n\tlongRoleName := \"thisisaverylongrolenamethatexceedsthenormallimitfortestingpurposes\"\n\tfunc(cur realm) { _, err = roles.CreateRole(0, cur, longRoleName) }(cross(cur))\n\tuassert.Error(t, err, \"should fail on very long role name\")\n}\n\nfunc TestRevokeRoleByNonOwner(cur realm, t *testing.T) {\n\troles := initSetup(admin)\n\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\tvar err error\n\tfunc(cur realm) { _, err = roles.CreateRole(0, cur, roleName) }(cross(cur))\n\tuassert.NoError(t, err)\n\tfunc(cur realm) { err = roles.GrantRole(0, cur, roleName, user1) }(cross(cur))\n\tuassert.NoError(t, err)\n\n\ttesting.SetRealm(testing.NewUserRealm(user2))\n\tfunc(cur realm) { err = roles.RevokeRole(0, cur, roleName, user1) }(cross(cur))\n\tuassert.Error(t, err, \"non-owner should not be able to revoke roles\")\n}\n\nfunc TestGrantRoleToNonExistentRole(cur realm, t *testing.T) {\n\troles := initSetup(admin)\n\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\tvar err error\n\tfunc(cur realm) { err = roles.GrantRole(0, cur, \"NonExistentRole\", user1) }(cross(cur))\n\tuassert.Error(t, err, \"should fail when granting non-existent role\")\n}\n\nfunc TestRevokeRoleFromNonExistentRole(cur realm, t *testing.T) {\n\troles := initSetup(admin)\n\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\tvar err error\n\tfunc(cur realm) { err = roles.RevokeRole(0, cur, \"NonExistentRole\", user1) }(cross(cur))\n\tuassert.Error(t, err, \"should fail when revoking non-existent role\")\n}\n\nfunc TestRenounceNonExistentRole(cur realm, t *testing.T) {\n\troles := initSetup(admin)\n\n\ttesting.SetRealm(testing.NewUserRealm(user1))\n\tvar err error\n\tfunc(cur realm) { err = roles.RenounceRole(0, cur, \"NonExistentRole\") }(cross(cur))\n\tuassert.Error(t, err, \"should fail when renouncing non-existent role\")\n}\n\nfunc TestDeleteRole(cur realm, t *testing.T) {\n\troles := initSetup(admin)\n\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\tvar (\n\t\trole *Role\n\t\terr  error\n\t)\n\tfunc(cur realm) { role, err = roles.CreateRole(0, cur, roleName) }(cross(cur))\n\tuassert.NoError(t, err)\n\tuassert.NotNil(t, role, \"role should not be nil\")\n\n\troles.Roles = []*Role{} // Clear roles for testing purpose\n\t_, err = roles.FindRole(roleName)\n\tuassert.Error(t, err, \"should fail when trying to find deleted role\")\n}\n\nfunc TestUserToRolesWithMultipleRoles(cur realm, t *testing.T) {\n\troles := initSetup(admin)\n\n\troleName1 := \"Role1\"\n\troleName2 := \"Role2\"\n\n\t// Create two roles\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\tvar err error\n\tfunc(cur realm) { _, err = roles.CreateRole(0, cur, roleName1) }(cross(cur))\n\tuassert.NoError(t, err)\n\tfunc(cur realm) { _, err = roles.CreateRole(0, cur, roleName2) }(cross(cur))\n\tuassert.NoError(t, err)\n\n\t// Grant both roles to user1\n\tfunc(cur realm) { err = roles.GrantRole(0, cur, roleName1, user1) }(cross(cur))\n\tuassert.NoError(t, err)\n\tfunc(cur realm) { err = roles.GrantRole(0, cur, roleName2, user1) }(cross(cur))\n\tuassert.NoError(t, err)\n\n\t// Check if user1 has both roles\n\trolesList := roles.UserToRoles.Get(user1.String())\n\tuassert.NotNil(t, rolesList, \"user1 should be in UserToRoles\")\n\trole1, _ := roles.FindRole(roleName1)\n\trole2, _ := roles.FindRole(roleName2)\n\tuassert.True(t, containsRole(rolesList.([]*Role), role1), \"UserToRoles should contain Role1 for user1\")\n\tuassert.True(t, containsRole(rolesList.([]*Role), role2), \"UserToRoles should contain Role2 for user1\")\n}\n\n// func test for check if a role is in a list of roles\nfunc containsRole(roles []*Role, target *Role) bool {\n\tfor _, role := range roles {\n\t\tif role == target {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"},{"name":"doc.gno","body":"// Package accesscontrol implements a role-based access control (RBAC) system for Gno applications.\n// It provides functionality to create, assign, revoke, and transfer roles.\n//\n// # Usage\n//\n// Import the `gno.land/p/demo/accesscontrol` package to manage roles within your Gno realm. You can create roles,\n// assign them to users, revoke them, and transfer role ownership.\n//\n// Roles can be created by the contract owner using `CreateRole`. Each role is uniquely identified by its name.\n//\n//\tCreateRole(\"editor\")\n//\n// Use `GrantRole` to assign a role to a user, and `RevokeRole` to remove a role from a user.\n//\n//\tGrantRole(\"editor\", userAddress)\n//\n//\tRevokeRole(\"editor\", userAddress)\n//\n// Users can renounce their roles using `RenounceRole`, voluntarily removing themselves from a role.\n//\n//\tRenounceRole(\"editor\")\n//\n// You can look up a role by name with `FindRole`.\n//\n// FindRole(\"editor\")\n//\n// Role ownership can be transferred using `SetRoleAdmin`.\n//\n// SetRoleAdmin(newAdminAddress)\n//\n// Key events\n// - `RoleCreatedEvent`: Triggered when a new role is created\n// Key includes:\n// `roleName` (name of the role)\n// `sender` (address of the sender)\n//\n// - `RoleGrantedEvent`: Triggered when a role is granted to an account\n// Key includes:\n// `roleName` (name of the role)\n// `account` (address of the account)\n// `sender` (address of the sender)\n//\n// - `RoleRevokedEvent`: Triggered when a role is revoked from an account\n// Key includes:\n// `roleName` (name of the role)\n// `account` (address of the account)\n// `sender` (address of the sender)\n//\n// - `RoleRenouncedEvent`: Triggered when a role is renounced by an account\n// Key includes:\n// `roleName` (name of the role)\n// `account` (address of the account)\n//\n// - `RoleSetEvent`: Triggered when a role's administrator is set or changed\n// Key includes:\n// `roleName` (name of the role)\n// `newAdmin` (address of the new administrator)\n// `sender` (address of the sender)\npackage accesscontrol\n"},{"name":"errors.gno","body":"package accesscontrol\n\nimport \"errors\"\n\nvar (\n\tErrNotMatchAccount = errors.New(\"accesscontrol: caller confirmation does not match account\")\n\tErrRoleSameName    = errors.New(\"accesscontrol: role already exists with the same name\")\n\tErrRoleNotFound    = errors.New(\"accesscontrol: role not found\")\n\tErrAccountNotRole  = errors.New(\"accesscontrol: this account does not have the role\")\n\tErrNameRole        = errors.New(\"accesscontrol: role name cannot be empty or exceed 30 characters\")\n\tErrNotOwner        = errors.New(\"accesscontrol: caller is not the owner of the role\")\n)\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/thox/accesscontrol\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"7AQ6VXzDV9tQnx2QblevNonrrl1oqKyPWfuQBpFTIE4W7ERYKHA5949YeAlmiKK9VBrPw2BGKXp10U6fTBhqTQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"snowflake","path":"gno.land/p/thox/snowflake","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/thox/snowflake\"\ngno = \"0.9\"\n"},{"name":"snowflake.gno","body":"// Package snowflake provides utility functions for generating unique identifiers\n// (Snowflake IDs) based on a custom implementation inspired by the Snowflake algorithm (https://pkg.go.dev/github.com/godruoyi/go-snowflake#section-sourcefiles).\n// The Snowflake IDs generated by this package ensure uniqueness and are suitable for\n// distributed systems. The package offers a mechanism to convert these IDs\n// to standard SnowflakeID format strings, making them compatible with systems\n// requiring UUIDs.\n// Add import \"gno.land/p/demo/entropy\" to use the entropy package. For of the pseudo-random number generator.\n// TimestampLength is 42-bit.\n// 42 it's the maximum value that can be returned \"2^42\", It's represent 139 years (2^42 secondes)\n// SequenBits allows for 4096 unique IDs to be generated per millisecond.\n\npackage snowflake\n\nimport (\n\t\"encoding/binary\"\n\t\"encoding/hex\"\n\t\"time\"\n\n\t\"gno.land/p/demo/entropy\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nconst (\n\tTimestampBits uint8  = 42\n\tMachineIDBits uint64 = 10\n\tSequenceBits  uint64 = 12\n\tMaxSequence   uint16 = 1\u003c\u003cSequenceBits - 1\n\tMaxTimestamp  uint64 = 1\u003c\u003cTimestampBits - 1\n\tMaxMachineID  uint64 = 1\u003c\u003cMachineIDBits - 1\n\n\tmachineIDMoveLength = SequenceBits\n\ttimestampMoveLength = MachineIDBits + SequenceBits\n\n\tstartTime uint64 = 1226354400000 // 10 Nov 2008 23:00:00 UTC in milliseconds (Snowflake epoch)\n)\n\n// SnowflakeID struct for generating unique Snowflake-based identifiers.\ntype SnowflakeID struct {\n\tmachineID     uint64\n\tsequence      uint16\n\tstartTime     uint64\n\tlastTimestamp uint64\n\tentropy       *entropy.Instance\n}\n\n// NewSnowflakeID constructs a SnowflakeID with machineID = 0. Callers\n// who want a realm-derived machineID should follow up with\n// SetMachineID(0, cur). Split from the constructor so the constructor\n// stays usable in non-crossing contexts (e.g. package-level var init,\n// where there's no rlm in scope).\nfunc NewSnowflakeID() *SnowflakeID {\n\treturn \u0026SnowflakeID{\n\t\tstartTime: startTime,\n\t\tentropy:   entropy.New(),\n\t}\n}\n\n// GenerateID generates a unique identifier based on the Snowflake algorithm.\n// It combines the current timestamp, machine ID, and a sequence number to ensure uniqueness\n// across distributed systems.\nfunc (s *SnowflakeID) GenerateID() uint64 {\n\tcurrent := s.getTimenow()\n\n\tif current == s.lastTimestamp {\n\t\ts.incrementSequence()\n\t} else {\n\t\ts.sequence = 0\n\t\ts.lastTimestamp = current\n\t}\n\n\telapsedTime := uint64(current-s.startTime) % MaxTimestamp\n\n\tid := (uint64(elapsedTime) \u003c\u003c timestampMoveLength) |\n\t\t(uint64(s.machineID) \u003c\u003c machineIDMoveLength) |\n\t\tuint64(s.incrementSequence())\n\treturn id\n}\n\nfunc (s *SnowflakeID) incrementSequence() uint16 {\n\trandom := uint16(s.entropy.Value())\n\ts.sequence = (s.sequence + 1 + random) \u0026 MaxSequence\n\treturn s.sequence\n}\n\nfunc (s *SnowflakeID) getTimenow() uint64 {\n\treturn uint64(time.Now().UnixMilli())\n}\n\n// SetMachineID derives the machine ID from the caller's address. rlm\n// must be the caller's own captured cur; the caller is\n// rlm.Previous().Address(). Pre-migration used unsafe.PreviousRealm()\n// which a non-crossing wrapper could spoof, producing a machineID\n// computed from the wrong address.\nfunc (s *SnowflakeID) SetMachineID(_ int, rlm realm) {\n\tif !rlm.IsCurrent() {\n\t\tpanic(\"snowflake: rlm does not match the current crossing frame\")\n\t}\n\tcaller := rlm.Previous().Address()\n\tmachineID := uint64(0)\n\tfor _, c := range caller.String() {\n\t\tmachineID += uint64(c)\n\t}\n\tmachineID %= MaxMachineID\n\ts.machineID = machineID\n}\n\nfunc SnowflakeIDToUUIDString(id uint64) string {\n\tbytes := make([]byte, 16)\n\n\t// Copy transformed ID into the second half of the array\n\tcopy(bytes[8:], uint64ToBytes(id))\n\n\t// Use bits from the Snowflake ID to generate the first half of the SnowflakeID\n\tbytes[0] = byte(id \u003e\u003e 60)\n\tbytes[1] = byte(id \u003e\u003e 52)\n\tbytes[2] = byte(id \u003e\u003e 44)\n\tbytes[3] = byte(id \u003e\u003e 36)\n\tbytes[4] = byte(id \u003e\u003e 28)\n\tbytes[5] = byte(id \u003e\u003e 20)\n\tbytes[6] = byte(id \u003e\u003e 12)\n\tbytes[7] = byte(id \u003e\u003e 4)\n\n\t// set the version and variant bits according to SnowflakeID specification.\n\tbytes[6] = (bytes[6] \u0026 0x0f) | 0x40 // version 4 (random)\n\tbytes[8] = (bytes[8] \u0026 0x3f) | 0x80 // variant 1 (RFC 4122)\n\n\thexStr := hex.EncodeToString(bytes)\n\treturn ufmt.Sprintf(\"%s-%s-%s-%s-%s\",\n\t\thexStr[0:8],\n\t\thexStr[8:12],\n\t\thexStr[12:16],\n\t\thexStr[16:20],\n\t\thexStr[20:],\n\t)\n}\n\nfunc uint64ToBytes(i uint64) []byte {\n\tbuf := make([]byte, 8)\n\tbinary.BigEndian.PutUint64(buf, i)\n\treturn buf\n}\n"},{"name":"snowflake_test.gno","body":"package snowflake\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nfunc TestID(cur realm, t *testing.T) {\n\tu := NewSnowflakeID()\n\tfunc(cur realm) { u.SetMachineID(0, cur) }(cross(cur))\n\tid1 := u.GenerateID()\n\tid2 := u.GenerateID()\n\tuuid1 := SnowflakeIDToUUIDString(id1)\n\tuuid2 := SnowflakeIDToUUIDString(id2)\n\n\tuassert.NotEqual(t, uuid1, uuid2)\n\n\tidMap := make(map[uint64]bool)\n\tuuidMap := make(map[string]bool)\n\n\tfor i := 0; i \u003c 101; i++ {\n\t\tid := u.GenerateID()\n\t\tif _, exists := idMap[id]; exists {\n\t\t\tt.Errorf(\"Duplicate ID found: %d\", id)\n\t\t}\n\t\tidMap[id] = true\n\n\t\tuuidStr := SnowflakeIDToUUIDString(id)\n\t\tif _, exists := uuidMap[uuidStr]; exists {\n\t\t\tt.Errorf(\"Duplicate UUID found: %s\", uuidStr)\n\t\t}\n\t\tuuidMap[uuidStr] = true\n\t\tt.Logf(\"Generated UUID %d: %s\", i, uuidStr)\n\t}\n}\n\nfunc TestGetEntropy(cur realm, t *testing.T) {\n\tu := NewSnowflakeID()\n\tinitialEntropyValue := u.entropy.Value()\n\n\tfor i := 0; i \u003c 10; i++ {\n\t\tu.GenerateID()\n\t}\n\n\tuassert.NotEqual(t, initialEntropyValue, u.entropy.Value())\n}\n\n// Output:\n// Generated UUID 0: 0007a63f-a542-4310-807a-63fa54253102\n// Generated UUID 1: 0007a63f-a542-43fe-807a-63fa54253fe1\n// Generated UUID 2: 0007a63f-a542-43e3-807a-63fa54253e34\n// Generated UUID 3: 0007a63f-a542-432f-807a-63fa542532fb\n// Generated UUID 4: 0007a63f-a542-4353-807a-63fa54253536\n// Generated UUID 5: 0007a63f-a542-43be-807a-63fa54253be5\n// Generated UUID 6: 0007a63f-a542-43e0-807a-63fa54253e08\n// Generated UUID 7: 0007a63f-a542-4329-807a-63fa5425329f\n// Generated UUID 8: 0007a63f-a542-430a-807a-63fa542530aa\n// Generated UUID 9: 0007a63f-a542-43f2-807a-63fa54253f29\n// Generated UUID 10: 0007a63f-a542-4351-807a-63fa5425351c\n// .......\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"+dD5bwis54Oi5v4Rh/IE3RY/MVrL0odl8DhJGVeC0jUC34Ip3g6Op8H+a6BUTyqN+MA6gBR0XBLSSQ5cufzUJg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"timelock","path":"gno.land/p/thox/timelock","files":[{"name":"doc.gno","body":"// Package timelock provides a library for scheduling, cancelling, and\n// executing time-locked operations in Gno. It ensures that\n// operations are only carried out after a specified delay and offers\n// mechanisms for managing and verifying the status of these operations.\n// This package leverages an AVL tree for efficient management of timestamps\n// and integrates role-based access control for administrative tasks.\n//\n// # Usage:\n//\n//\timport \"gno.land/p/demo/timelock\"\n//\timport \"gno.land/p/demo/accesscontrol\"\n//\n//\tInitialize timelock utility with an AVL tree and access control.\n//\ttimestamps := avl.NewTree()\n//\tadminRole := accesscontrol.NewRole(\"admin\", address(\"admin-address\"))\n//\ttimeLockUtil := timelock.NewTimeLockUtil(timestamps, adminRole, 30)\n//\n//\tSchedule an operation with a delay of 60 seconds.\n//\tid := seqid.ID()\n//\ttimeLockUtil.Schedule(id, 60)\n//\n//\tCheck if an operation is pending.\n//\tisPending := timeLockUtil.IsPending(id)\n//\n//\tExecute the operation when it is pending.\n//\tif timeLockUtil.IsPending(id) {\n//\t    timeLockUtil.Execute(id)\n//\t}\n//\n//\tUpdate the minimum delay for future operations.\n//\ttimeLockUtil.UpdateDelay(45)\npackage timelock\n"},{"name":"errors.gno","body":"package timelock\n\nimport \"errors\"\n\nvar (\n\tErrNilTimestampsOrAccessControl = errors.New(\"timelock: timestamps and accesscontrol values must be different from nil\")\n\tErrInsufficientDelay            = errors.New(\"timelockutil: Schedule: insufficient delay\")\n\tErrOperationAlreadyScheduled    = errors.New(\"timelockutil: Schedule: operation already scheduled\")\n\tErrOperationNotPending          = errors.New(\"timelock: operation not pending\")\n\tErrUnexpectedType               = errors.New(\"timelockutil: GetTimestamp: unexpected type\")\n\tErrUpadateDelay                 = errors.New(\"timelock: UpdateDelay: only admin can update delay\")\n\tErrOperationCancelNotPending    = errors.New(\"timelock: Cancel: operation not pending\")\n\tErrOperationExecuteNotPending   = errors.New(\"timelock: Execute: operation not pending\")\n)\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/thox/timelock\"\ngno = \"0.9\"\n"},{"name":"timelock.gno","body":"package timelock\n\nimport (\n\t\"chain\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/thox/accesscontrol\"\n)\n\n// Represents the status of a planned operation\ntype OperationState int\n\nconst (\n\tUnset OperationState = iota\n\tPending\n\tReady\n\tDone\n)\n\nfunc (os OperationState) StateToString() string {\n\tswitch os {\n\tcase Unset:\n\t\treturn \"Unset\"\n\tcase Pending:\n\t\treturn \"Pending\"\n\tcase Ready:\n\t\treturn \"Ready\"\n\tcase Done:\n\t\treturn \"Done\"\n\tdefault:\n\t\treturn \"Unknown\"\n\t}\n}\n\n// OperationStatus represents the status of an operation\ntype OperationStatus struct {\n\tsheduleTime int64\n\tisDone      bool\n}\n\n// TimeLock stores the necessary parameters for the timelock operations\ntype TimeLock struct {\n\ttimestamps    *avl.Tree // id -\u003e time.Time\n\taccessControl *accesscontrol.Role\n\tminDelay      uint64\n}\n\n// New instance of TimeLock\nfunc NewTimeLock(timestamps *avl.Tree, accessControl *accesscontrol.Role, minDelay uint64) (*TimeLock, error) {\n\tif timestamps == nil || accessControl == nil {\n\t\treturn nil, ErrNilTimestampsOrAccessControl\n\t}\n\n\treturn \u0026TimeLock{\n\t\ttimestamps:    timestamps,\n\t\taccessControl: accessControl,\n\t\tminDelay:      minDelay,\n\t}, nil\n}\n\n// Schedules an operation to be carried out after a minimum delay\nfunc (tl *TimeLock) Schedule(id seqid.ID, delay uint64) error {\n\tif delay \u003c tl.minDelay {\n\t\treturn ErrInsufficientDelay\n\t}\n\n\tif tl.timestamps.Has(id.Binary()) {\n\t\treturn ErrOperationAlreadyScheduled\n\t}\n\n\ttimestamp := time.Now().Unix() + int64(delay)\n\tstatus := OperationStatus{sheduleTime: timestamp, isDone: false}\n\ttl.timestamps.Set(id.Binary(), status)\n\n\tchain.Emit(\n\t\t\"TimeLockScheduled\",\n\t\t\"id\", id.String(),\n\t\t\"delay\", strconv.FormatInt(int64(delay), 10),\n\t)\n\n\treturn nil\n}\n\n// Remove operation\nfunc (tl *TimeLock) Remove(id seqid.ID) {\n\ttl.timestamps.Remove(id.Binary())\n\n\tchain.Emit(\n\t\t\"TimeLockRemoved\",\n\t\t\"id\", id.String(),\n\t)\n}\n\n// Cancels a planned operation\nfunc (tl *TimeLock) Cancel(id seqid.ID) error {\n\tif !tl.IsPending(id) {\n\t\treturn ErrOperationCancelNotPending\n\t}\n\n\ttl.timestamps.Remove(id.Binary())\n\n\tchain.Emit(\n\t\t\"TimeLockCancelled\",\n\t\t\"id\", id.String(),\n\t)\n\treturn nil\n}\n\n// Executes a pending operation\nfunc (tl *TimeLock) Execute(id seqid.ID) error {\n\tif !tl.IsPending(id) {\n\t\treturn ErrOperationExecuteNotPending\n\t}\n\n\tstatus, err := tl.GetOperationStatus(id)\n\tif err != nil {\n\t\treturn err\n\t}\n\tstatus.isDone = true\n\ttl.timestamps.Set(id.Binary(), status)\n\n\tchain.Emit(\n\t\t\"TimeLockExecuted\",\n\t\t\"id\", id.String(),\n\t)\n\n\treturn nil\n}\n\n// UpdateDelay changes the minimum lead time for future operations. rlm\n// must be the caller's own captured cur; rlm.Previous().Address() must\n// be the TimeLock's owner.\nfunc (tl *TimeLock) UpdateDelay(_ int, rlm realm, newDelay uint64) error {\n\tif !rlm.IsCurrent() {\n\t\treturn ErrUpadateDelay\n\t}\n\tif rlm.Previous().Address() != tl.accessControl.Ownable.Owner() {\n\t\treturn ErrUpadateDelay\n\t}\n\n\tchain.Emit(\n\t\t\"TimeLockMinDelayChanged\",\n\t\t\"oldDelay\", strconv.FormatInt(int64(tl.minDelay), 10),\n\t\t\"newDelay\", strconv.FormatInt(int64(newDelay), 10),\n\t)\n\n\ttl.minDelay = newDelay\n\n\treturn nil\n}\n\n// Checks if an operation is pending\nfunc (tl *TimeLock) IsPending(id seqid.ID) bool {\n\tstate, err := tl.GetOperationState(id)\n\tif err != nil {\n\t\t// Handle the error appropriately; for now, we assume the operation is not pending if there's an error\n\t\tufmt.Errorf(\"Error retrieving operation state: %v\", err)\n\t\treturn false\n\t}\n\n\treturn state == Pending\n}\n\n// Checks if an operation is ready\nfunc (tl *TimeLock) IsReady(id seqid.ID) bool {\n\tstate, err := tl.GetOperationState(id)\n\tif err != nil {\n\t\t// Handle the error appropriately; for now, we assume the operation is not pending if there's an error\n\t\tufmt.Errorf(\"Error retrieving operation state: %v\", err)\n\t\treturn false\n\t}\n\n\treturn state == Ready\n}\n\n// Returns the status of an operation\nfunc (tl *TimeLock) GetOperationState(id seqid.ID) (OperationState, error) {\n\tstatus, err := tl.GetOperationStatus(id)\n\tif err != nil {\n\t\treturn Unset, err\n\t}\n\tif status.isDone {\n\t\treturn Done, nil\n\t}\n\tif status.sheduleTime == 0 {\n\t\treturn Unset, nil\n\t}\n\tif status.sheduleTime \u003e time.Now().Unix() {\n\t\treturn Pending, nil\n\t}\n\treturn Ready, nil\n}\n\n// Returns the status of an operation\nfunc (tl *TimeLock) GetOperationStatus(id seqid.ID) (OperationStatus, error) {\n\tvalue := tl.timestamps.Get(id.Binary())\n\n\tif value == nil {\n\t\treturn OperationStatus{}, nil // Return an empty status if the operation is not found\n\t}\n\tif status, ok := value.(OperationStatus); ok {\n\t\treturn status, nil\n\t} else {\n\t\treturn OperationStatus{}, ErrUnexpectedType\n\t}\n}\n"},{"name":"timelock_test.gno","body":"package timelock\n\nimport (\n\t\"chain/runtime/unsafe\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/thox/accesscontrol\"\n)\n\nfunc TestTimelock(cur realm, t *testing.T) {\n\t// Initialization\n\ttimestamps := avl.NewTree()\n\tminDelay := uint64(2) // 2 seconds to simplify testing\n\taccessControl, _ := accesscontrol.NewRole(\"admin\", unsafe.OriginCaller())\n\ttimelockUtil, err := NewTimeLock(timestamps, accessControl, minDelay)\n\n\t// Generate a new ID from time.Now().UnixNano() with seconds added to guarantee uniqueness\n\tnewID := func(offset int64) seqid.ID {\n\t\treturn seqid.ID(time.Now().UnixNano() + offset)\n\t}\n\n\tuassert.NoError(t, err, \"Failed to create TimeLock instance\")\n\n\t// Test Schedule\n\tt.Run(\"Schedule\", func(t *testing.T) {\n\t\tid := newID(0)\n\t\tdelay := uint64(3) // 3 seconds\n\n\t\terr := timelockUtil.Schedule(id, delay)\n\n\t\tuassert.NoError(t, err, \"Schedule failed\")\n\n\t\tstatus, err := timelockUtil.GetOperationStatus(id)\n\n\t\tuassert.NoError(t, err, \"failed to get operation status\")\n\t\tuassert.NotEmpty(t, status.sheduleTime, \"operation status not set or invalid\")\n\t})\n\n\t// Test Cancel\n\tt.Run(\"Cancel\", func(t *testing.T) {\n\t\tid := newID(1)\n\n\t\t// Plan a new operation to ensure it is unique\n\t\terr := timelockUtil.Schedule(id, uint64(3))\n\t\tuassert.NoError(t, err, \"Failed to schedule operation for cancellation\")\n\n\t\terr = timelockUtil.Cancel(id)\n\t\tuassert.NoError(t, err, \"Cancel failed\")\n\n\t\tstatus, err := timelockUtil.GetOperationStatus(id)\n\t\tuassert.NoError(t, err, \"failed to get operation status\")\n\t\tuassert.Empty(t, status.sheduleTime, \"operation not cancelled\")\n\t})\n\n\t// Test Execute\n\tt.Run(\"Execute\", func(t *testing.T) {\n\t\tid := newID(2)\n\t\tdelay := uint64(3) // 3 seconds\n\t\tfutureTime := time.Now().Unix() + int64(delay)\n\n\t\t// Schedule the operation with a future timestamp\n\t\terr := timelockUtil.Schedule(id, delay)\n\t\tuassert.NoError(t, err, \"Failed to schedule operation for execution\")\n\n\t\t// Simulates the passage of time by setting the timestamp to a future time\n\t\ttimestamps.Set(id.Binary(), OperationStatus{sheduleTime: futureTime, isDone: false})\n\n\t\terr = timelockUtil.Execute(id)\n\t\tuassert.NoError(t, err, \"Execute failed\")\n\n\t\tstate, err := timelockUtil.GetOperationState(id)\n\t\tuassert.NoError(t, err, \"failed to get operation state\")\n\t\tuassert.Equal(t, Done.StateToString(), state.StateToString(), \"operation not executed\")\n\t})\n\n\t// Test UpdateDelay\n\tt.Run(\"UpdateDelay\", func(t *testing.T) {\n\t\tnewDelay := uint64(4) // 4 seconds\n\n\t\t// UpdateDelay now requires the caller to be the timelock owner;\n\t\t// thread cur via a crossing closure under SetRealm(NewUserRealm)\n\t\t// so rlm.Previous().Address() resolves to the test's OriginCaller\n\t\t// (the owner set at NewTimeLock construction).\n\t\ttesting.SetRealm(testing.NewUserRealm(unsafe.OriginCaller()))\n\t\tvar err error\n\t\tfunc(cur realm) { err = timelockUtil.UpdateDelay(0, cur, newDelay) }(cross(cur))\n\t\tuassert.NoError(t, err, \"UpdateDelay failed\")\n\n\t\tuassert.Equal(t, newDelay, timelockUtil.minDelay, \"minDelay not updated\")\n\t})\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"zXCrP0jt926GMRkMUIRWteChnQ/wkcxyC15vWTArMX8w1KGcIpP/PmCC76rNPsBijLwdaH1orKZoYrtwNA8jvA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"xorshiftr128plus","path":"gno.land/p/wyhaines/rand/xorshiftr128plus","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/wyhaines/rand/xorshiftr128plus\"\ngno = \"0.9\"\n"},{"name":"xorshiftr128plus.gno","body":"// Xorshiftr128+ is a very fast psuedo-random number generation algorithm with strong\n// statistical properties.\n//\n// The default random number algorithm in gno was ported from Go's v2 rand implementatoon, which\n// defaults to the PCG algorithm. This algorithm is commonly used in language PRNG implementations\n// because it has modest seeding requirements, and generates statistically strong randomness.\n//\n// This package provides an implementation of the Xorshiftr128+ PRNG algorithm. This algorithm provides\n// strong statistical performance with most seeds (just don't seed it with zeros), and the performance\n// of this implementation in Gno is more than four times faster than the default PCG implementation in\n// `math/rand`.\n//\n//\tBenchmark\n//\t---------\n//\tPCG:           1000000 Uint64 generated in 15.48s\n//\tXorshiftr128+: 1000000 Uint64 generated in 3.22s\n//\tRatio:         x4.81 times faster than PCG\n//\n// Use it directly:\n//\n//\tprng = xorshiftr128plus.New() // pass a uint64 to seed it or pass nothing to seed it with entropy\n//\n// Or use it as a drop-in replacement for the default PRNT in Rand:\n//\n//\tsource = xorshiftr128plus.New()\n//\tprng := rand.New(source)\npackage xorshiftr128plus\n\nimport (\n\t\"errors\"\n\t\"math\"\n\n\t\"gno.land/p/demo/entropy\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\ntype Xorshiftr128Plus struct {\n\tseed [2]uint64 // Seeds\n}\n\nfunc New(seeds ...uint64) *Xorshiftr128Plus {\n\tvar s1, s2 uint64\n\tseed_length := len(seeds)\n\tif seed_length \u003c 2 {\n\t\te := entropy.New()\n\t\tif seed_length == 0 {\n\t\t\ts1 = e.Value64()\n\t\t\ts2 = e.Value64()\n\t\t} else {\n\t\t\ts1 = seeds[0]\n\t\t\ts2 = e.Value64()\n\t\t}\n\t} else {\n\t\ts1 = seeds[0]\n\t\ts2 = seeds[1]\n\t}\n\n\tprng := \u0026Xorshiftr128Plus{}\n\tprng.Seed(s1, s2)\n\treturn prng\n}\n\nfunc (x *Xorshiftr128Plus) Seed(s1, s2 uint64) {\n\tif s1 == 0 \u0026\u0026 s2 == 0 {\n\t\tpanic(\"Seeds must not both be zero\")\n\t}\n\tx.seed[0] = s1\n\tx.seed[1] = s2\n}\n\n// beUint64() decodes a uint64 from a set of eight bytes, assuming big endian encoding.\n// binary.bigEndian.Uint64, copied to avoid dependency\nfunc beUint64(b []byte) uint64 {\n\t_ = b[7] // bounds check hint to compiler; see golang.org/issue/14808\n\treturn uint64(b[7]) | uint64(b[6])\u003c\u003c8 | uint64(b[5])\u003c\u003c16 | uint64(b[4])\u003c\u003c24 |\n\t\tuint64(b[3])\u003c\u003c32 | uint64(b[2])\u003c\u003c40 | uint64(b[1])\u003c\u003c48 | uint64(b[0])\u003c\u003c56\n}\n\n// bePutUint64() encodes a uint64 into a buffer of eight bytes.\n// binary.bigEndian.PutUint64, copied to avoid dependency\nfunc bePutUint64(b []byte, v uint64) {\n\t_ = b[7] // early bounds check to guarantee safety of writes below\n\tb[0] = byte(v \u003e\u003e 56)\n\tb[1] = byte(v \u003e\u003e 48)\n\tb[2] = byte(v \u003e\u003e 40)\n\tb[3] = byte(v \u003e\u003e 32)\n\tb[4] = byte(v \u003e\u003e 24)\n\tb[5] = byte(v \u003e\u003e 16)\n\tb[6] = byte(v \u003e\u003e 8)\n\tb[7] = byte(v)\n}\n\n// A label to identify the marshalled data.\nvar marshalXorshiftr128PlusLabel = []byte(\"xorshiftr128+:\")\n\n// MarshalBinary() returns a byte array that encodes the state of the PRNG. This can later be used\n// with UnmarshalBinary() to restore the state of the PRNG.\n// MarshalBinary implements the encoding.BinaryMarshaler interface.\nfunc (xs *Xorshiftr128Plus) MarshalBinary() ([]byte, error) {\n\tb := make([]byte, 30)\n\tcopy(b, marshalXorshiftr128PlusLabel)\n\tbePutUint64(b[14:], xs.seed[0])\n\tbePutUint64(b[22:], xs.seed[1])\n\treturn b, nil\n}\n\n// errUnmarshalXorshiftr128Plus is returned when unmarshalling fails.\nvar errUnmarshalXorshiftr128Plus = errors.New(\"invalid Xorshiftr128Plus encoding\")\n\n// UnmarshalBinary() restores the state of the PRNG from a byte array that was created with MarshalBinary().\n// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.\nfunc (xs *Xorshiftr128Plus) UnmarshalBinary(data []byte) error {\n\tif len(data) != 30 || string(data[:14]) != string(marshalXorshiftr128PlusLabel) {\n\t\treturn errUnmarshalXorshiftr128Plus\n\t}\n\txs.seed[0] = beUint64(data[14:])\n\txs.seed[1] = beUint64(data[22:])\n\treturn nil\n}\n\nfunc (x *Xorshiftr128Plus) Uint64() uint64 {\n\tx0 := x.seed[0]\n\tx1 := x.seed[1]\n\tx.seed[0] = x1\n\tx0 ^= x0 \u003c\u003c 23\n\tx0 ^= x0 \u003e\u003e 17\n\tx0 ^= x1\n\tx.seed[1] = x0 + x1\n\treturn x.seed[1]\n}\n\n// Until there is better benchmarking support in gno, you can test the performance of this PRNG with this function.\n// This isn't perfect, since it will include the startup time of gno in the results, but this will give you a timing\n// for generating a million random uint64 numbers on any unix based system:\n//\n// `time gno run -expr 'benchmarkXorshiftr128Plus()' xorshiftr128plus.gno\nfunc benchmarkXorshiftr128Plus(_iterations ...int) {\n\titerations := 1000000\n\tif len(_iterations) \u003e 0 {\n\t\titerations = _iterations[0]\n\t}\n\txs128p := New()\n\n\tfor i := 0; i \u003c iterations; i++ {\n\t\t_ = xs128p.Uint64()\n\t}\n\tufmt.Println(ufmt.Sprintf(\"Xorshiftr128Plus: generate %d uint64\\n\", iterations))\n}\n\n// The averageXorshiftr128Plus() function is a simple benchmarking helper to demonstrate\n// the most basic statistical property of the Xorshiftr128+ PRNG.\nfunc averageXorshiftr128Plus(_iterations ...int) {\n\ttarget := uint64(500000)\n\titerations := 1000000\n\tvar squares [1000000]uint64\n\n\tufmt.Println(\n\t\tufmt.Sprintf(\n\t\t\t\"Averaging %d random numbers. The average should be very close to %d.\\n\",\n\t\t\titerations,\n\t\t\ttarget))\n\n\tif len(_iterations) \u003e 0 {\n\t\titerations = _iterations[0]\n\t}\n\txs128p := New()\n\n\tvar average float64 = 0\n\tfor i := 0; i \u003c iterations; i++ {\n\t\tn := xs128p.Uint64()%(target*2) + 1\n\t\taverage += (float64(n) - average) / float64(i+1)\n\t\tsquares[i] = n\n\t}\n\n\tsum_of_squares := uint64(0)\n\t// transform numbers into their squares of the distance from the average\n\tfor i := 0; i \u003c iterations; i++ {\n\t\tdifference := average - float64(squares[i])\n\t\tsquare := uint64(difference * difference)\n\t\tsum_of_squares += square\n\t}\n\n\tufmt.Println(ufmt.Sprintf(\"Xorshiftr128+ average of %d uint64: %f\\n\", iterations, average))\n\tufmt.Println(ufmt.Sprintf(\"Xorshiftr128+ standard deviation  : %f\\n\", math.Sqrt(float64(sum_of_squares)/float64(iterations))))\n\tufmt.Println(ufmt.Sprintf(\"Xorshiftr128+ theoretical perfect deviation: %f\\n\", (float64(target*2)-1)/math.Sqrt(12)))\n}\n"},{"name":"xorshiftr128plus_test.gno","body":"package xorshiftr128plus\n\nimport (\n\t\"math/rand\"\n\t\"testing\"\n)\n\nfunc TestXorshift64StarSeeding(t *testing.T) {\n\trnd := New()\n\tvalue1 := rnd.Uint64()\n\n\trnd = New(987654321)\n\tvalue2 := rnd.Uint64()\n\n\trnd = New(987654321, 9876543210)\n\tvalue3 := rnd.Uint64()\n\n\tif value1 != 4368859828809982745 ||\n\t\tvalue2 != 6152356058823566752 ||\n\t\tvalue3 != 8285073084540510 ||\n\t\tvalue1 == value2 ||\n\t\tvalue2 == value3 ||\n\t\tvalue1 == value3 {\n\t\tt.Errorf(\"Expected three different values\\n  got: %d, %d, %d\", value1, value2, value3)\n\t}\n}\n\nfunc TestXorshiftr128PlusRand(t *testing.T) {\n\trnd := New(987654321)\n\trng := rand.New(rnd)\n\n\t// Expected outputs for the first 5 random floats with the given seed\n\texpected := []float64{\n\t\t0.048735219800779106,\n\t\t0.0372152171449619,\n\t\t0.667254760531175,\n\t\t0.16615979111253953,\n\t\t0.27578895545492665,\n\t\t0.48342823127830337,\n\t\t0.7825693830495895,\n\t\t0.14643955390763952,\n\t\t0.29003469381875835,\n\t\t0.726334398545258,\n\t}\n\n\tfor i, exp := range expected {\n\t\tval := rng.Float64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"Rand.Float64() at iteration %d: got %g, expected %g\", i, val, exp)\n\t\t}\n\t}\n}\n\nfunc TestXorshiftr128PlusUint64(t *testing.T) {\n\trnd := New(987654321, 9876543210)\n\n\texpected := []uint64{\n\t\t8285073084540510,\n\t\t97010855169053386,\n\t\t11353359435625603792,\n\t\t10289232744262291728,\n\t\t14019961444418950453,\n\t\t15829492476941720545,\n\t\t2764732928842099222,\n\t\t6871047144273883379,\n\t\t16142204260470661970,\n\t\t11803223757041229095,\n\t}\n\n\tfor i, exp := range expected {\n\t\tval := rnd.Uint64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"Xorshiftr128Plus.Uint64() at iteration %d: got %d, expected %d\", i, val, exp)\n\t\t}\n\t}\n}\n\nfunc TestXorshiftr128PlusMarshalUnmarshal(t *testing.T) {\n\trnd := New(987654321, 9876543210)\n\n\texpected1 := []uint64{\n\t\t8285073084540510,\n\t\t97010855169053386,\n\t\t11353359435625603792,\n\t\t10289232744262291728,\n\t\t14019961444418950453,\n\t}\n\n\texpected2 := []uint64{\n\t\t15829492476941720545,\n\t\t2764732928842099222,\n\t\t6871047144273883379,\n\t\t16142204260470661970,\n\t\t11803223757041229095,\n\t}\n\n\tfor i, exp := range expected1 {\n\t\tval := rnd.Uint64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"Xorshiftr128Plus.Uint64() at iteration %d: got %d, expected %d\", i, val, exp)\n\t\t}\n\t}\n\n\tmarshalled, err := rnd.MarshalBinary()\n\n\t// t.Logf(\"Original State: [%x]\\n\", rnd.seed)\n\t// t.Logf(\"Marshalled State: [%x] -- %v\\n\", marshalled, err)\n\tstate_before := rnd.seed\n\n\tif err != nil {\n\t\tt.Errorf(\"Xorshiftr128Plus.MarshalBinary() error: %v\", err)\n\t}\n\n\t// Advance state by one number; then check the next 5. The expectation is that they _will_ fail.\n\trnd.Uint64()\n\n\tfor i, exp := range expected2 {\n\t\tval := rnd.Uint64()\n\t\tif exp == val {\n\t\t\tt.Errorf(\"  Iteration %d matched %d; which is from iteration %d; something strange is happening.\", (i + 6), val, (i + 5))\n\t\t}\n\t}\n\n\t// t.Logf(\"State before unmarshall: [%x]\\n\", rnd.seed)\n\n\t// Now restore the state of the PRNG\n\terr = rnd.UnmarshalBinary(marshalled)\n\n\t// t.Logf(\"State after unmarshall: [%x]\\n\", rnd.seed)\n\n\tif state_before != rnd.seed {\n\t\tt.Errorf(\"States before and after marshal/unmarshal are not equal; go %x and %x\", state_before, rnd.seed)\n\t}\n\n\t// Now we should be back on track for the last 5 numbers\n\tfor i, exp := range expected2 {\n\t\tval := rnd.Uint64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"Xorshiftr128Plus.Uint64() at iteration %d: got %d, expected %d\", (i + 5), val, exp)\n\t\t}\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"JDWimtXsMh+3otRTRiZSAtWcobVOQIek2K/ibCL9KVta3p7AATuh4mY47W+oP8Bx8lM79gkmzUOBA7v8nVu1Xg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"isaac","path":"gno.land/p/wyhaines/rand/isaac","files":[{"name":"README.md","body":"# package isaac // import \"gno.land/p/demo/math/rand/isaac\"\n\nThis is a port of the ISAAC cryptographically secure PRNG,\noriginally based on the reference implementation found at\nhttps://burtleburtle.net/bob/rand/isaacafa.html\n\nISAAC has excellent statistical properties, with long cycle times, and\nuniformly distributed, unbiased, and unpredictable number generation. It can\nnot be distinguished from real random data, and in three decades of scrutiny,\nno practical attacks have been found.\n\nThe default random number algorithm in gno was ported from Go's v2 rand\nimplementatoon, which defaults to the PCG algorithm. This algorithm is\ncommonly used in language PRNG implementations because it has modest seeding\nrequirements, and generates statistically strong randomness.\n\nThis package provides an implementation of the 32-bit ISAAC PRNG algorithm. This\nalgorithm provides very strong statistical performance, and is cryptographically\nsecure, while still being substantially faster than the default PCG\nimplementation in `math/rand`. Note that this package does implement a `Uint64()`\nfunction in order to generate a 64 bit number out of two 32 bit numbers. Doing this\nmakes the generator only slightly faster than PCG, however,\n\nNote that the approach to seeing with ISAAC is very important for best results,\nand seeding with ISAAC is not as simple as seeding with a single uint64 value.\nThe ISAAC algorithm requires a 256-element seed. If used for cryptographic\npurposes, this will likely require entropy generated off-chain for actual\ncryptographically secure seeding. For other purposes, however, one can utilize\nthe built-in seeding mechanism, which will leverage the xorshiftr128plus PRNG to\ngenerate any missing seeds if fewer than 256 are provided.\n\n\n```\nBenchmark\n---------\nPCG:         1000000 Uint64 generated in 15.58s\nISAAC:       1000000 Uint64 generated in 13.23s (uint64)\nISAAC:       1000000 Uint32 generated in 6.43s (uint32)\nRatio:       x1.18 times faster than PCG (uint64)\nRatio:       x2.42 times faster than PCG (uint32)\n```\n\nUse it directly:\n\n```\nprng = isaac.New() // pass 0 to 256 uint32 seeds; if fewer than 256 are provided, the rest\n                   // will be generated using the xorshiftr128plus PRNG.\n```\n\nOr use it as a drop-in replacement for the default PRNT in Rand:\n\n```\nsource = isaac.New()\nprng := rand.New(source)\n```\n\n# TYPES\n\n`\ntype ISAAC struct {\n\t// Has unexported fields.\n}\n`\n\n`func New(seeds ...uint32) *ISAAC`\n    ISAAC requires a large, 256-element seed. This implementation will leverage\n    the entropy package combined with the the xorshiftr128plus PRNG to generate\n    any missing seeds of fewer than the required number of arguments are\n    provided.\n\n`func (isaac *ISAAC) MarshalBinary() ([]byte, error)`\n    MarshalBinary() returns a byte array that encodes the state of the PRNG.\n    This can later be used with UnmarshalBinary() to restore the state of the\n    PRNG. MarshalBinary implements the encoding.BinaryMarshaler interface.\n\n`func (isaac *ISAAC) Seed(seed [256]uint32)`\n\n`func (isaac *ISAAC) Uint32() uint32`\n\n`func (isaac *ISAAC) Uint64() uint64`\n\n`func (isaac *ISAAC) UnmarshalBinary(data []byte) error`\n    UnmarshalBinary() restores the state of the PRNG from a byte array\n    that was created with MarshalBinary(). UnmarshalBinary implements the\n    encoding.BinaryUnmarshaler interface.\n\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/wyhaines/rand/isaac\"\ngno = \"0.9\"\n"},{"name":"isaac.gno","body":"// This is a port of the ISAAC cryptographically secure PRNG, originally based on the reference\n// implementation found at https://burtleburtle.net/bob/rand/isaacafa.html\n//\n// ISAAC has excellent statistical properties, with long cycle times, and uniformly distributed,\n// unbiased, and unpredictable number generation. It can not be distinguished from real random\n// data, and in three decades of scrutiny, no practical attacks have been found.\n//\n// The default random number algorithm in gno was ported from Go's v2 rand implementation, which\n// defaults to the PCG algorithm. This algorithm is commonly used in language PRNG implementations\n// because it has modest seeding requirements, and generates statistically strong randomness.\n//\n// This package provides an implementation of the 32-bit ISAAC PRNG algorithm. This\n// algorithm provides very strong statistical performance, and is cryptographically\n// secure, while still being substantially faster than the default PCG\n// implementation in `math/rand`. Note that this package does implement a `Uint64()`\n// function in order to generate a 64 bit number out of two 32 bit numbers. Doing this\n// makes the generator only slightly faster than PCG, however,\n//\n// Note that the approach to seeing with ISAAC is very important for best results, and seeding with\n// ISAAC is not as simple as seeding with a single uint64 value. The ISAAC algorithm requires a\n// 256-element seed. If used for cryptographic purposes, this will likely require entropy generated\n// off-chain for actual cryptographically secure seeding. For other purposes, however, one can\n// utilize the built-in seeding mechanism, which will leverage the xorshiftr128plus PRNG to generate\n// any missing seeds if fewer than 256 are provided.\n//\n//\t\tBenchmark\n//\t\t---------\n//\t\tPCG:         1000000 Uint64 generated in 15.58s\n//\t\tISAAC:       1000000 Uint64 generated in 13.23s\n//\t\tISAAC:       1000000 Uint32 generated in 6.43s\n//\t     Ratio:       x1.18 times faster than PCG (uint64)\n//\t     Ratio:       x2.42 times faster than PCG (uint32)\n//\n// Use it directly:\n//\n//\t\tprng = isaac.New() // pass 0 to 256 uint32 seeds; if fewer than 256 are provided, the rest\n//\t                    // will be generated using the xorshiftr128plus PRNG.\n//\n// Or use it as a drop-in replacement for the default PRNG in Rand:\n//\n//\tsource = isaac.New()\n//\tprng := rand.New(source)\npackage isaac\n\nimport (\n\t\"errors\"\n\t\"math\"\n\t\"math/rand\"\n\n\t\"gno.land/p/demo/entropy\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/wyhaines/rand/xorshiftr128plus\"\n)\n\ntype ISAAC struct {\n\trandrsl    [256]uint32\n\trandcnt    uint32\n\tmm         [256]uint32\n\taa, bb, cc uint32\n\tseed       [256]uint32\n}\n\n// ISAAC requires a large, 256-element seed. This implementation will leverage the entropy\n// package combined with the the xorshiftr128plus PRNG to generate any missing seeds of\n// fewer than the required number of arguments are provided.\nfunc New(seeds ...uint32) *ISAAC {\n\tisaac := \u0026ISAAC{}\n\tseed := [256]uint32{}\n\n\tindex := 0\n\tfor index = 0; index \u003c len(seeds); index++ {\n\t\tseed[index] = seeds[index]\n\t}\n\n\tif index \u003c 4 {\n\t\te := entropy.New()\n\t\tfor ; index \u003c 4; index++ {\n\t\t\tseed[index] = e.Value()\n\t\t}\n\t}\n\n\t// Use up to the first four seeds as seeding inputs for xorshiftr128+, in order to\n\t// use it to provide any remaining missing seeds.\n\tprng := xorshiftr128plus.New(\n\t\t(uint64(seed[0])\u003c\u003c32)|uint64(seed[1]),\n\t\t(uint64(seed[2])\u003c\u003c32)|uint64(seed[3]),\n\t)\n\tfor ; index \u003c 256; index += 2 {\n\t\tval := prng.Uint64()\n\t\tseed[index] = uint32(val \u0026 0xffffffff)\n\t\tif index+1 \u003c 256 {\n\t\t\tseed[index+1] = uint32(val \u003e\u003e 32)\n\t\t}\n\t}\n\tisaac.Seed(seed)\n\treturn isaac\n}\n\nfunc (isaac *ISAAC) Seed(seed [256]uint32) {\n\tisaac.randrsl = seed\n\tisaac.seed = seed\n\tisaac.randinit(true)\n}\n\n// beUint32() decodes a uint32 from a set of four bytes, assuming big endian encoding.\n// binary.bigEndian.Uint32, copied to avoid dependency\nfunc beUint32(b []byte) uint32 {\n\t_ = b[3] // bounds check hint to compiler; see golang.org/issue/14808\n\treturn uint32(b[3]) | uint32(b[2])\u003c\u003c8 | uint32(b[1])\u003c\u003c16 | uint32(b[0])\u003c\u003c24\n}\n\n// bePutUint32() encodes a uint64 into a buffer of eight bytes.\n// binary.bigEndian.PutUint32, copied to avoid dependency\nfunc bePutUint32(b []byte, v uint32) {\n\t_ = b[3] // early bounds check to guarantee safety of writes below\n\tb[0] = byte(v \u003e\u003e 24)\n\tb[1] = byte(v \u003e\u003e 16)\n\tb[2] = byte(v \u003e\u003e 8)\n\tb[3] = byte(v)\n}\n\n// A label to identify the marshalled data.\nvar marshalISAACLabel = []byte(\"isaac:\")\n\n// MarshalBinary() returns a byte array that encodes the state of the PRNG. This can later be used\n// with UnmarshalBinary() to restore the state of the PRNG.\n// MarshalBinary implements the encoding.BinaryMarshaler interface.\nfunc (isaac *ISAAC) MarshalBinary() ([]byte, error) {\n\tb := make([]byte, 3094) // 6 + 1024 + 1024 + 1024 + 4 + 4 + 4 + 4 == 3090\n\tcopy(b, marshalISAACLabel)\n\tfor i := 0; i \u003c 256; i++ {\n\t\tbePutUint32(b[6+i*4:], isaac.seed[i])\n\t}\n\tfor i := 256; i \u003c 512; i++ {\n\t\tbePutUint32(b[6+i*4:], isaac.randrsl[i-256])\n\t}\n\tfor i := 512; i \u003c 768; i++ {\n\t\tbePutUint32(b[6+i*4:], isaac.mm[i-512])\n\t}\n\tbePutUint32(b[3078:], isaac.aa)\n\tbePutUint32(b[3082:], isaac.bb)\n\tbePutUint32(b[3086:], isaac.cc)\n\tbePutUint32(b[3090:], isaac.randcnt)\n\n\treturn b, nil\n}\n\n// errUnmarshalISAAC is returned when unmarshalling fails.\nvar errUnmarshalISAAC = errors.New(\"invalid ISAAC encoding\")\n\n// UnmarshalBinary() restores the state of the PRNG from a byte array that was created with MarshalBinary().\n// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.\nfunc (isaac *ISAAC) UnmarshalBinary(data []byte) error {\n\tif len(data) != 3094 || string(data[:6]) != string(marshalISAACLabel) {\n\t\treturn errUnmarshalISAAC\n\t}\n\tfor i := 0; i \u003c 256; i++ {\n\t\tisaac.seed[i] = beUint32(data[6+i*4:])\n\t}\n\tfor i := 256; i \u003c 512; i++ {\n\t\tisaac.randrsl[i-256] = beUint32(data[6+i*4:])\n\t}\n\tfor i := 512; i \u003c 768; i++ {\n\t\tisaac.mm[i-512] = beUint32(data[6+i*4:])\n\t}\n\tisaac.aa = beUint32(data[3078:])\n\tisaac.bb = beUint32(data[3082:])\n\tisaac.cc = beUint32(data[3086:])\n\tisaac.randcnt = beUint32(data[3090:])\n\treturn nil\n}\n\nfunc (isaac *ISAAC) randinit(flag bool) {\n\tisaac.aa = 0\n\tisaac.bb = 0\n\tisaac.cc = 0\n\n\tvar a, b, c, d, e, f, g, h uint32 = 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, 0x9e3779b9\n\n\tfor i := 0; i \u003c 4; i++ {\n\t\ta ^= b \u003c\u003c 11\n\t\td += a\n\t\tb += c\n\t\tb ^= c \u003e\u003e 2\n\t\te += b\n\t\tc += d\n\t\tc ^= d \u003c\u003c 8\n\t\tf += c\n\t\td += e\n\t\td ^= e \u003e\u003e 16\n\t\tg += d\n\t\te += f\n\t\te ^= f \u003c\u003c 10\n\t\th += e\n\t\tf += g\n\t\tf ^= g \u003e\u003e 4\n\t\ta += f\n\t\tg += h\n\t\tg ^= h \u003c\u003c 8\n\t\tb += g\n\t\th += a\n\t\th ^= a \u003e\u003e 9\n\t\tc += h\n\t\ta += b\n\t}\n\n\tfor i := 0; i \u003c 256; i += 8 {\n\t\tif flag {\n\t\t\ta += isaac.randrsl[i]\n\t\t\tb += isaac.randrsl[i+1]\n\t\t\tc += isaac.randrsl[i+2]\n\t\t\td += isaac.randrsl[i+3]\n\t\t\te += isaac.randrsl[i+4]\n\t\t\tf += isaac.randrsl[i+5]\n\t\t\tg += isaac.randrsl[i+6]\n\t\t\th += isaac.randrsl[i+7]\n\t\t}\n\n\t\ta ^= b \u003c\u003c 11\n\t\td += a\n\t\tb += c\n\t\tb ^= c \u003e\u003e 2\n\t\te += b\n\t\tc += d\n\t\tc ^= d \u003c\u003c 8\n\t\tf += c\n\t\td += e\n\t\td ^= e \u003e\u003e 16\n\t\tg += d\n\t\te += f\n\t\te ^= f \u003c\u003c 10\n\t\th += e\n\t\tf += g\n\t\tf ^= g \u003e\u003e 4\n\t\ta += f\n\t\tg += h\n\t\tg ^= h \u003c\u003c 8\n\t\tb += g\n\t\th += a\n\t\th ^= a \u003e\u003e 9\n\t\tc += h\n\t\ta += b\n\n\t\tisaac.mm[i] = a\n\t\tisaac.mm[i+1] = b\n\t\tisaac.mm[i+2] = c\n\t\tisaac.mm[i+3] = d\n\t\tisaac.mm[i+4] = e\n\t\tisaac.mm[i+5] = f\n\t\tisaac.mm[i+6] = g\n\t\tisaac.mm[i+7] = h\n\t}\n\n\tif flag {\n\t\tfor i := 0; i \u003c 256; i += 8 {\n\t\t\ta += isaac.mm[i]\n\t\t\tb += isaac.mm[i+1]\n\t\t\tc += isaac.mm[i+2]\n\t\t\td += isaac.mm[i+3]\n\t\t\te += isaac.mm[i+4]\n\t\t\tf += isaac.mm[i+5]\n\t\t\tg += isaac.mm[i+6]\n\t\t\th += isaac.mm[i+7]\n\n\t\t\ta ^= b \u003c\u003c 11\n\t\t\td += a\n\t\t\tb += c\n\t\t\tb ^= c \u003e\u003e 2\n\t\t\te += b\n\t\t\tc += d\n\t\t\tc ^= d \u003c\u003c 8\n\t\t\tf += c\n\t\t\td += e\n\t\t\td ^= e \u003e\u003e 16\n\t\t\tg += d\n\t\t\te += f\n\t\t\te ^= f \u003c\u003c 10\n\t\t\th += e\n\t\t\tf += g\n\t\t\tf ^= g \u003e\u003e 4\n\t\t\ta += f\n\t\t\tg += h\n\t\t\tg ^= h \u003c\u003c 8\n\t\t\tb += g\n\t\t\th += a\n\t\t\th ^= a \u003e\u003e 9\n\t\t\tc += h\n\t\t\ta += b\n\n\t\t\tisaac.mm[i] = a\n\t\t\tisaac.mm[i+1] = b\n\t\t\tisaac.mm[i+2] = c\n\t\t\tisaac.mm[i+3] = d\n\t\t\tisaac.mm[i+4] = e\n\t\t\tisaac.mm[i+5] = f\n\t\t\tisaac.mm[i+6] = g\n\t\t\tisaac.mm[i+7] = h\n\t\t}\n\t}\n\n\tisaac.isaac()\n\tisaac.randcnt = uint32(256)\n}\n\nfunc (isaac *ISAAC) isaac() {\n\tisaac.cc++\n\tisaac.bb += isaac.cc\n\n\tfor i := 0; i \u003c 256; i++ {\n\t\tx := isaac.mm[i]\n\t\tswitch i % 4 {\n\t\tcase 0:\n\t\t\tisaac.aa ^= isaac.aa \u003c\u003c 13\n\t\tcase 1:\n\t\t\tisaac.aa ^= isaac.aa \u003e\u003e 6\n\t\tcase 2:\n\t\t\tisaac.aa ^= isaac.aa \u003c\u003c 2\n\t\tcase 3:\n\t\t\tisaac.aa ^= isaac.aa \u003e\u003e 16\n\t\t}\n\t\tisaac.aa += isaac.mm[(i+128)\u00260xff]\n\n\t\ty := isaac.mm[(x\u003e\u003e2)\u00260xff] + isaac.aa + isaac.bb\n\t\tisaac.mm[i] = y\n\t\tisaac.bb = isaac.mm[(y\u003e\u003e10)\u00260xff] + x\n\t\tisaac.randrsl[i] = isaac.bb\n\t}\n}\n\n// Returns a random uint32.\nfunc (isaac *ISAAC) Uint32() uint32 {\n\tif isaac.randcnt == uint32(0) {\n\t\tisaac.isaac()\n\t\tisaac.randcnt = uint32(256)\n\t}\n\tisaac.randcnt--\n\treturn isaac.randrsl[isaac.randcnt]\n}\n\n// Returns a random uint64 by combining two uint32s.\nfunc (isaac *ISAAC) Uint64() uint64 {\n\treturn uint64(isaac.Uint32()) | (uint64(isaac.Uint32()) \u003c\u003c 32)\n}\n\n// Until there is better benchmarking support in gno, you can test the performance of this PRNG with this function.\n// This isn't perfect, since it will include the startup time of gno in the results, but this will give you a timing\n// for generating a million random uint64 numbers on any unix based system:\n//\n// `time gno run -expr 'benchmarkISAAC()' xorshift64star.gno\nfunc benchmarkISAAC(_iterations ...int) {\n\titerations := 1000000\n\tif len(_iterations) \u003e 0 {\n\t\titerations = _iterations[0]\n\t}\n\tisaac := New()\n\n\tfor i := 0; i \u003c iterations; i++ {\n\t\t_ = isaac.Uint64()\n\t}\n\tufmt.Println(ufmt.Sprintf(\"ISAAC: generate %d uint64\\n\", iterations))\n}\n\n// The averageISAAC() function is a simple benchmarking helper to demonstrate\n// the most basic statistical property of the ISAAC PRNG.\nfunc averageISAAC(_iterations ...int) {\n\ttarget := uint64(500000)\n\titerations := 1000000\n\tvar squares [1000000]uint64\n\n\tufmt.Println(\n\t\tufmt.Sprintf(\n\t\t\t\"Averaging %d random numbers. The average should be very close to %d.\\n\",\n\t\t\titerations,\n\t\t\ttarget))\n\n\tif len(_iterations) \u003e 0 {\n\t\titerations = _iterations[0]\n\t}\n\tisaac := New(987654321, 123456789, 999999999, 111111111)\n\n\tvar average float64 = 0\n\tfor i := 0; i \u003c iterations; i++ {\n\t\tn := isaac.Uint64()%(target*2) + 1\n\t\taverage += (float64(n) - average) / float64(i+1)\n\t\tsquares[i] = n\n\t}\n\n\tsum_of_squares := uint64(0)\n\t// transform numbers into their squares of the distance from the average\n\tfor i := 0; i \u003c iterations; i++ {\n\t\tdifference := average - float64(squares[i])\n\t\tsquare := uint64(difference * difference)\n\t\tsum_of_squares += square\n\t}\n\n\tufmt.Println(ufmt.Sprintf(\"ISAAC average of %d uint64: %f\\n\", iterations, average))\n\tufmt.Println(ufmt.Sprintf(\"ISAAC standard deviation  : %f\\n\", math.Sqrt(float64(sum_of_squares)/float64(iterations))))\n\tufmt.Println(ufmt.Sprintf(\"ISAAC theoretical perfect deviation: %f\\n\", (float64(target*2)-1)/math.Sqrt(12)))\n}\n\nfunc averagePCG(_iterations ...int) {\n\ttarget := uint64(500000)\n\titerations := 1000000\n\tvar squares [1000000]uint64\n\n\tufmt.Println(\n\t\tufmt.Sprintf(\n\t\t\t\"Averaging %d random numbers. The average should be very close to %d.\\n\",\n\t\t\titerations,\n\t\t\ttarget))\n\n\tif len(_iterations) \u003e 0 {\n\t\titerations = _iterations[0]\n\t}\n\tisaac := rand.NewPCG(987654321, 123456789)\n\n\tvar average float64 = 0\n\tfor i := 0; i \u003c iterations; i++ {\n\t\tn := isaac.Uint64()%(target*2) + 1\n\t\taverage += (float64(n) - average) / float64(i+1)\n\t\tsquares[i] = n\n\t}\n\n\tsum_of_squares := uint64(0)\n\t// transform numbers into their squares of the distance from the average\n\tfor i := 0; i \u003c iterations; i++ {\n\t\tdifference := average - float64(squares[i])\n\t\tsquare := uint64(difference * difference)\n\t\tsum_of_squares += square\n\t}\n\n\tufmt.Println(ufmt.Sprintf(\"PCG average of %d uint64: %f\\n\", iterations, average))\n\tufmt.Println(ufmt.Sprintf(\"PCG standard deviation  : %f\\n\", math.Sqrt(float64(sum_of_squares)/float64(iterations))))\n\tufmt.Println(ufmt.Sprintf(\"PCG theoretical perfect deviation: %f\\n\", (float64(target*2)-1)/math.Sqrt(12)))\n}\n"},{"name":"isaac_test.gno","body":"package isaac\n\nimport (\n\t\"math/rand\"\n\t\"testing\"\n)\n\ntype OpenISAAC struct {\n\tRandrsl    [256]uint32\n\tRandcnt    uint32\n\tMm         [256]uint32\n\tAa, Bb, Cc uint32\n\tSeed       [256]uint32\n}\n\nfunc TestISAACSeeding(t *testing.T) {\n\t_ = New()\n}\n\nfunc TestISAACRand(t *testing.T) {\n\trnd := New(987654321)\n\trng := rand.New(rnd)\n\n\texpected := []float64{\n\t\t0.3590173976876423,\n\t\t0.7045500585814575,\n\t\t0.3307624938209778,\n\t\t0.9174646414250772,\n\t\t0.11232269485263391,\n\t\t0.9276658847827113,\n\t\t0.9561549853128902,\n\t\t0.3921638978394879,\n\t\t0.9824881209760224,\n\t\t0.8213784955963486,\n\t}\n\n\tfor i, exp := range expected {\n\t\tval := rng.Float64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"Rand.Float64() at iteration %d: got %g, expected %g\", i, val, exp)\n\t\t}\n\t}\n}\n\nfunc TestISAACUint64(t *testing.T) {\n\trnd := New(1000)\n\n\texpected := []uint64{\n\t\t13706738165129397958,\n\t\t11158865851759859683,\n\t\t7282911433372880595,\n\t\t10191257834247701829,\n\t\t6756510588635422211,\n\t\t9188469355127567259,\n\t\t3870407692778398450,\n\t\t7510499000403643056,\n\t\t11921506945015596058,\n\t\t5594436529078496461,\n\t}\n\n\tfor i, exp := range expected {\n\t\tval := rnd.Uint64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"ISAAC.Uint64() at iteration %d: got %d, expected %d\", i, val, exp)\n\t\t}\n\t}\n}\n\nfunc dupState(i *ISAAC) *OpenISAAC {\n\tstate := \u0026OpenISAAC{}\n\tstate.Seed = i.seed\n\tstate.Randrsl = i.randrsl\n\tstate.Mm = i.mm\n\tstate.Aa = i.aa\n\tstate.Bb = i.bb\n\tstate.Cc = i.cc\n\tstate.Randcnt = i.randcnt\n\n\treturn state\n}\n\nfunc TestISAACMarshalUnmarshal(t *testing.T) {\n\trnd := New(1001)\n\n\texpected1 := []uint64{\n\t\t15520355889829550420,\n\t\t14048062122838424762,\n\t\t17386984608929258959,\n\t\t6631892771034928162,\n\t\t10939419587267807737,\n\t}\n\n\texpected2 := []uint64{\n\t\t3835981378089717525,\n\t\t13955658256492919532,\n\t\t10077846634918708781,\n\t\t5773204650675356768,\n\t\t4971527406542890875,\n\t}\n\n\tfor i, exp := range expected1 {\n\t\tval := rnd.Uint64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"ISAAC.Uint64() at iteration %d: got %d, expected %d\", i, val, exp)\n\t\t}\n\t}\n\n\tmarshalled, err := rnd.MarshalBinary()\n\n\t// t.Logf(\"State: [%v]\\n\", dupState(rnd))\n\t// t.Logf(\"Marshalled State: [%x] -- %v\\n\", marshalled, err)\n\tstate_before := dupState(rnd)\n\n\tif err != nil {\n\t\tt.Errorf(\"ISAAC.MarshalBinary() error: %v\", err)\n\t}\n\n\t// Advance state by one number; then check the next 5. The expectation is that they _will_ fail.\n\trnd.Uint64()\n\n\tfor i, exp := range expected2 {\n\t\tval := rnd.Uint64()\n\t\tif exp == val {\n\t\t\tt.Errorf(\"  Iteration %d matched %d; which is from iteration %d; something strange is happening.\", (i + 6), val, (i + 5))\n\t\t}\n\t}\n\n\t// t.Logf(\"State before unmarshall: [%v]\\n\", dupState(rnd))\n\n\t// Now restore the state of the PRNG\n\terr = rnd.UnmarshalBinary(marshalled)\n\n\t// t.Logf(\"State after unmarshall: [%v]\\n\", dupState(rnd))\n\n\tif state_before.Seed != dupState(rnd).Seed {\n\t\tt.Errorf(\"Seed mismatch\")\n\t}\n\tif state_before.Randrsl != dupState(rnd).Randrsl {\n\t\tt.Errorf(\"Randrsl mismatch\")\n\t}\n\tif state_before.Mm != dupState(rnd).Mm {\n\t\tt.Errorf(\"Mm mismatch\")\n\t}\n\tif state_before.Aa != dupState(rnd).Aa {\n\t\tt.Errorf(\"Aa mismatch\")\n\t}\n\tif state_before.Bb != dupState(rnd).Bb {\n\t\tt.Errorf(\"Bb mismatch\")\n\t}\n\tif state_before.Cc != dupState(rnd).Cc {\n\t\tt.Errorf(\"Cc mismatch\")\n\t}\n\tif state_before.Randcnt != dupState(rnd).Randcnt {\n\t\tt.Errorf(\"Randcnt mismatch\")\n\t}\n\n\t// Now we should be back on track for the last 5 numbers\n\tfor i, exp := range expected2 {\n\t\tval := rnd.Uint64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"ISAAC.Uint64() at iteration %d: got %d, expected %d\", (i + 5), val, exp)\n\t\t}\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"FeRUCuvGjLe2MQ4fdcDDd6B9jTvegL3oc0lWyLpbC0AkV4vkHNHvsr67PQYs9LrBwoYmzPjberg9U1cKULkpZg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"isaac64","path":"gno.land/p/wyhaines/rand/isaac64","files":[{"name":"README.md","body":"# package isaac64 // import \"gno.land/p/demo/math/rand/isaac64\"\n\nThis is a port of the 64-bit version of the ISAAC cryptographically\nsecure PRNG, originally based on the reference implementation found at\nhttps://burtleburtle.net/bob/rand/isaacafa.html\n\nISAAC has excellent statistical properties, with long cycle times, and\nuniformly distributed, unbiased, and unpredictable number generation. It can\nnot be distinguished from real random data, and in three decades of scrutiny,\nno practical attacks have been found.\n\nThe default random number algorithm in gno was ported from Go's v2 rand\nimplementatoon, which defaults to the PCG algorithm. This algorithm is\ncommonly used in language PRNG implementations because it has modest seeding\nrequirements, and generates statistically strong randomness.\n\nThis package provides an implementation of the 64-bit ISAAC PRNG algorithm. This\nalgorithm provides very strong statistical performance, and is cryptographically\nsecure, while still being substantially faster than the default PCG\nimplementation in `math/rand`.\n\nNote that the approach to seeing with ISAAC is very important for best results,\nand seeding with ISAAC is not as simple as seeding with a single uint64 value.\nThe ISAAC algorithm requires a 256-element seed. If used for cryptographic\npurposes, this will likely require entropy generated off-chain for actual\ncryptographically secure seeding. For other purposes, however, one can utilize\nthe built-in seeding mechanism, which will leverage the xorshiftr128plus PRNG to\ngenerate any missing seeds if fewer than 256 are provided.\n\n\n```\nBenchmark\n---------\nPCG:         1000000 Uint64 generated in 15.58s\nISAAC:       1000000 Uint64 generated in 8.95s\nISAAC:       1000000 Uint32 generated in 7.66s\nRatio:       x1.74 times faster than PCG (uint64)\nRatio:       x2.03 times faster than PCG (uint32)\n```\n\nUse it directly:\n\n\n```\nprng = isaac.New() // pass 0 to 256 uint64 seeds; if fewer than 256 are provided, the rest\n                   // will be generated using the xorshiftr128plus PRNG.\n```\n\nOr use it as a drop-in replacement for the default PRNT in Rand:\n\n```\nsource = isaac64.New()\nprng := rand.New(source)\n```\n\n## CONSTANTS\n\n\n```\nconst (\n\tRANDSIZL = 8\n\tRANDSIZ  = 1 \u003c\u003c RANDSIZL // 256\n)\n```\n\n## TYPES\n\n\n```\ntype ISAAC struct {\n\t// Has unexported fields.\n}\n```\n\n`func New(seeds ...uint64) *ISAAC`\nISAAC requires a large, 256-element seed. This implementation will leverage\nthe entropy package combined with the xorshiftr128plus PRNG to generate any\nmissing seeds if fewer than the required number of arguments are provided.\n\n`func (isaac *ISAAC) MarshalBinary() ([]byte, error)`\nMarshalBinary() returns a byte array that encodes the state of the PRNG.\nThis can later be used with UnmarshalBinary() to restore the state of the\nPRNG. MarshalBinary implements the encoding.BinaryMarshaler interface.\n\n`func (isaac *ISAAC) Seed(seed [256]uint64)`\nReinitialize the generator with a new seed. A seed must be composed of 256 uint64.\n\n`func (isaac *ISAAC) Uint32() uint32`\nReturn a 32 bit random integer, composed of the high 32 bits of the generated 32 bit result.\n\n`func (isaac *ISAAC) Uint64() uint64`\nReturn a 64 bit random integer.\n\n`func (isaac *ISAAC) UnmarshalBinary(data []byte) error`\nUnmarshalBinary() restores the state of the PRNG from a byte array\nthat was created with MarshalBinary(). UnmarshalBinary implements the\nencoding.BinaryUnmarshaler interface.\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/wyhaines/rand/isaac64\"\ngno = \"0.9\"\n"},{"name":"isaac64.gno","body":"// This is a port of the 64-bit version of the ISAAC cryptographically secure PRNG, originally\n// based on the reference implementation found at https://burtleburtle.net/bob/rand/isaacafa.html\n//\n// ISAAC has excellent statistical properties, with long cycle times, and uniformly distributed,\n// unbiased, and unpredictable number generation. It can not be distinguished from real random\n// data, and in three decades of scrutiny, no practical attacks have been found.\n//\n// The default random number algorithm in gno was ported from Go's v2 rand implementatoon, which\n// defaults to the PCG algorithm. This algorithm is commonly used in language PRNG implementations\n// because it has modest seeding requirements, and generates statistically strong randomness.\n//\n// This package provides an implementation of the 64-bit ISAAC PRNG algorithm. This algorithm\n// provides very strong statistical performance, and is cryptographically secure, while still\n// being substantially faster than the default PCG implementation in `math/rand`.\n//\n// Note that the approach to seeing with ISAAC is very important for best results, and seeding with\n// ISAAC is not as simple as seeding with a single uint64 value. The ISAAC algorithm requires a\n// 256-element seed. If used for cryptographic purposes, this will likely require entropy generated\n// off-chain for actual cryptographically secure seeding. For other purposes, however, one can\n// utilize the built-in seeding mechanism, which will leverage the xorshiftr128plus PRNG to generate\n// any missing seeds if fewer than 256 are provided.\n//\n//\t\tBenchmark\n//\t\t---------\n//\t\tPCG:         1000000 Uint64 generated in 15.58s\n//\t\tISAAC:       1000000 Uint64 generated in 8.95s\n//\t        ISAAC:       1000000 Uint32 generated in 7.66s\n//\t\tRatio:       x1.74 times faster than PCG (uint64)\n//\t        Ratio:       x2.03 times faster than PCG (uint32)\n//\n// Use it directly:\n//\n//\t\tprng = isaac.New() // pass 0 to 256 uint64 seeds; if fewer than 256 are provided, the rest\n//\t                    // will be generated using the xorshiftr128plus PRNG.\n//\n// Or use it as a drop-in replacement for the default PRNT in Rand:\n//\n//\tsource = isaac64.New()\n//\tprng := rand.New(source)\npackage isaac64\n\nimport (\n\t\"errors\"\n\t\"math\"\n\n\t\"gno.land/p/demo/entropy\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/wyhaines/rand/xorshiftr128plus\"\n)\n\nconst (\n\tRANDSIZL = 8\n\tRANDSIZ  = 1 \u003c\u003c RANDSIZL // 256\n)\n\ntype ISAAC struct {\n\trandrsl    [256]uint64\n\trandcnt    uint64\n\tmm         [256]uint64\n\taa, bb, cc uint64\n\tseed       [256]uint64\n}\n\n// ISAAC requires a large, 256-element seed. This implementation will leverage the entropy\n// package combined with the xorshiftr128plus PRNG to generate any missing seeds if fewer than\n// the required number of arguments are provided.\nfunc New(seeds ...uint64) *ISAAC {\n\tisaac := \u0026ISAAC{}\n\tseed := [256]uint64{}\n\n\tindex := 0\n\tfor index = 0; index \u003c len(seeds) \u0026\u0026 index \u003c 256; index++ {\n\t\tseed[index] = seeds[index]\n\t}\n\n\tif index \u003c 2 {\n\t\te := entropy.New()\n\t\tfor ; index \u003c 2; index++ {\n\t\t\tseed[index] = e.Value64()\n\t\t}\n\t}\n\n\t// Use the first two seeds as seeding inputs for xorshiftr128plus, in order to\n\t// use it to provide any remaining missing seeds.\n\tprng := xorshiftr128plus.New(\n\t\tseed[0],\n\t\tseed[1],\n\t)\n\tfor ; index \u003c 256; index++ {\n\t\tseed[index] = prng.Uint64()\n\t}\n\tisaac.Seed(seed)\n\treturn isaac\n}\n\n// Reinitialize the generator with a new seed. A seed must be composed of 256 uint64.\nfunc (isaac *ISAAC) Seed(seed [256]uint64) {\n\tisaac.randrsl = seed\n\tisaac.seed = seed\n\tisaac.randinit(true)\n}\n\n// beUint64() decodes a uint64 from a set of eight bytes, assuming big endian encoding.\nfunc beUint64(b []byte) uint64 {\n\t_ = b[7] // bounds check hint to compiler\n\treturn uint64(b[7]) | uint64(b[6])\u003c\u003c8 | uint64(b[5])\u003c\u003c16 | uint64(b[4])\u003c\u003c24 |\n\t\tuint64(b[3])\u003c\u003c32 | uint64(b[2])\u003c\u003c40 | uint64(b[1])\u003c\u003c48 | uint64(b[0])\u003c\u003c56\n}\n\n// bePutUint64() encodes a uint64 into a buffer of eight bytes.\nfunc bePutUint64(b []byte, v uint64) {\n\t_ = b[7] // early bounds check to guarantee safety of writes below\n\tb[0] = byte(v \u003e\u003e 56)\n\tb[1] = byte(v \u003e\u003e 48)\n\tb[2] = byte(v \u003e\u003e 40)\n\tb[3] = byte(v \u003e\u003e 32)\n\tb[4] = byte(v \u003e\u003e 24)\n\tb[5] = byte(v \u003e\u003e 16)\n\tb[6] = byte(v \u003e\u003e 8)\n\tb[7] = byte(v)\n}\n\n// A label to identify the marshalled data.\nvar marshalISAACLabel = []byte(\"isaac:\")\n\n// MarshalBinary() returns a byte array that encodes the state of the PRNG. This can later be used\n// with UnmarshalBinary() to restore the state of the PRNG.\n// MarshalBinary implements the encoding.BinaryMarshaler interface.\nfunc (isaac *ISAAC) MarshalBinary() ([]byte, error) {\n\tb := make([]byte, 6+2048*3+8*3+8) // 6 + 2048*3 + 8*3 + 8 == 6182\n\tcopy(b, marshalISAACLabel)\n\toffset := 6\n\tfor i := 0; i \u003c 256; i++ {\n\t\tbePutUint64(b[offset:], isaac.seed[i])\n\t\toffset += 8\n\t}\n\tfor i := 0; i \u003c 256; i++ {\n\t\tbePutUint64(b[offset:], isaac.randrsl[i])\n\t\toffset += 8\n\t}\n\tfor i := 0; i \u003c 256; i++ {\n\t\tbePutUint64(b[offset:], isaac.mm[i])\n\t\toffset += 8\n\t}\n\tbePutUint64(b[offset:], isaac.aa)\n\toffset += 8\n\tbePutUint64(b[offset:], isaac.bb)\n\toffset += 8\n\tbePutUint64(b[offset:], isaac.cc)\n\toffset += 8\n\tbePutUint64(b[offset:], isaac.randcnt)\n\treturn b, nil\n}\n\n// errUnmarshalISAAC is returned when unmarshalling fails.\nvar errUnmarshalISAAC = errors.New(\"invalid ISAAC encoding\")\n\n// UnmarshalBinary() restores the state of the PRNG from a byte array that was created with MarshalBinary().\n// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.\nfunc (isaac *ISAAC) UnmarshalBinary(data []byte) error {\n\tif len(data) != 6182 || string(data[:6]) != string(marshalISAACLabel) {\n\t\treturn errUnmarshalISAAC\n\t}\n\toffset := 6\n\tfor i := 0; i \u003c 256; i++ {\n\t\tisaac.seed[i] = beUint64(data[offset:])\n\t\toffset += 8\n\t}\n\tfor i := 0; i \u003c 256; i++ {\n\t\tisaac.randrsl[i] = beUint64(data[offset:])\n\t\toffset += 8\n\t}\n\tfor i := 0; i \u003c 256; i++ {\n\t\tisaac.mm[i] = beUint64(data[offset:])\n\t\toffset += 8\n\t}\n\tisaac.aa = beUint64(data[offset:])\n\toffset += 8\n\tisaac.bb = beUint64(data[offset:])\n\toffset += 8\n\tisaac.cc = beUint64(data[offset:])\n\toffset += 8\n\tisaac.randcnt = beUint64(data[offset:])\n\treturn nil\n}\n\nfunc (isaac *ISAAC) randinit(flag bool) {\n\tvar a, b, c, d, e, f, g, h uint64\n\tisaac.aa = 0\n\tisaac.bb = 0\n\tisaac.cc = 0\n\n\ta = 0x9e3779b97f4a7c13\n\tb = 0x9e3779b97f4a7c13\n\tc = 0x9e3779b97f4a7c13\n\td = 0x9e3779b97f4a7c13\n\te = 0x9e3779b97f4a7c13\n\tf = 0x9e3779b97f4a7c13\n\tg = 0x9e3779b97f4a7c13\n\th = 0x9e3779b97f4a7c13\n\n\t// scramble it\n\tfor i := 0; i \u003c 4; i++ {\n\t\tmix(\u0026a, \u0026b, \u0026c, \u0026d, \u0026e, \u0026f, \u0026g, \u0026h)\n\t}\n\n\t// fill in mm[] with messy stuff\n\tfor i := 0; i \u003c RANDSIZ; i += 8 {\n\t\tif flag {\n\t\t\ta += isaac.randrsl[i]\n\t\t\tb += isaac.randrsl[i+1]\n\t\t\tc += isaac.randrsl[i+2]\n\t\t\td += isaac.randrsl[i+3]\n\t\t\te += isaac.randrsl[i+4]\n\t\t\tf += isaac.randrsl[i+5]\n\t\t\tg += isaac.randrsl[i+6]\n\t\t\th += isaac.randrsl[i+7]\n\t\t}\n\t\tmix(\u0026a, \u0026b, \u0026c, \u0026d, \u0026e, \u0026f, \u0026g, \u0026h)\n\t\tisaac.mm[i] = a\n\t\tisaac.mm[i+1] = b\n\t\tisaac.mm[i+2] = c\n\t\tisaac.mm[i+3] = d\n\t\tisaac.mm[i+4] = e\n\t\tisaac.mm[i+5] = f\n\t\tisaac.mm[i+6] = g\n\t\tisaac.mm[i+7] = h\n\t}\n\n\tif flag {\n\t\t// do a second pass to make all of the seed affect all of mm\n\t\tfor i := 0; i \u003c RANDSIZ; i += 8 {\n\t\t\ta += isaac.mm[i]\n\t\t\tb += isaac.mm[i+1]\n\t\t\tc += isaac.mm[i+2]\n\t\t\td += isaac.mm[i+3]\n\t\t\te += isaac.mm[i+4]\n\t\t\tf += isaac.mm[i+5]\n\t\t\tg += isaac.mm[i+6]\n\t\t\th += isaac.mm[i+7]\n\t\t\tmix(\u0026a, \u0026b, \u0026c, \u0026d, \u0026e, \u0026f, \u0026g, \u0026h)\n\t\t\tisaac.mm[i] = a\n\t\t\tisaac.mm[i+1] = b\n\t\t\tisaac.mm[i+2] = c\n\t\t\tisaac.mm[i+3] = d\n\t\t\tisaac.mm[i+4] = e\n\t\t\tisaac.mm[i+5] = f\n\t\t\tisaac.mm[i+6] = g\n\t\t\tisaac.mm[i+7] = h\n\t\t}\n\t}\n\n\tisaac.isaac()\n\tisaac.randcnt = RANDSIZ\n}\n\nfunc mix(a, b, c, d, e, f, g, h *uint64) {\n\t*a -= *e\n\t*f ^= *h \u003e\u003e 9\n\t*h += *a\n\n\t*b -= *f\n\t*g ^= *a \u003c\u003c 9\n\t*a += *b\n\n\t*c -= *g\n\t*h ^= *b \u003e\u003e 23\n\t*b += *c\n\n\t*d -= *h\n\t*a ^= *c \u003c\u003c 15\n\t*c += *d\n\n\t*e -= *a\n\t*b ^= *d \u003e\u003e 14\n\t*d += *e\n\n\t*f -= *b\n\t*c ^= *e \u003c\u003c 20\n\t*e += *f\n\n\t*g -= *c\n\t*d ^= *f \u003e\u003e 17\n\t*f += *g\n\n\t*h -= *d\n\t*e ^= *g \u003c\u003c 14\n\t*g += *h\n}\n\nfunc ind(mm []uint64, x uint64) uint64 {\n\treturn mm[(x\u003e\u003e3)\u0026(RANDSIZ-1)]\n}\n\nfunc (isaac *ISAAC) isaac() {\n\tvar a, b, x, y uint64\n\ta = isaac.aa\n\tb = isaac.bb + isaac.cc + 1\n\tisaac.cc++\n\n\tm := isaac.mm[:]\n\tr := isaac.randrsl[:]\n\n\tvar i, m2Index int\n\n\t// First half\n\tfor i = 0; i \u003c RANDSIZ/2; i++ {\n\t\tm2Index = i + RANDSIZ/2\n\t\tswitch i % 4 {\n\t\tcase 0:\n\t\t\ta = ^(a ^ (a \u003c\u003c 21)) + m[m2Index]\n\t\tcase 1:\n\t\t\ta = (a ^ (a \u003e\u003e 5)) + m[m2Index]\n\t\tcase 2:\n\t\t\ta = (a ^ (a \u003c\u003c 12)) + m[m2Index]\n\t\tcase 3:\n\t\t\ta = (a ^ (a \u003e\u003e 33)) + m[m2Index]\n\t\t}\n\t\tx = m[i]\n\t\ty = ind(m, x) + a + b\n\t\tm[i] = y\n\t\tb = ind(m, y\u003e\u003eRANDSIZL) + x\n\t\tr[i] = b\n\t}\n\n\t// Second half\n\tfor i = RANDSIZ / 2; i \u003c RANDSIZ; i++ {\n\t\tm2Index = i - RANDSIZ/2\n\t\tswitch i % 4 {\n\t\tcase 0:\n\t\t\ta = ^(a ^ (a \u003c\u003c 21)) + m[m2Index]\n\t\tcase 1:\n\t\t\ta = (a ^ (a \u003e\u003e 5)) + m[m2Index]\n\t\tcase 2:\n\t\t\ta = (a ^ (a \u003c\u003c 12)) + m[m2Index]\n\t\tcase 3:\n\t\t\ta = (a ^ (a \u003e\u003e 33)) + m[m2Index]\n\t\t}\n\t\tx = m[i]\n\t\ty = ind(m, x) + a + b\n\t\tm[i] = y\n\t\tb = ind(m, y\u003e\u003eRANDSIZL) + x\n\t\tr[i] = b\n\t}\n\n\tisaac.bb = b\n\tisaac.aa = a\n}\n\n// Return a 64 bit random integer.\nfunc (isaac *ISAAC) Uint64() uint64 {\n\tif isaac.randcnt == 0 {\n\t\tisaac.isaac()\n\t\tisaac.randcnt = RANDSIZ\n\t}\n\tisaac.randcnt--\n\treturn isaac.randrsl[isaac.randcnt]\n}\n\nvar gencycle int = 0\nvar bufferFor32 uint64 = uint64(0)\n\n// Return a 32 bit random integer, composed of the high 32 bits of the generated 32 bit result.\nfunc (isaac *ISAAC) Uint32() uint32 {\n\tif gencycle == 0 {\n\t\tbufferFor32 = isaac.Uint64()\n\t\tgencycle = 1\n\t\treturn uint32(bufferFor32 \u003e\u003e 32)\n\t}\n\n\tgencycle = 0\n\treturn uint32(bufferFor32 \u0026 0xffffffff)\n}\n\n// Until there is better benchmarking support in gno, you can test the performance of this PRNG with this function.\n// This isn't perfect, since it will include the startup time of gno in the results, but this will give you a timing\n// for generating a million random uint64 numbers on any unix based system:\n//\n// `time gno run -expr 'benchmarkISAAC()' isaac64.gno\nfunc benchmarkISAAC(_iterations ...int) {\n\titerations := 1000000\n\tif len(_iterations) \u003e 0 {\n\t\titerations = _iterations[0]\n\t}\n\tisaac := New()\n\n\tfor i := 0; i \u003c iterations; i++ {\n\t\t_ = isaac.Uint64()\n\t}\n\tufmt.Println(ufmt.Sprintf(\"ISAAC: generated %d uint64\\n\", iterations))\n}\n\n// The averageISAAC() function is a simple benchmarking helper to demonstrate\n// the most basic statistical property of the ISAAC PRNG.\nfunc averageISAAC(_iterations ...int) {\n\ttarget := uint64(500000)\n\titerations := 1000000\n\n\tufmt.Println(\n\t\tufmt.Sprintf(\n\t\t\t\"Averaging %d random numbers. The average should be very close to %d.\\n\",\n\t\t\titerations,\n\t\t\ttarget))\n\n\tif len(_iterations) \u003e 0 {\n\t\titerations = _iterations[0]\n\t}\n\tisaac := New(987654321987654321, 123456789987654321, 1, 997755331886644220)\n\n\tvar average float64 = 0\n\tvar squares []uint64 = make([]uint64, iterations)\n\tfor i := 0; i \u003c iterations; i++ {\n\t\tn := isaac.Uint64()%(target*2) + 1\n\t\taverage += (float64(n) - average) / float64(i+1)\n\t\tsquares[i] = n\n\t}\n\n\tsum_of_squares := uint64(0)\n\t// transform numbers into their squares of the distance from the average\n\tfor i := 0; i \u003c iterations; i++ {\n\t\tdifference := average - float64(squares[i])\n\t\tsquare := uint64(difference * difference)\n\t\tsum_of_squares += square\n\t}\n\n\tufmt.Println(ufmt.Sprintf(\"ISAAC average of %d uint64: %f\\n\", iterations, average))\n\tufmt.Println(ufmt.Sprintf(\"ISAAC standard deviation  : %f\\n\", math.Sqrt(float64(sum_of_squares)/float64(iterations))))\n\tufmt.Println(ufmt.Sprintf(\"ISAAC theoretical perfect deviation: %f\\n\", (float64(target*2)-1)/math.Sqrt(12)))\n}\n"},{"name":"isaac64_test.gno","body":"package isaac64\n\nimport (\n\t\"math/rand\"\n\t\"testing\"\n)\n\ntype OpenISAAC struct {\n\tRandrsl    [256]uint64\n\tRandcnt    uint64\n\tMm         [256]uint64\n\tAa, Bb, Cc uint64\n\tSeed       [256]uint64\n}\n\nfunc TestISAACSeeding(t *testing.T) {\n\t_ = New()\n}\n\nfunc TestISAACRand(t *testing.T) {\n\trnd := New(987654321)\n\trng := rand.New(rnd)\n\n\t// Expected outputs for the first 5 random floats with the given seed\n\texpected := []float64{\n\t\t0.2818878834295122,\n\t\t0.8575461830821571,\n\t\t0.9878021063787968,\n\t\t0.6503544780116336,\n\t\t0.5158329690433359,\n\t\t0.7959152461588924,\n\t\t0.5432366486934906,\n\t\t0.824665978209607,\n\t\t0.8615372170680458,\n\t\t0.22954589404739578,\n\t}\n\n\tfor i, exp := range expected {\n\t\tval := rng.Float64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"Rand.Float64() at iteration %d: got %g, expected %g\", i, val, exp)\n\t\t}\n\t}\n}\n\nfunc TestISAACUint64(t *testing.T) {\n\trnd := New(1000)\n\n\texpected := []uint64{\n\t\t10083220283665581455,\n\t\t10039389761195725041,\n\t\t6820016387036140989,\n\t\t6784213597523088182,\n\t\t13120722600477653778,\n\t\t3491117614651563646,\n\t\t1297676147275528930,\n\t\t15006384980354042338,\n\t\t3104467119059991036,\n\t\t4914319123654344819,\n\t}\n\n\tfor i, exp := range expected {\n\t\tval := rnd.Uint64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"ISAAC.Uint64() at iteration %d: got %d, expected %d\", i, val, exp)\n\t\t}\n\t}\n}\n\nfunc dupState(i *ISAAC) *OpenISAAC {\n\tstate := \u0026OpenISAAC{}\n\tstate.Seed = i.seed\n\tstate.Randrsl = i.randrsl\n\tstate.Mm = i.mm\n\tstate.Aa = i.aa\n\tstate.Bb = i.bb\n\tstate.Cc = i.cc\n\tstate.Randcnt = i.randcnt\n\n\treturn state\n}\n\nfunc TestISAACMarshalUnmarshal(t *testing.T) {\n\trnd := New(1001)\n\n\texpected1 := []uint64{\n\t\t4398183556077595549,\n\t\t14479654616302101831,\n\t\t15852653767232940552,\n\t\t2801765968457115882,\n\t\t8875575139772470433,\n\t}\n\n\texpected2 := []uint64{\n\t\t17583056722733587141,\n\t\t16906215529544723388,\n\t\t7599862885469865851,\n\t\t9623269843822592805,\n\t\t4311429062865512072,\n\t}\n\n\tfor i, exp := range expected1 {\n\t\tval := rnd.Uint64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"ISAAC.Uint64() at iteration %d: got %d, expected %d\", i, val, exp)\n\t\t}\n\t}\n\n\tmarshalled, err := rnd.MarshalBinary()\n\n\t// t.Logf(\"State: [%v]\\n\", dupState(rnd))\n\t// t.Logf(\"Marshalled State: [%x] -- %v\\n\", marshalled, err)\n\tstate_before := dupState(rnd)\n\n\tif err != nil {\n\t\tt.Errorf(\"ISAAC.MarshalBinary() error: %v\", err)\n\t}\n\n\t// Advance state by one number; then check the next 5. The expectation is that they _will_ fail.\n\trnd.Uint64()\n\n\tfor i, exp := range expected2 {\n\t\tval := rnd.Uint64()\n\t\tif exp == val {\n\t\t\tt.Errorf(\"  Iteration %d matched %d; which is from iteration %d; something strange is happening.\", (i + 6), val, (i + 5))\n\t\t}\n\t}\n\n\t// t.Logf(\"State before unmarshall: [%v]\\n\", dupState(rnd))\n\n\t// Now restore the state of the PRNG\n\terr = rnd.UnmarshalBinary(marshalled)\n\n\t// t.Logf(\"State after unmarshall: [%v]\\n\", dupState(rnd))\n\n\tif state_before.Seed != dupState(rnd).Seed {\n\t\tt.Errorf(\"Seed mismatch\")\n\t}\n\tif state_before.Randrsl != dupState(rnd).Randrsl {\n\t\tt.Errorf(\"Randrsl mismatch\")\n\t}\n\tif state_before.Mm != dupState(rnd).Mm {\n\t\tt.Errorf(\"Mm mismatch\")\n\t}\n\tif state_before.Aa != dupState(rnd).Aa {\n\t\tt.Errorf(\"Aa mismatch\")\n\t}\n\tif state_before.Bb != dupState(rnd).Bb {\n\t\tt.Errorf(\"Bb mismatch\")\n\t}\n\tif state_before.Cc != dupState(rnd).Cc {\n\t\tt.Errorf(\"Cc mismatch\")\n\t}\n\tif state_before.Randcnt != dupState(rnd).Randcnt {\n\t\tt.Errorf(\"Randcnt mismatch\")\n\t}\n\n\t// Now we should be back on track for the last 5 numbers\n\tfor i, exp := range expected2 {\n\t\tval := rnd.Uint64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"ISAAC.Uint64() at iteration %d: got %d, expected %d\", (i + 5), val, exp)\n\t\t}\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"ldDb7xd3tgnO8LNfQS8oyay8FjFtcczMJ1pFEHXIljhW5Bsu+T6AAbNjDaJ3YzMfMGAd4wG+sEU1yySvRyVHow=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"xorshift64star","path":"gno.land/p/wyhaines/rand/xorshift64star","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/wyhaines/rand/xorshift64star\"\ngno = \"0.9\"\n"},{"name":"xorshift64star.gno","body":"// Xorshift64* is a very fast psuedo-random number generation algorithm with strong\n// statistical properties.\n//\n// The default random number algorithm in gno was ported from Go's v2 rand implementatoon, which\n// defaults to the PCG algorithm. This algorithm is commonly used in language PRNG implementations\n// because it has modest seeding requirements, and generates statistically strong randomness.\n//\n// This package provides an implementation of the Xorshift64* PRNG algorithm. This algorithm provides\n// strong statistical performance with most seeds (just don't seed it with zero), and the performance\n// of this implementation in Gno is more than four times faster than the default PCG implementation in\n// `math/rand`.\n//\n//\tBenchmark\n//\t---------\n//\tPCG:         1000000 Uint64 generated in 15.58s\n//\tXorshift64*: 1000000 Uint64 generated in 3.77s\n//\tRatio:       x4.11 times faster than PCG\n//\n// Use it directly:\n//\n//\tprng = xorshift64star.New() // pass a uint64 to seed it or pass nothing to seed it with entropy\n//\n// Or use it as a drop-in replacement for the default PRNT in Rand:\n//\n//\tsource = xorshift64star.New()\n//\tprng := rand.New(source)\npackage xorshift64star\n\nimport (\n\t\"errors\"\n\t\"math\"\n\n\t\"gno.land/p/demo/entropy\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Xorshift64Star is a PRNG that implements the Xorshift64* algorithm.\ntype Xorshift64Star struct {\n\tseed uint64\n}\n\n// New() creates a new instance of the PRNG with a given seed, which\n// should be a uint64. If no seed is provided, the PRNG will be seeded via the\n// gno.land/p/demo/entropy package.\nfunc New(seed ...uint64) *Xorshift64Star {\n\txs := \u0026Xorshift64Star{}\n\txs.Seed(seed...)\n\treturn xs\n}\n\n// Seed() implements the rand.Source interface. It provides a way to set the seed for the PRNG.\nfunc (xs *Xorshift64Star) Seed(seed ...uint64) {\n\tif len(seed) == 0 {\n\t\te := entropy.New()\n\t\txs.seed = e.Value64()\n\t} else {\n\t\txs.seed = seed[0]\n\t}\n}\n\n// beUint64() decodes a uint64 from a set of eight bytes, assuming big endian encoding.\n// binary.bigEndian.Uint64, copied to avoid dependency\nfunc beUint64(b []byte) uint64 {\n\t_ = b[7] // bounds check hint to compiler; see golang.org/issue/14808\n\treturn uint64(b[7]) | uint64(b[6])\u003c\u003c8 | uint64(b[5])\u003c\u003c16 | uint64(b[4])\u003c\u003c24 |\n\t\tuint64(b[3])\u003c\u003c32 | uint64(b[2])\u003c\u003c40 | uint64(b[1])\u003c\u003c48 | uint64(b[0])\u003c\u003c56\n}\n\n// bePutUint64() encodes a uint64 into a buffer of eight bytes.\n// binary.bigEndian.PutUint64, copied to avoid dependency\nfunc bePutUint64(b []byte, v uint64) {\n\t_ = b[7] // early bounds check to guarantee safety of writes below\n\tb[0] = byte(v \u003e\u003e 56)\n\tb[1] = byte(v \u003e\u003e 48)\n\tb[2] = byte(v \u003e\u003e 40)\n\tb[3] = byte(v \u003e\u003e 32)\n\tb[4] = byte(v \u003e\u003e 24)\n\tb[5] = byte(v \u003e\u003e 16)\n\tb[6] = byte(v \u003e\u003e 8)\n\tb[7] = byte(v)\n}\n\n// A label to identify the marshalled data.\nvar marshalXorshift64StarLabel = []byte(\"xorshift64*:\")\n\n// MarshalBinary() returns a byte array that encodes the state of the PRNG. This can later be used\n// with UnmarshalBinary() to restore the state of the PRNG.\n// MarshalBinary implements the encoding.BinaryMarshaler interface.\nfunc (xs *Xorshift64Star) MarshalBinary() ([]byte, error) {\n\tb := make([]byte, 20)\n\tcopy(b, marshalXorshift64StarLabel)\n\tbePutUint64(b[12:], xs.seed)\n\treturn b, nil\n}\n\n// errUnmarshalXorshift64Star is returned when unmarshalling fails.\nvar errUnmarshalXorshift64Star = errors.New(\"invalid Xorshift64* encoding\")\n\n// UnmarshalBinary() restores the state of the PRNG from a byte array that was created with MarshalBinary().\n// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.\nfunc (xs *Xorshift64Star) UnmarshalBinary(data []byte) error {\n\tif len(data) != 20 || string(data[:12]) != string(marshalXorshift64StarLabel) {\n\t\treturn errUnmarshalXorshift64Star\n\t}\n\txs.seed = beUint64(data[12:])\n\treturn nil\n}\n\n// Uint64() generates the next random uint64 value.\nfunc (xs *Xorshift64Star) Uint64() uint64 {\n\txs.seed ^= xs.seed \u003e\u003e 12\n\txs.seed ^= xs.seed \u003c\u003c 25\n\txs.seed ^= xs.seed \u003e\u003e 27\n\txs.seed *= 2685821657736338717\n\treturn xs.seed // Operations naturally wrap around in uint64\n}\n\n// Until there is better benchmarking support in gno, you can test the performance of this PRNG with this function.\n// This isn't perfect, since it will include the startup time of gno in the results, but this will give you a timing\n// for generating a million random uint64 numbers on any unix based system:\n//\n// `time gno run -expr 'benchmarkXorshift64Star()' xorshift64star.gno\nfunc benchmarkXorshift64Star(_iterations ...int) {\n\titerations := 1000000\n\tif len(_iterations) \u003e 0 {\n\t\titerations = _iterations[0]\n\t}\n\txs64s := New()\n\n\tfor i := 0; i \u003c iterations; i++ {\n\t\t_ = xs64s.Uint64()\n\t}\n\tufmt.Println(ufmt.Sprintf(\"Xorshift64*: generate %d uint64\\n\", iterations))\n}\n\n// The averageXorshift64Star() function is a simple benchmarking helper to demonstrate\n// the most basic statistical property of the Xorshift64* PRNG.\nfunc averageXorshift64Star(_iterations ...int) {\n\ttarget := uint64(500000)\n\titerations := 1000000\n\tvar squares [1000000]uint64\n\n\tufmt.Println(\n\t\tufmt.Sprintf(\n\t\t\t\"Averaging %d random numbers. The average should be very close to %d.\\n\",\n\t\t\titerations,\n\t\t\ttarget))\n\n\tif len(_iterations) \u003e 0 {\n\t\titerations = _iterations[0]\n\t}\n\txs64s := New()\n\n\tvar average float64 = 0\n\tfor i := 0; i \u003c iterations; i++ {\n\t\tn := xs64s.Uint64()%(target*2) + 1\n\t\taverage += (float64(n) - average) / float64(i+1)\n\t\tsquares[i] = n\n\t}\n\n\tsum_of_squares := uint64(0)\n\t// transform numbers into their squares of the distance from the average\n\tfor i := 0; i \u003c iterations; i++ {\n\t\tdifference := average - float64(squares[i])\n\t\tsquare := uint64(difference * difference)\n\t\tsum_of_squares += square\n\t}\n\n\tufmt.Println(ufmt.Sprintf(\"Xorshift64* average of %d uint64: %f\\n\", iterations, average))\n\tufmt.Println(ufmt.Sprintf(\"Xorshift64* standard deviation  : %f\\n\", math.Sqrt(float64(sum_of_squares)/float64(iterations))))\n\tufmt.Println(ufmt.Sprintf(\"Xorshift64* theoretical perfect deviation: %f\\n\", (float64(target*2)-1)/math.Sqrt(12)))\n}\n"},{"name":"xorshift64star_test.gno","body":"package xorshift64star\n\nimport (\n\t\"math/rand\"\n\t\"testing\"\n)\n\nfunc TestXorshift64StarSeeding(t *testing.T) {\n\trnd := New()\n\tvalue1 := rnd.Uint64()\n\n\trnd = New(987654321)\n\tvalue2 := rnd.Uint64()\n\n\tif value1 != 3506338452768534464 || value2 != 18211065302896784785 || value1 == value2 {\n\t\tt.Errorf(\"Expected 2 different values; got: %d and %d\", value1, value2)\n\t}\n}\n\nfunc TestXorshift64StarRand(t *testing.T) {\n\trnd := New(987654321)\n\trng := rand.New(rnd)\n\n\t// Expected outputs for the first 5 random floats with the given seed\n\texpected := []float64{\n\t\t.8344002228310946,\n\t\t0.01777174153236205,\n\t\t0.23521769507865276,\n\t\t0.5387610198576143,\n\t\t0.631539862225968,\n\t\t0.9369068148346704,\n\t\t0.6387002315083188,\n\t\t0.5047507613688854,\n\t\t0.5208486273732391,\n\t\t0.25023746271541747,\n\t}\n\n\tfor i, exp := range expected {\n\t\tval := rng.Float64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"Rand.Float64() at iteration %d: got %g, expected %g\", i, val, exp)\n\t\t}\n\t}\n}\n\nfunc TestXorshift64StarUint64(t *testing.T) {\n\trnd := New(1000)\n\n\texpected := []uint64{\n\t\t12487186097140327178,\n\t\t14661465266242046735,\n\t\t14694269887751719025,\n\t\t15763651124252725051,\n\t\t6358063137690011177,\n\t\t16123467710013993794,\n\t\t8086526499083208127,\n\t\t5907916440057635441,\n\t\t7074828965897564835,\n\t\t219959441764368518,\n\t}\n\n\tfor i, exp := range expected {\n\t\tval := rnd.Uint64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"Xorshift64Star.Uint64() at iteration %d: got %d, expected %d\", i, val, exp)\n\t\t}\n\t}\n}\n\nfunc TestXorshift64StarMarshalUnmarshal(t *testing.T) {\n\trnd := New(1001)\n\n\texpected1 := []uint64{\n\t\t17667678392346722343,\n\t\t8998941448230025236,\n\t\t6038719778092581458,\n\t\t9916057400810746083,\n\t\t3240504040424884895,\n\t}\n\n\texpected2 := []uint64{\n\t\t3977048231667561376,\n\t\t18438555156602529247,\n\t\t2172795924893074637,\n\t\t12043507788481457357,\n\t\t8032279100325099159,\n\t}\n\n\tfor i, exp := range expected1 {\n\t\tval := rnd.Uint64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"Xorshift64Star.Uint64() at iteration %d: got %d, expected %d\", i, val, exp)\n\t\t}\n\t}\n\n\tmarshalled, err := rnd.MarshalBinary()\n\n\t// t.Logf(\"Original State: [%x]\\n\", rnd.seed)\n\t// t.Logf(\"Marshalled State: [%x] -- %v\\n\", marshalled, err)\n\tstate_before := rnd.seed\n\n\tif err != nil {\n\t\tt.Errorf(\"Xorshift64Star.MarshalBinary() error: %v\", err)\n\t}\n\n\t// Advance state by one number; then check the next 5. The expectation is that they _will_ fail.\n\trnd.Uint64()\n\n\tfor i, exp := range expected2 {\n\t\tval := rnd.Uint64()\n\t\tif exp == val {\n\t\t\tt.Errorf(\"  Iteration %d matched %d; which is from iteration %d; something strange is happening.\", (i + 6), val, (i + 5))\n\t\t}\n\t}\n\n\t// t.Logf(\"State before unmarshall: [%x]\\n\", rnd.seed)\n\n\t// Now restore the state of the PRNG\n\terr = rnd.UnmarshalBinary(marshalled)\n\n\t// t.Logf(\"State after unmarshall: [%x]\\n\", rnd.seed)\n\n\tif state_before != rnd.seed {\n\t\tt.Errorf(\"States before and after marshal/unmarshal are not equal; go %x and %x\", state_before, rnd.seed)\n\t}\n\n\t// Now we should be back on track for the last 5 numbers\n\tfor i, exp := range expected2 {\n\t\tval := rnd.Uint64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"Xorshift64Star.Uint64() at iteration %d: got %d, expected %d\", (i + 5), val, exp)\n\t\t}\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"stso7usp+HI0BMaSZi+BsOfvxyel7AADgdWJqdsREwd9g8pIEWs71G1ihoTmgJhml/8Im0j7TsGVHk291WWiuQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5","package":{"name":"config","path":"gno.land/r/leon/config","files":[{"name":"config.gno","body":"package config\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/ownable/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n)\n\nconst absPath = \"/r/leon/config\"\n\nvar (\n\tcfgID   seqid.ID\n\tconfigs = avl.NewTree()\n\n\t// SafeObjects\n\tOwnableMain   = ownable.NewWithAddress(\"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\")\n\tOwnableBackup = ownable.NewWithAddress(\"g1lavlav7zwsjqlzzl3qdl3nl242qtf638vnhdjh\")\n\n\tErrUnauthorized = errors.New(\"leon's config: unauthorized\")\n)\n\ntype Config struct {\n\tid      seqid.ID\n\tname    string\n\tlines   string\n\tupdated time.Time\n}\n\nfunc AddConfig(cur realm, name, lines string) {\n\tif !IsAuthorized(cur.Previous().Address()) {\n\t\tpanic(ErrUnauthorized)\n\t}\n\n\tid := cfgID.Next()\n\tconfigs.Set(id.String(), Config{\n\t\tid:      id,\n\t\tname:    name,\n\t\tlines:   lines,\n\t\tupdated: time.Now(),\n\t})\n}\n\nfunc EditConfig(cur realm, id string, name, lines string) {\n\tif !IsAuthorized(cur.Previous().Address()) {\n\t\tpanic(ErrUnauthorized)\n\t}\n\n\traw, ok := configs.Remove(id)\n\tif !ok {\n\t\tpanic(\"no config with that id\")\n\t}\n\n\tconf := raw.(Config)\n\t// Overwrites data\n\tconf.lines = lines\n\tconf.name = name\n\tconf.updated = time.Now()\n}\n\nfunc RemoveConfig(cur realm, id string) {\n\tif !IsAuthorized(cur.Previous().Address()) {\n\t\tpanic(ErrUnauthorized)\n\t}\n\n\tif _, ok := configs.Remove(id); !ok {\n\t\tpanic(\"no config with that id\")\n\t}\n}\n\nfunc UpdateBanner(cur realm, newBanner string) {\n\tif !IsAuthorized(cur.Previous().Address()) {\n\t\tpanic(ErrUnauthorized)\n\t}\n\n\tbanner = newBanner\n}\n\nfunc IsAuthorized(addr address) bool {\n\treturn addr == OwnableMain.Owner() || addr == OwnableBackup.Owner()\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/leon/config\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\"\n"},{"name":"render.gno","body":"package config\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/moul/realmpath/v0\"\n\t\"gno.land/p/moul/txlink/v0\"\n\tp \"gno.land/p/nt/avl/pager/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nvar (\n\tbanner = \"---\\n[[Leon's Home page]](/r/leon/home) | [[Leon's snippets]](/r/leon/config) | [[GitHub: @leohhhn]](https://github.com/leohhhn)\\n\\n---\"\n\tpager  = p.NewPager(configs, 10, true)\n)\n\nfunc Banner() string {\n\treturn banner\n}\n\nfunc Render(path string) (out string) {\n\treq := realmpath.Parse(path)\n\tif req.Path == \"\" {\n\t\tout += md.H1(\"Leon's configs \u0026 snippets\")\n\n\t\tout += ufmt.Sprintf(\"Leon's main address: %s\\n\\n\", OwnableMain.Owner().String())\n\t\tout += ufmt.Sprintf(\"Leon's backup address: %s\\n\\n\", OwnableBackup.Owner().String())\n\n\t\tout += md.H2(\"Snippets\")\n\n\t\tif configs.Size() == 0 {\n\t\t\tout += \"No configs yet :c\\n\\n\"\n\t\t} else {\n\t\t\tpage := pager.MustGetPageByPath(path)\n\t\t\tfor _, item := range page.Items {\n\t\t\t\tout += ufmt.Sprintf(\"- [%s](%s:%s)\\n\\n\", item.Value.(Config).name, absPath, item.Key)\n\t\t\t}\n\n\t\t\tout += page.Picker(path)\n\t\t\tout += \"\\n\\n\"\n\t\t\tout += \"Page \" + strconv.Itoa(page.PageNumber) + \" of \" + strconv.Itoa(page.TotalPages) + \"\\n\\n\"\n\t\t}\n\n\t\tout += Banner()\n\n\t\treturn out\n\t}\n\n\treturn renderConfPage(req.Path)\n}\n\nfunc renderConfPage(id string) (out string) {\n\traw := configs.Get(id)\n\tif raw == nil {\n\t\tout += md.H1(\"404\")\n\t\tout += \"That config does not exist :/\"\n\t\treturn out\n\t}\n\n\tconf := raw.(Config)\n\tout += md.H1(conf.name)\n\tout += ufmt.Sprintf(\"```\\n%s\\n```\\n\\n\", conf.lines)\n\tout += ufmt.Sprintf(\"_Last updated on %s_\\n\\n\", conf.updated.Format(\"02 Jan, 2006\"))\n\tout += md.HorizontalRule()\n\tout += ufmt.Sprintf(\"[[EDIT]](%s) - [[DELETE]](%s)\", txlink.Call(\"EditConfig\", \"id\", conf.id.String()), txlink.Call(\"RemoveConfig\", \"id\", conf.id.String()))\n\n\treturn out\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"UxCfypX6nMxEch2gax/ZnBoPxlhKDfalys0kz8P/ZfpWlaT4KGu2D8gXmNhCvxVBiX0Azoc4oCsmFhL/JRBlNA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5","package":{"name":"hor","path":"gno.land/r/leon/hor","files":[{"name":"datasource.gno","body":"package hor\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/jeronimoalbi/datasource\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nfunc NewDatasource() Datasource {\n\treturn Datasource{exhibition}\n}\n\ntype Datasource struct {\n\texhibition *Exhibition\n}\n\nfunc (ds Datasource) Size() int { return ds.exhibition.items.Size() }\n\nfunc (ds Datasource) Records(q datasource.Query) datasource.Iterator {\n\treturn \u0026iterator{\n\t\texhibition: ds.exhibition,\n\t\tindex:      q.Offset,\n\t\tmaxIndex:   q.Offset + q.Count,\n\t}\n}\n\nfunc (ds Datasource) Record(id string) (datasource.Record, error) {\n\tv := ds.exhibition.items.Get(id)\n\tif v == nil {\n\t\treturn nil, errors.New(\"realm submission not found\")\n\t}\n\treturn record{v.(*Item)}, nil\n}\n\ntype record struct {\n\titem *Item\n}\n\nfunc (r record) ID() string     { return r.item.id.String() }\nfunc (r record) String() string { return r.item.pkgpath }\n\nfunc (r record) Fields() (datasource.Fields, error) {\n\tfields := avl.NewTree()\n\tfields.Set(\n\t\t\"details\",\n\t\tufmt.Sprintf(\"Votes: ⏶ %d - ⏷ %d\", r.item.upvote.Size(), r.item.downvote.Size()),\n\t)\n\treturn fields, nil\n}\n\nfunc (r record) Content() (string, error) {\n\tcontent := r.item.Render(false)\n\treturn content, nil\n}\n\ntype iterator struct {\n\texhibition      *Exhibition\n\tindex, maxIndex int\n\trecord          *record\n}\n\nfunc (it iterator) Record() datasource.Record { return it.record }\nfunc (it iterator) Err() error                { return nil }\n\nfunc (it *iterator) Next() bool {\n\tif it.index \u003e= it.maxIndex || it.index \u003e= it.exhibition.items.Size() {\n\t\treturn false\n\t}\n\n\t_, v := it.exhibition.items.GetByIndex(it.index)\n\tit.record = \u0026record{v.(*Item)}\n\tit.index++\n\treturn true\n}\n"},{"name":"datasource_test.gno","body":"package hor\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/jeronimoalbi/datasource\"\n\t\"gno.land/p/moul/addrset/v0\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nfunc init(cur realm) {\n\t// Pre-register \"demo\" user for render tests. uinit.RegisterUser is\n\t// genesis-only (height==0) since the security fix; tests run at\n\t// DefaultHeight=123, so we temporarily reset and restore.\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"demo\", address(\"g13ek2zz9qurzynzvssyc4sthwppnruhnp0gdz8n\"))\n\ttesting.SetHeight(123)\n}\n\nvar (\n\t_ datasource.Datasource    = (*Datasource)(nil)\n\t_ datasource.Record        = (*record)(nil)\n\t_ datasource.ContentRecord = (*record)(nil)\n\t_ datasource.Iterator      = (*iterator)(nil)\n)\n\nfunc TestDatasourceRecords(cur realm, t *testing.T) {\n\tcases := []struct {\n\t\tname      string\n\t\titems     []*Item\n\t\trecordIDs []string\n\t\toptions   []datasource.QueryOption\n\t}{\n\t\t{\n\t\t\tname:      \"all items\",\n\t\t\titems:     []*Item{{id: 1}, {id: 2}, {id: 3}},\n\t\t\trecordIDs: []string{\"0000001\", \"0000002\", \"0000003\"},\n\t\t},\n\t\t{\n\t\t\tname:      \"with offset\",\n\t\t\titems:     []*Item{{id: 1}, {id: 2}, {id: 3}},\n\t\t\trecordIDs: []string{\"0000002\", \"0000003\"},\n\t\t\toptions:   []datasource.QueryOption{datasource.WithOffset(1)},\n\t\t},\n\t\t{\n\t\t\tname:      \"with count\",\n\t\t\titems:     []*Item{{id: 1}, {id: 2}, {id: 3}},\n\t\t\trecordIDs: []string{\"0000001\", \"0000002\"},\n\t\t\toptions:   []datasource.QueryOption{datasource.WithCount(2)},\n\t\t},\n\t\t{\n\t\t\tname:      \"with offset and count\",\n\t\t\titems:     []*Item{{id: 1}, {id: 2}, {id: 3}},\n\t\t\trecordIDs: []string{\"0000002\"},\n\t\t\toptions: []datasource.QueryOption{\n\t\t\t\tdatasource.WithOffset(1),\n\t\t\t\tdatasource.WithCount(1),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Initialize a local instance of exhibition\n\t\t\texhibition := \u0026Exhibition{items: avl.NewTree()}\n\t\t\tfor _, item := range tc.items {\n\t\t\t\texhibition.items.Set(item.id.String(), item)\n\t\t\t}\n\n\t\t\t// Get a records iterator\n\t\t\tds := Datasource{exhibition}\n\t\t\tquery := datasource.NewQuery(tc.options...)\n\t\t\titer := ds.Records(query)\n\n\t\t\t// Start asserting\n\t\t\turequire.Equal(t, len(tc.items), ds.Size(), \"datasource size\")\n\n\t\t\tvar records []datasource.Record\n\t\t\tfor iter.Next() {\n\t\t\t\trecords = append(records, iter.Record())\n\t\t\t}\n\t\t\turequire.Equal(t, len(tc.recordIDs), len(records), \"record count\")\n\n\t\t\tfor i, r := range records {\n\t\t\t\tuassert.Equal(t, tc.recordIDs[i], r.ID())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDatasourceRecord(cur realm, t *testing.T) {\n\tcases := []struct {\n\t\tname  string\n\t\titems []*Item\n\t\tid    string\n\t\terr   string\n\t}{\n\t\t{\n\t\t\tname:  \"found\",\n\t\t\titems: []*Item{{id: 1}, {id: 2}, {id: 3}},\n\t\t\tid:    \"0000001\",\n\t\t},\n\t\t{\n\t\t\tname:  \"no found\",\n\t\t\titems: []*Item{{id: 1}, {id: 2}, {id: 3}},\n\t\t\tid:    \"42\",\n\t\t\terr:   \"realm submission not found\",\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Initialize a local instance of exhibition\n\t\t\texhibition := \u0026Exhibition{items: avl.NewTree()}\n\t\t\tfor _, item := range tc.items {\n\t\t\t\texhibition.items.Set(item.id.String(), item)\n\t\t\t}\n\n\t\t\t// Get a single record\n\t\t\tds := Datasource{exhibition}\n\t\t\tr, err := ds.Record(tc.id)\n\n\t\t\t// Start asserting\n\t\t\tif tc.err != \"\" {\n\t\t\t\tuassert.ErrorContains(t, err, tc.err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.NoError(t, err, \"no error\")\n\t\t\turequire.NotEqual(t, nil, r, \"record not nil\")\n\t\t\tuassert.Equal(t, tc.id, r.ID())\n\t\t})\n\t}\n}\n\nfunc TestItemRecord(cur realm, t *testing.T) {\n\tpkgpath := \"gno.land/r/demo/test\"\n\titem := Item{\n\t\tid:          1,\n\t\tpkgpath:     pkgpath,\n\t\ttitle:       \"Test Realm\",\n\t\tdescription: \"This is a test realm in the Hall of Fame\",\n\t\tblockNum:    42,\n\t\tupvote:      \u0026addrset.Set{},\n\t\tdownvote:    \u0026addrset.Set{},\n\t}\n\titem.downvote.Add(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\titem.upvote.Add(\"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\")\n\titem.upvote.Add(\"g1w4ek2u3jta047h6lta047h6lta047h6l9huexc\")\n\n\tr := record{\u0026item}\n\n\tuassert.Equal(t, \"0000001\", r.ID())\n\tuassert.Equal(t, pkgpath, r.String())\n\n\tfields, _ := r.Fields()\n\tdetails := fields.Get(\"details\")\n\turequire.True(t, details != nil, \"details field\")\n\tuassert.Equal(t, \"Votes: ⏶ 2 - ⏷ 1\", details)\n\n\tcontent, _ := r.Content()\n\twantContent := `### [Test Realm](/r/demo/test)\nThis is a test realm in the Hall of Fame\n\nby [@demo](/u/demo)\n\nSubmitted at Block #42\n\n**[2👍](/r/leon/hor$help\u0026func=Upvote\u0026pkgpath=gno.land%2Fr%2Fdemo%2Ftest) - [1👎](/r/leon/hor$help\u0026func=Downvote\u0026pkgpath=gno.land%2Fr%2Fdemo%2Ftest)**`\n\tuassert.Equal(t, wantContent, content)\n}\n"},{"name":"errors.gno","body":"package hor\n\nimport (\n\t\"gno.land/p/leon/pkgerr\"\n)\n\nvar (\n\tErrNoSuchItem     = pkgerr.New(\"no such item exists\")\n\tErrDoubleUpvote   = pkgerr.New(\"cannot upvote twice\")\n\tErrDoubleDownvote = pkgerr.New(\"cannot downvote twice\")\n)\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/leon/hor\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\"\n"},{"name":"hof_test.gno","body":"package hor\n\nimport (\n\t\"chain/runtime\"\n\t\"testing\"\n\n\t\"gno.land/p/moul/addrset/v0\"\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\n// cur is a zero-value realm used as a placeholder when forwarding to\n// uassert/urequire dispatch helpers that gained an `rlm realm` param.\n// These tests pass `func()` callbacks (no crossing inside the callback),\n// so rlm is ignored — a nil realm here is safe.\nvar cur realm\n\nconst (\n\trlmPath  = \"gno.land/r/gnoland/home\"\n\trlmPath2 = \"gno.land/r/gnoland/test2\"\n\n\trlmPath3 = \"gno.land/r/gnoland/test3\"\n\trlmPath4 = \"gno.land/r/gnoland/test4\"\n\trlmPath5 = \"gno.land/r/gnoland/test5\"\n\n\tvalidTitle         = \"valid title\"\n\tinvalidTitle       = \"This title is very very very long, longer than 30 characters\"\n\tvalidDesc          = \"valid description\"\n\tinvalidDescription = \"This description is very very very long, longer than 50 characters\"\n)\n\nvar (\n\tadmin      address = \"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\" // hardcoded from config.OwnableMain\n\tadminRealm runtime.Realm\n\talice      = testutils.TestAddress(\"alice\")\n)\n\nfunc init() {\n\tadminRealm = testing.NewUserRealm(admin)\n}\n\nfunc TestRegister(cur realm, t *testing.T) {\n\t// Test user realm register\n\taliceRealm := testing.NewUserRealm(alice)\n\ttesting.SetRealm(aliceRealm)\n\n\tRegister(cross(cur), validTitle, validDesc)\n\tuassert.False(t, itemExists(t, rlmPath))\n\n\t// Test register while paused\n\ttesting.SetRealm(adminRealm)\n\terr := Pause(cross(cur))\n\tuassert.NoError(t, err)\n\n\t// Set legitimate caller\n\ttesting.SetRealm(testing.NewCodeRealm(rlmPath))\n\n\tRegister(cross(cur), validTitle, validDesc)\n\tuassert.False(t, itemExists(t, rlmPath))\n\n\t// Unpause\n\ttesting.SetRealm(adminRealm)\n\terr = Unpause(cross(cur))\n\tuassert.NoError(t, err)\n\n\t// Set legitimate caller\n\ttesting.SetRealm(testing.NewCodeRealm(rlmPath))\n\tRegister(cross(cur), validTitle, validDesc)\n\n\t// Find registered items\n\tuassert.True(t, itemExists(t, rlmPath))\n\n\t// Test register with invalid title\n\ttesting.SetRealm(testing.NewCodeRealm(rlmPath2))\n\tRegister(cross(cur), invalidTitle, validDesc)\n\tuassert.False(t, itemExists(t, rlmPath2))\n\n\t// Test register with invalid description\n\ttesting.SetRealm(testing.NewCodeRealm(rlmPath2))\n\tRegister(cross(cur), validTitle, invalidDescription)\n\tuassert.False(t, itemExists(t, rlmPath2))\n}\n\nfunc TestUpvote(cur realm, t *testing.T) {\n\traw := exhibition.items.Get(rlmPath)\n\titem := raw.(*Item)\n\n\t// 0 upvotes by default\n\turequire.Equal(t, item.upvote.Size(), 0)\n\n\ttesting.SetRealm(adminRealm)\n\n\turequire.NotPanics(t, cur, func() {\n\t\tUpvote(cross(cur), rlmPath)\n\t})\n\n\t// Check both trees for 1 upvote\n\tuassert.Equal(t, item.upvote.Size(), 1)\n\n\t// Check double upvote\n\tuassert.AbortsWithMessage(t, cur, ErrDoubleUpvote.Error(), func() {\n\t\tUpvote(cross(cur), rlmPath)\n\t})\n}\n\nfunc TestDownvote(cur realm, t *testing.T) {\n\traw := exhibition.items.Get(rlmPath)\n\titem := raw.(*Item)\n\n\t// 0 downvotes by default\n\turequire.Equal(t, item.downvote.Size(), 0)\n\n\tuserRealm := testing.NewUserRealm(alice)\n\ttesting.SetRealm(userRealm)\n\n\turequire.NotPanics(t, cur, func() {\n\t\tDownvote(cross(cur), rlmPath)\n\t})\n\n\t// Check both trees for 1 upvote\n\tuassert.Equal(t, item.downvote.Size(), 1)\n\n\t// Check double downvote\n\tuassert.AbortsWithMessage(t, cur, ErrDoubleDownvote.Error(), func() {\n\t\tDownvote(cross(cur), rlmPath)\n\t})\n}\n\nfunc TestDelete(cur realm, t *testing.T) {\n\tuserRealm := testing.NewUserRealm(admin)\n\ttesting.SetRealm(userRealm)\n\ttesting.SetOriginCaller(admin)\n\n\tuassert.AbortsWithMessage(t, cur, ErrNoSuchItem.Error(), func() {\n\t\tDelete(cross(cur), \"nonexistentpkgpath\")\n\t})\n\n\ti := exhibition.items.Get(rlmPath)\n\t_ = i.(*Item).id\n\n\tuassert.NotPanics(t, cur, func() {\n\t\tDelete(cross(cur), rlmPath)\n\t})\n\n\tuassert.False(t, exhibition.items.Has(rlmPath))\n}\n\nfunc itemExists(t *testing.T, rlmPath string) bool {\n\tt.Helper()\n\n\treturn exhibition.items.Has(rlmPath)\n}\n\nfunc TestgetVoteSortKey(t *testing.T) {\n\ti := \u0026Item{\n\t\tid:          1,\n\t\ttitle:       validTitle,\n\t\tdescription: validDesc,\n\t\tpkgpath:     rlmPath,\n\t\tblockNum:    runtime.ChainHeight(),\n\t\tupvote:      \u0026addrset.Set{},\n\t\tdownvote:    \u0026addrset.Set{},\n\t}\n\n\ti.upvote.Add(alice)\n\n\tgeneratedKey := getVoteSortKey(i.upvote.Size(), i.id)\n\texpectedKey := \"0000000001:1\"\n\n\turequire.Equal(t, generatedKey, expectedKey)\n}\n\nfunc TestSortByUpvote(cur realm, t *testing.T) {\n\t// Remove all items from all trees\n\texhibition.items.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\texhibition.items.Remove(key)\n\t\treturn false\n\t})\n\texhibition.itemsSortedByUpvotes.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\texhibition.itemsSortedByUpvotes.Remove(key)\n\t\treturn false\n\t})\n\texhibition.itemsSortedByDownvotes.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\texhibition.itemsSortedByDownvotes.Remove(key)\n\t\treturn false\n\t})\n\texhibition.itemsSortedByCreation.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\texhibition.itemsSortedByCreation.Remove(key)\n\t\treturn false\n\t})\n\n\t// Add items\n\ttesting.SetRealm(testing.NewCodeRealm(rlmPath3))\n\tRegister(cross(cur), validTitle, validDesc)\n\n\ttesting.SetRealm(testing.NewCodeRealm(rlmPath4))\n\tRegister(cross(cur), validTitle, validDesc)\n\n\ttesting.SetRealm(testing.NewCodeRealm(rlmPath5))\n\tRegister(cross(cur), validTitle, validDesc)\n\n\tuser1 := testutils.TestAddress(\"user1\")\n\tuser2 := testutils.TestAddress(\"user2\")\n\tuser3 := testutils.TestAddress(\"user3\")\n\n\ttesting.SetOriginCaller(user1)\n\ttesting.SetRealm(testing.NewUserRealm(user1))\n\tUpvote(cross(cur), rlmPath3)\n\tUpvote(cross(cur), rlmPath4)\n\tUpvote(cross(cur), rlmPath5)\n\n\ttesting.SetOriginCaller(user2)\n\ttesting.SetRealm(testing.NewUserRealm(user2))\n\tUpvote(cross(cur), rlmPath4)\n\tUpvote(cross(cur), rlmPath5)\n\n\ttesting.SetOriginCaller(user3)\n\ttesting.SetRealm(testing.NewUserRealm(user3))\n\tUpvote(cross(cur), rlmPath5)\n\n\t// We are displaying data in reverse order in render, so items should be sorted in reverse order\n\t_, firstRawValue := exhibition.itemsSortedByUpvotes.GetByIndex(0)\n\tfirstValue := firstRawValue.(*Item)\n\tuassert.Equal(t, firstValue.pkgpath, rlmPath3)\n\n\t_, secondRawValue := exhibition.itemsSortedByUpvotes.GetByIndex(1)\n\tsecondValue := secondRawValue.(*Item)\n\tuassert.Equal(t, secondValue.pkgpath, rlmPath4)\n}\n\nfunc TestSortByDownvote(cur realm, t *testing.T) {\n\t// Remove all items from all trees\n\texhibition.items.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\texhibition.items.Remove(key)\n\t\treturn false\n\t})\n\texhibition.itemsSortedByUpvotes.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\texhibition.itemsSortedByUpvotes.Remove(key)\n\t\treturn false\n\t})\n\texhibition.itemsSortedByDownvotes.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\texhibition.itemsSortedByDownvotes.Remove(key)\n\t\treturn false\n\t})\n\texhibition.itemsSortedByCreation.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\texhibition.itemsSortedByCreation.Remove(key)\n\t\treturn false\n\t})\n\n\t// Add items\n\ttesting.SetRealm(testing.NewCodeRealm(rlmPath3))\n\tRegister(cross(cur), validTitle, validDesc)\n\n\ttesting.SetRealm(testing.NewCodeRealm(rlmPath4))\n\tRegister(cross(cur), validTitle, validDesc)\n\n\ttesting.SetRealm(testing.NewCodeRealm(rlmPath5))\n\tRegister(cross(cur), validTitle, validDesc)\n\n\tuser1 := testutils.TestAddress(\"user1\")\n\tuser2 := testutils.TestAddress(\"user2\")\n\tuser3 := testutils.TestAddress(\"user3\")\n\n\ttesting.SetOriginCaller(user1)\n\ttesting.SetRealm(testing.NewUserRealm(user1))\n\tDownvote(cross(cur), rlmPath3)\n\tDownvote(cross(cur), rlmPath4)\n\tDownvote(cross(cur), rlmPath5)\n\n\ttesting.SetOriginCaller(user2)\n\ttesting.SetRealm(testing.NewUserRealm(user2))\n\tDownvote(cross(cur), rlmPath4)\n\tDownvote(cross(cur), rlmPath5)\n\n\ttesting.SetOriginCaller(user3)\n\ttesting.SetRealm(testing.NewUserRealm(user3))\n\tDownvote(cross(cur), rlmPath5)\n\n\t// We are dispalying data is reverse order in render, so items should be sorted in reverse order\n\t_, firstRawValue := exhibition.itemsSortedByDownvotes.GetByIndex(0)\n\n\tfirstValue := firstRawValue.(*Item)\n\n\tuassert.Equal(t, firstValue.pkgpath, rlmPath3)\n\n\t_, secondRawValue := exhibition.itemsSortedByDownvotes.GetByIndex(1)\n\n\tsecondValue := secondRawValue.(*Item)\n\n\tuassert.Equal(t, secondValue.pkgpath, rlmPath4)\n}\n\nfunc TestSortByCreation(cur realm, t *testing.T) {\n\t// Remove all items from all trees\n\texhibition.items.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\texhibition.items.Remove(key)\n\t\treturn false\n\t})\n\texhibition.itemsSortedByUpvotes.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\texhibition.itemsSortedByUpvotes.Remove(key)\n\t\treturn false\n\t})\n\texhibition.itemsSortedByDownvotes.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\texhibition.itemsSortedByDownvotes.Remove(key)\n\t\treturn false\n\t})\n\texhibition.itemsSortedByCreation.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\texhibition.itemsSortedByCreation.Remove(key)\n\t\treturn false\n\t})\n\n\ttesting.SkipHeights(10)\n\ttesting.SetRealm(testing.NewCodeRealm(rlmPath3))\n\tRegister(cross(cur), validTitle, validDesc)\n\n\ttesting.SkipHeights(10)\n\ttesting.SetRealm(testing.NewCodeRealm(rlmPath4))\n\tRegister(cross(cur), validTitle, validDesc)\n\n\ttesting.SkipHeights(10)\n\ttesting.SetRealm(testing.NewCodeRealm(rlmPath5))\n\tRegister(cross(cur), validTitle, validDesc)\n\n\t// We are dispalying data is reverse order in render, so items should be sorted in reverse order\n\t_, firstRawValue := exhibition.itemsSortedByCreation.GetByIndex(0)\n\n\tfirstValue := firstRawValue.(*Item)\n\n\tuassert.Equal(t, firstValue.pkgpath, rlmPath3)\n\n\t_, secondRawValue := exhibition.itemsSortedByCreation.GetByIndex(1)\n\n\tsecondValue := secondRawValue.(*Item)\n\n\tuassert.Equal(t, secondValue.pkgpath, rlmPath4)\n}\n\n/* XXX fix @moul\nfunc TestGetScore(cur realm, t *testing.T) {\n\tscore, err := h.GetScore(42)\n\tuassert.Nil(t, err)\n\tuassert.Equal(t, 0, score)\n\tuassert.NotPanics(t, cur, func() {\n\t\tuassert.Equal(t, 0, h.MustGetScore(42))\n\t})\n}\n*/\n"},{"name":"hor.gno","body":"// Package hor is the hall of realms.\n// The Hall of Realms is an exhibition that holds items. Users can add their realms to the Hall of Realms by\n// importing the Hall of Realms package and calling hor.Register() from their init function.\npackage hor\n\nimport (\n\t\"chain\"\n\t\"chain/runtime\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/moul/addrset/v0\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/ownable/v0\"\n\t\"gno.land/p/nt/pausable/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/r/leon/config\"\n)\n\nconst (\n\tmaxTitleLength       = 30\n\tmaxDescriptionLength = 50\n)\n\nvar (\n\texhibition *Exhibition\n\n\t// Safe objects\n\tOwnable  *ownable.Ownable\n\tPausable *pausable.Pausable\n)\n\ntype (\n\tExhibition struct {\n\t\titemCounter            seqid.ID\n\t\tdescription            string\n\t\titems                  *avl.Tree // pkgPath \u003e *Item\n\t\titemsSortedByCreation  *avl.Tree // same data but sorted by creation time\n\t\titemsSortedByUpvotes   *avl.Tree // same data but sorted by upvotes\n\t\titemsSortedByDownvotes *avl.Tree // same data but sorted by downvotes\n\t}\n\n\tItem struct {\n\t\tid          seqid.ID\n\t\ttitle       string\n\t\tdescription string\n\t\tpkgpath     string\n\t\tblockNum    int64\n\t\tupvote      *addrset.Set\n\t\tdownvote    *addrset.Set\n\t}\n)\n\nfunc init() {\n\texhibition = \u0026Exhibition{\n\t\titems:                  avl.NewTree(),\n\t\titemsSortedByCreation:  avl.NewTree(),\n\t\titemsSortedByUpvotes:   avl.NewTree(),\n\t\titemsSortedByDownvotes: avl.NewTree(),\n\t}\n\n\tOwnable = ownable.NewWithAddress(config.OwnableMain.Owner()) // OrigSendOwnable?\n\tPausable = pausable.NewFromOwnable(Ownable)\n}\n\n// Register registers your realm to the Hall of Fame\n// Should be called from within code\nfunc Register(cur realm, title, description string) {\n\tif Pausable.IsPaused() {\n\t\treturn\n\t}\n\n\tsubmission := cur.Previous()\n\tpkgpath := submission.PkgPath()\n\n\t// Must be called from code\n\tif submission.IsUser() {\n\t\treturn\n\t}\n\n\t// Must not yet exist\n\tif exhibition.items.Has(pkgpath) {\n\t\treturn\n\t}\n\n\t// Title must be between 1 maxTitleLength long\n\tif title == \"\" || len(title) \u003e maxTitleLength {\n\t\treturn\n\t}\n\n\t// Description must be between 1 maxDescriptionLength long\n\tif len(description) \u003e maxDescriptionLength {\n\t\treturn\n\t}\n\n\tid := exhibition.itemCounter.Next()\n\ti := \u0026Item{\n\t\tid:          id,\n\t\ttitle:       title,\n\t\tdescription: description,\n\t\tpkgpath:     pkgpath,\n\t\tblockNum:    runtime.ChainHeight(),\n\t\tupvote:      \u0026addrset.Set{},\n\t\tdownvote:    \u0026addrset.Set{},\n\t}\n\n\texhibition.items.Set(pkgpath, i)\n\texhibition.itemsSortedByCreation.Set(getCreationSortKey(i.blockNum, i.id), i)\n\texhibition.itemsSortedByUpvotes.Set(getVoteSortKey(i.upvote.Size(), i.id), i)\n\texhibition.itemsSortedByDownvotes.Set(getVoteSortKey(i.downvote.Size(), i.id), i)\n\n\tchain.Emit(\"Registration\")\n}\n\nfunc Upvote(cur realm, pkgpath string) {\n\trawItem := exhibition.items.Get(pkgpath)\n\tif rawItem == nil {\n\t\tpanic(ErrNoSuchItem)\n\t}\n\n\titem := rawItem.(*Item)\n\tcaller := cur.Previous().Address()\n\n\tif item.upvote.Has(caller) {\n\t\tpanic(ErrDoubleUpvote)\n\t}\n\n\tif _, exists := exhibition.itemsSortedByUpvotes.Remove(getVoteSortKey(item.upvote.Size(), item.id)); !exists {\n\t\tpanic(\"error removing old upvote entry\")\n\t}\n\n\titem.upvote.Add(caller)\n\n\texhibition.itemsSortedByUpvotes.Set(getVoteSortKey(item.upvote.Size(), item.id), item)\n}\n\nfunc Downvote(cur realm, pkgpath string) {\n\trawItem := exhibition.items.Get(pkgpath)\n\tif rawItem == nil {\n\t\tpanic(ErrNoSuchItem)\n\t}\n\n\titem := rawItem.(*Item)\n\tcaller := cur.Previous().Address()\n\n\tif item.downvote.Has(caller) {\n\t\tpanic(ErrDoubleDownvote)\n\t}\n\n\tif _, exist := exhibition.itemsSortedByDownvotes.Remove(getVoteSortKey(item.downvote.Size(), item.id)); !exist {\n\t\tpanic(\"error removing old downvote entry\")\n\n\t}\n\n\titem.downvote.Add(caller)\n\n\texhibition.itemsSortedByDownvotes.Set(getVoteSortKey(item.downvote.Size(), item.id), item)\n}\n\nfunc Pause(cur realm) error {\n\treturn Pausable.Pause(0, cur)\n}\n\nfunc Unpause(cur realm) error {\n\treturn Pausable.Unpause(0, cur)\n}\n\nfunc Delete(cur realm, pkgpath string) {\n\tif !Ownable.OwnedBy(cur.Previous().Address()) {\n\t\tpanic(ownable.ErrUnauthorized)\n\t}\n\n\ti := exhibition.items.Get(pkgpath)\n\tif i == nil {\n\t\tpanic(ErrNoSuchItem)\n\t}\n\n\titem := i.(*Item)\n\tupvoteKey := getVoteSortKey(item.upvote.Size(), item.id)\n\tdownvoteKey := getVoteSortKey(item.downvote.Size(), item.id)\n\n\tif _, removed := exhibition.items.Remove(pkgpath); !removed {\n\t\tpanic(ErrNoSuchItem)\n\t}\n\n\tif _, removed := exhibition.itemsSortedByUpvotes.Remove(upvoteKey); !removed {\n\t\tpanic(ErrNoSuchItem)\n\t}\n\n\tif _, removed := exhibition.itemsSortedByDownvotes.Remove(downvoteKey); !removed {\n\t\tpanic(ErrNoSuchItem)\n\t}\n\n\tif _, removed := exhibition.itemsSortedByCreation.Remove(getCreationSortKey(item.blockNum, item.id)); !removed {\n\t\tpanic(ErrNoSuchItem)\n\t}\n}\n\nfunc getVoteSortKey(votes int, id seqid.ID) string {\n\tvotesStr := strconv.Itoa(votes)\n\tpaddedVotes := strings.Repeat(\"0\", 10-len(votesStr)) + votesStr\n\treturn paddedVotes + \":\" + strconv.FormatUint(uint64(id), 10)\n}\n\nfunc getCreationSortKey(blockNum int64, id seqid.ID) string {\n\tblockNumStr := strconv.Itoa(int(blockNum))\n\tpaddedBlockNum := strings.Repeat(\"0\", 10-len(blockNumStr)) + blockNumStr\n\treturn paddedBlockNum + \":\" + strconv.FormatUint(uint64(id), 10)\n}\n"},{"name":"render.gno","body":"package hor\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/moul/txlink/v0\"\n\t\"gno.land/p/nt/avl/pager/v0\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/fqname/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\n\t\"gno.land/r/sys/users\"\n)\n\nconst colsPerRow = 3\n\nfunc Render(path string) string {\n\tout := md.H1(\"The Hall of Realms\\n\\n\")\n\n\tif !strings.Contains(path, \"hall\") \u0026\u0026 !strings.Contains(path, \"dashboard\") {\n\t\tout += renderAbout()\n\t\treturn out\n\t}\n\n\tdashboardEnabled := strings.Contains(path, \"dashboard\")\n\tif dashboardEnabled {\n\t\tout += renderDashboard()\n\t\tout += renderActions(path)\n\t\treturn out\n\t}\n\n\tout += renderActions(path)\n\tout += exhibition.Render(path, dashboardEnabled)\n\n\treturn out\n}\n\nfunc (e Exhibition) Render(path string, dashboard bool) string {\n\ttree := getTreeByPath(\u0026e, path)\n\n\tu, _ := url.Parse(path)\n\treversed := u.Query().Get(\"sort\") != \"oldest\"\n\n\tpage := pager.NewPager(tree, colsPerRow*3, reversed).MustGetPageByPath(path)\n\tout := ufmt.Sprintf(\"%s\\n\\n\", e.description)\n\n\tif e.items.Size() == 0 {\n\t\tout += \"No items in this exhibition currently.\\n\\n\"\n\t\treturn out\n\t}\n\n\tstr := make([]string, len(page.Items))\n\tfor i, item := range page.Items {\n\t\titemValue := item.Value.(*Item)\n\t\tstr[i] += itemValue.Render(dashboard)\n\t}\n\n\tout += md.ColumnsN(str, 3, true)\n\tout += md.H3(page.Picker(path))\n\n\treturn out\n}\n\nfunc (i Item) Render(dashboard bool) string {\n\tout := ufmt.Sprintf(\"### [%s](%s)\\n\",\n\t\ti.title,\n\t\tstrings.TrimPrefix(i.pkgpath, \"gno.land\"),\n\t)\n\n\tif i.description == \"\" {\n\t\ti.description = \"_No description provided._\\n\"\n\t}\n\tout += ufmt.Sprintf(\"%s\\n\\n\", i.description)\n\n\tnamespace := strings.Split(i.pkgpath, \"/\")[2]\n\tuser, _ := users.ResolveAny(namespace)\n\tif user != nil {\n\t\tnamespace = user.RenderLink(\"\")\n\t}\n\tout += ufmt.Sprintf(\"by %s\\n\\n\", namespace)\n\n\tblockMsg := \"Submitted via the [Monorepo](https://github.com/gnolang/gno)\"\n\tif i.blockNum \u003e 0 {\n\t\tblockMsg = ufmt.Sprintf(\"Submitted at Block #%d\", i.blockNum)\n\t}\n\tout += ufmt.Sprintf(\"%s\\n\\n\", blockMsg)\n\n\tout += md.Bold(ufmt.Sprintf(\"[%d👍](%s) - [%d👎](%s)\",\n\t\ti.upvote.Size(), txlink.Call(\"Upvote\", \"pkgpath\", i.pkgpath),\n\t\ti.downvote.Size(), txlink.Call(\"Downvote\", \"pkgpath\", i.pkgpath),\n\t))\n\n\tif dashboard {\n\t\tout += md.Link(\"Delete\", txlink.Call(\"Delete\", \"pkgpath\", i.pkgpath))\n\t}\n\n\treturn out\n}\n\nfunc renderDashboard() string {\n\tout := md.H3(\"Dashboard\\n\\n\")\n\tout += ufmt.Sprintf(\"Total submissions: %d\\n\\n\", exhibition.items.Size())\n\n\tout += ufmt.Sprintf(\"Exhibition admin: %s\\n\\n\", Ownable.Owner().String())\n\n\tif !Pausable.IsPaused() {\n\t\tout += md.Link(\"Pause exhibition\", txlink.Call(\"Pause\"))\n\t} else {\n\t\tout += md.Link(\"Unpause exhibition\", txlink.Call(\"Unpause\"))\n\t}\n\n\tout += \"\\n\\n\"\n\treturn out\n}\n\nfunc RenderExhibWidget(itemsToRender int) string {\n\tif itemsToRender \u003c 1 {\n\t\treturn \"\"\n\t}\n\n\tif exhibition.items.Size() == 0 {\n\t\treturn \"No items in the Hall of Realms.\\n\\n\"\n\t}\n\n\tout := \"\"\n\ti := 0\n\texhibition.items.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\titem := value.(*Item)\n\n\t\tout += ufmt.Sprintf(\"- %s\\n\", fqname.RenderLink(item.pkgpath, \"\"))\n\n\t\ti++\n\t\treturn i \u003e= itemsToRender\n\t})\n\n\treturn out\n}\n\nfunc getTreeByPath(e *Exhibition, path string) *avl.Tree {\n\tu, _ := url.Parse(path)\n\tswitch u.Query().Get(\"sort\") {\n\tcase \"upvotes\":\n\t\treturn e.itemsSortedByUpvotes\n\tcase \"downvotes\":\n\t\treturn e.itemsSortedByDownvotes\n\tcase \"creation\":\n\t\treturn e.itemsSortedByCreation\n\tcase \"oldest\":\n\t\treturn e.itemsSortedByCreation\n\tdefault:\n\t\treturn e.itemsSortedByCreation\n\t}\n}\n\nfunc renderAbout() string {\n\tout := `\nWelcome, gnomes!\n\nThe Hall of Realms is a simple \u0026 permissionless dashboard for gnomes to share\ntheir work with the community.\n\nHere, anyone is welcome to submit their own code. This realm utilizes a common\nGno pattern - the registry pattern - to allow anyone to programmatically submit\ntheir work.\n\nSimply import the Hall of Realms in your code, and call the \"Register()\" function\ninside your realm init, as shown below:\n\n\"\"\"go\npackage myrealm\n\nimport \"gno.land/r/leon/hor\"\n\nfunc init(cur realm) {\n\thor.Register(cross(cur), \"My Gnome App\", \"This is my submission to the Hall of Realms.\")\n}\n...\n\"\"\"\n\n## [Visit The Hall -\u003e](/r/leon/hor:hall)\n\n`\n\tout = strings.ReplaceAll(out, \"\\\"\", \"`\")\n\n\treturn out\n}\n\nfunc renderActions(path string) string {\n\tout := md.HorizontalRule()\n\tout += md.Link(\"Reset Sort\", \"?\") + \" | \"\n\tout += md.Link(\"Sort by Upvotes\", \"?sort=upvotes\") + \" | \"\n\tout += md.Link(\"Sort by Downvotes\", \"?sort=downvotes\") + \" | \"\n\tout += md.Link(\"Sort by Most Recent\", \"?sort=creation\") + \" | \"\n\tout += md.Link(\"Sort by Oldest\", \"?sort=oldest\") + \" | \"\n\n\tif !strings.Contains(path, \"dashboard\") {\n\t\tout += md.Link(\"Dashboard\", \"/r/leon/hor:dashboard\")\n\t} else {\n\t\tout += md.Link(\"Dashboard off\", \"/r/leon/hor:hall\")\n\t}\n\n\tout += \" | \"\n\tout += md.Link(\"About\", \"/r/leon/hor\") + \"\\n\\n\"\n\n\tout += md.HorizontalRule()\n\n\treturn out\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"dLwfo+UKdfvvyu5IuJYsRjMsZ0f5E73PA1Nd9UBF8u1hsSK+yh1XvV/GWoFlfgctGH38WOItyEB7r0fWp4JpGw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"forms","path":"gno.land/r/agherasie/forms","files":[{"name":"forms.gno","body":"package forms\n\nimport (\n\t\"gno.land/p/agherasie/forms\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/onbloc/json/v0\"\n\t\"gno.land/r/leon/hor\"\n)\n\nvar db *forms.FormDB\n\nfunc init(cur realm) {\n\thor.Register(cross(cur), \"Ahgerasle's forms\", \"\")\n\tdb = forms.NewDB()\n}\n\nfunc CreateForm(cur realm, title string, description string, openAt string, closeAt string, data string) string {\n\tid, err := db.CreateForm(0, cur, title, description, openAt, closeAt, data)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn id\n}\n\nfunc GetForms() string {\n\tbuilder := forms.FormNodeBuilder{json.Builder()}\n\n\tbuilder.WriteArray(\"forms\", func(builder *forms.FormArrayBuilder) {\n\t\tfor _, form := range db.Forms {\n\t\t\tbuilder.WriteObject(func(builder *forms.FormNodeBuilder) {\n\t\t\t\tbuilder.WriteForm(\"form\", form)\n\t\t\t})\n\t\t}\n\t})\n\n\tencoded, err := json.Marshal(builder.Node())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn string(encoded)\n}\n\nfunc GetFormByID(id string) string {\n\tform, err := db.GetForm(id)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tbuilder := forms.FormNodeBuilder{json.Builder()}\n\n\tbuilder.WriteForm(\"form\", form).\n\t\tWriteObject(\"submissions\", func(builder *forms.FormNodeBuilder) {\n\t\t\tformSubmissions := db.GetSubmissionsByFormID(form.ID)\n\t\t\tfor _, submission := range formSubmissions {\n\t\t\t\tbuilder.WriteFormSubmission(submission.Author.String(), submission)\n\t\t\t}\n\t\t})\n\n\topenAt, err := form.OpenAt()\n\tif err == nil {\n\t\tbuilder.WriteString(\"openAt\", openAt.Format(\"2006-01-02 15:04:05\"))\n\t}\n\tcloseAt, err := form.CloseAt()\n\tif err == nil {\n\t\tbuilder.WriteString(\"closeAt\", closeAt.Format(\"2006-01-02 15:04:05\"))\n\t}\n\n\tencoded, err := json.Marshal(builder.Node())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn string(encoded)\n}\n\nfunc GetAnswer(formID string, authorID string) string {\n\t_, err := db.GetForm(formID)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tanswer, err := db.GetAnswer(formID, address(authorID))\n\tif answer != nil {\n\t\tpanic(err)\n\t}\n\n\treturn answer.Answers\n}\n\nfunc SubmitForm(cur realm, formID string, answers string) {\n\t_, err := db.GetForm(formID)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tdb.SubmitForm(0, cur, formID, answers)\n}\n\nfunc Render(_ string) string {\n\tif len(db.Forms) == 0 {\n\t\tresponse := \"No forms yet !\"\n\t\treturn response\n\t}\n\n\tresponse := \"Forms:\\n\\n\"\n\tfor _, form := range db.Forms {\n\t\tresponse += ufmt.Sprintf(\"- %s\\n\\n\", GetFormByID(form.ID))\n\t}\n\tresponse += \"Answers:\\n\\n\"\n\tfor _, answer := range db.Answers {\n\t\tresponse += ufmt.Sprintf(\"- Form ID: %s\\nAuthor: %s\\nSubmitted At: %s\\n\u003eAnswers: %s\\n\\n\", answer.FormID, answer.Author, answer.SubmittedAt, answer.Answers)\n\t}\n\n\treturn response\n}\n"},{"name":"forms_test.gno","body":"package forms\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\n// cur is a zero-value realm used as a placeholder when forwarding to\n// uassert/urequire dispatch helpers that gained an `rlm realm` param.\n// These tests pass `func()` callbacks (no crossing inside the callback),\n// so rlm is ignored — a nil realm here is safe.\nvar cur realm\n\nfunc TestGetFormByID(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\ttesting.SetOriginCaller(alice)\n\ttitle := \"Simple Form\"\n\tdescription := \"This is a form\"\n\topenAt := \"2021-01-01T00:00:00Z\"\n\tcloseAt := \"2021-01-02T00:00:00Z\"\n\tdata := `[{\"label\":\"Name\",\"fieldType\":\"string\",\"required\":true},{\"label\":\"Age\",\"fieldType\":\"number\",\"required\":false},{\"label\":\"Is this a test?\",\"fieldType\":\"boolean\",\"required\":false},{\"label\":\"Favorite Food\",\"fieldType\":\"['Pizza', 'Schnitzel', 'Burger']\",\"required\":true},{\"label\":\"Favorite Foods\",\"fieldType\":\"{'Pizza', 'Schnitzel', 'Burger'}\",\"required\":true}]`\n\n\turequire.NotPanics(t, cur, func() {\n\t\tid := CreateForm(cross(cur), title, description, openAt, closeAt, data)\n\n\t\tform := GetFormByID(id)\n\n\t\turequire.True(t, strings.Contains(form, data), \"Form JSON was not rebuilt properly\")\n\t})\n}\n\nfunc TestGetForms(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\ttesting.SetOriginCaller(alice)\n\tdescription := \"This is a form\"\n\topenAt := \"2021-01-01T00:00:00Z\"\n\tcloseAt := \"2021-01-02T00:00:00Z\"\n\n\turequire.NotPanics(t, cur, func() {\n\t\tdata1 := `[{\"label\":\"Name\",\"fieldType\":\"string\",\"required\":true}]`\n\t\tCreateForm(cross(cur), \"NameForm\", description, openAt, closeAt, data1)\n\t\tdata2 := `[{\"label\":\"Age\",\"fieldType\":\"number\",\"required\":false}]`\n\t\tCreateForm(cross(cur), \"AgeForm\", description, openAt, closeAt, data2)\n\n\t\tforms := GetForms()\n\n\t\turequire.True(t, strings.Contains(forms, data1) \u0026\u0026 strings.Contains(forms, data2), \"Forms JSON were not rebuilt properly\")\n\t})\n\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/agherasie/forms\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"40BE2o2VeOdB+T+hMfrpzJAM2jlhBoLfZx0ALRdDyIgRpkE9bSzEIsyh34DFUPt3C3p3hTcBNsonYcKgy0sr3A=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"banktest","path":"gno.land/r/archive/banktest","files":[{"name":"README.md","body":"This is a simple test realm contract that demonstrates how to use the banker.\n\nSee [gno.land/r/demo/banktest/banktest.go](/r/demo/banktest/banktest.go) to see the original contract code.\n\nThis article will go through each line to explain how it works.\n\n```go\npackage banktest\n```\n\nThis package is locally named \"banktest\" (could be anything).\n\n```go\nimport (\n    \"std\"\n)\n```\n\nThe \"std\" package is defined by the gno code in stdlibs/std/. \u003c/br\u003e Self explanatory; and you'll see more usage from std later.\n\n```go\ntype activity struct {\n    caller   address\n    sent     std.Coins\n    returned std.Coins\n    time     time.Time\n}\n\nfunc (act *activity) String() string {\n    return act.caller.String() + \" \" +\n        act.sent.String() + \" sent, \" +\n        act.returned.String() + \" returned, at \" +\n        act.time.Format(\"2006-01-02 3:04pm MST\")\n}\n\nvar latest [10]*activity\n```\n\nThis is just maintaining a list of recent activity to this contract. Notice that the \"latest\" variable is defined \"globally\" within the context of the realm with path \"gno.land/r/demo/banktest\".\n\nThis means that calls to functions defined within this package are encapsulated within this \"data realm\", where the data is mutated based on transactions that can potentially cross many realm and non-realm package boundaries (in the call stack).\n\n```go\n// Deposit will take the coins (to the realm's pkgaddr) or return them to user.\nfunc Deposit(returnDenom string, returnAmount int64) string {\n    std.AssertOriginCall()\n    caller := std.OriginCaller()\n    send := std.Coins{{returnDenom, returnAmount}}\n```\n\nThis is the beginning of the definition of the contract function named \"Deposit\". `std.AssertOriginCall() asserts that this function was called by a gno transactional Message. The caller is the user who signed off on this transactional message. Send is the amount of deposit sent along with this message.\n\n```go\n    // record activity\n    act := \u0026activity{\n        caller:   caller,\n        sent:     std.OriginSend(),\n        returned: send,\n        time:     time.Now(),\n    }\n    for i := len(latest) - 2; i \u003e= 0; i-- {\n        latest[i+1] = latest[i] // shift by +1.\n    }\n    latest[0] = act\n```\n\nUpdating the \"latest\" array for viewing at gno.land/r/demo/banktest: (w/ trailing colon).\n\n```go\n    // return if any.\n    if returnAmount \u003e 0 {\n```\n\nIf the user requested the return of coins...\n\n```go\n        banker := std.NewBanker(std.BankerTypeOriginSend)\n```\n\nuse a std.Banker instance to return any deposited coins to the original sender.\n\n```go\n        pkgaddr := std.CurrentRealm().Address()\n        // TODO: use std.Coins constructors, this isn't generally safe.\n        banker.SendCoins(pkgaddr, caller, send)\n        return \"returned!\"\n```\n\nNotice that each realm package has an associated Cosmos address.\n\nFinally, the results are rendered via an ABCI query call when you visit [/r/demo/banktest:](/r/demo/banktest:).\n\n```go\nfunc Render(path string) string {\n    // get realm coins.\n    banker := std.NewReadonlyBanker()\n    coins := banker.GetCoins(std.CurrentRealm().Address())\n\n    // render\n    res := \"\"\n    res += \"## recent activity\\n\"\n    res += \"\\n\"\n    for _, act := range latest {\n        if act == nil {\n            break\n        }\n        res += \" * \" + act.String() + \"\\n\"\n    }\n    res += \"\\n\"\n    res += \"## total deposits\\n\"\n    res += coins.String()\n    return res\n}\n```\n\nYou can call this contract yourself, by vistiing [/r/demo/banktest](/r/demo/banktest) and the [quickstart guide](/r/demo/boards:gnolang/4).\n"},{"name":"banktest.gno","body":"package banktest\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"chain/runtime\"\n\t\"chain/runtime/unsafe\"\n\t\"time\"\n)\n\ntype activity struct {\n\tcaller   address\n\tsent     chain.Coins\n\treturned chain.Coins\n\ttime     time.Time\n}\n\nfunc (act *activity) String() string {\n\treturn act.caller.String() + \" \" +\n\t\tact.sent.String() + \" sent, \" +\n\t\tact.returned.String() + \" returned, at \" +\n\t\tact.time.Format(\"2006-01-02 3:04pm MST\")\n}\n\nvar latest [10]*activity\n\nfunc addActivity(act *activity) {\n\tfor i := len(latest) - 2; i \u003e= 0; i-- {\n\t\tlatest[i+1] = latest[i] // shift by +1.\n\t}\n\tlatest[0] = act\n}\n\n// Deposit will take the coins (to the realm's pkgaddr) if returnAmount is 0,\n// or return the specified return amount back to user.\nfunc Deposit(cur realm, returnDenom string, returnAmount int64) string {\n\truntime.AssertOriginCall()\n\tcaller := unsafe.OriginCaller()\n\tsend := chain.Coins{{returnDenom, returnAmount}}\n\t// record activity\n\tact := \u0026activity{\n\t\tcaller:   caller,\n\t\tsent:     unsafe.OriginSend(),\n\t\treturned: send,\n\t\ttime:     time.Now(),\n\t}\n\taddActivity(act)\n\t// return if any.\n\tif returnAmount \u003e 0 {\n\t\tbanker_ := banker.NewBanker(banker.BankerTypeOriginSend, cur)\n\t\tpkgaddr := cur.Address()\n\t\t// TODO: use std.Coins constructors, this isn't generally safe.\n\t\tbanker_.SendCoins(pkgaddr, caller, send)\n\t\treturn \"returned!\"\n\t} else {\n\t\treturn \"thank you!\"\n\t}\n}\n\nfunc bankerAddr(cur realm) address {\n\treturn cur.Address()\n}\n\nfunc Render(cur realm, path string) string {\n\t// get realm coins.\n\tbanker_ := banker.NewReadonlyBanker()\n\tcoins := banker_.GetCoins(bankerAddr(cross(cur)))\n\n\t// render\n\tres := \"\"\n\tres += \"## recent activity\\n\"\n\tres += \"\\n\"\n\tfor _, act := range latest {\n\t\tif act == nil {\n\t\t\tbreak\n\t\t}\n\t\tres += \" * \" + act.String() + \"\\n\"\n\t}\n\tres += \"\\n\"\n\tres += \"## total deposits\\n\"\n\tres += coins.String()\n\treturn res\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/archive/banktest\"\ngno = \"0.9\"\n"},{"name":"z_0_filetest.gno","body":"// PKGPATH: gno.land/r/demo/bank1\n// SEND: 100000000ugnot\n\npackage bank1\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n\n\t\"gno.land/r/archive/banktest\"\n)\n\nfunc main(cur realm) {\n\t// set up main address and banktest addr.\n\tbanktestAddr := chain.PackageAddress(\"gno.land/r/archive/banktest\")\n\tmainaddr := chain.PackageAddress(\"gno.land/r/demo/bank1\")\n\ttesting.SetOriginCaller(mainaddr)\n\n\t// get and print balance of mainaddr.\n\t// with the SEND, + 200 gnot given by the TestContext, main should have 300gnot.\n\tbanker_ := banker.NewBanker(banker.BankerTypeRealmSend, cur)\n\tmainbal := banker_.GetCoins(mainaddr)\n\tprintln(\"main before:\", mainbal)\n\n\t// simulate a Deposit call. use Send + OriginSend to simulate -send.\n\tbanker_.SendCoins(mainaddr, banktestAddr, chain.Coins{{\"ugnot\", 100_000_000}})\n\ttesting.SetOriginSend(chain.Coins{{\"ugnot\", 100_000_000}})\n\t// The -send envelope lands on banktest, so name banktest as the payee.\n\t// Must come before the user-realm line below: after it, cur is a code\n\t// realm and banker.NewBanker(BankerTypeOriginSend, cur) is refused.\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/archive/banktest\"))\n\ttesting.SetRealm(testing.NewUserRealm(mainaddr))\n\tres := banktest.Deposit(cross(cur), \"ugnot\", 50_000_000) // bank1 can't send? should be r/demo/bank1 to r/demo/banktest, is bank1 -\u003e bank1.\n\tprintln(\"Deposit():\", res)\n\n\t// print main balance after.\n\tmainbal = banker_.GetCoins(mainaddr)\n\tprintln(\"main after:\", mainbal)\n\n\t// simulate a Render(). banker should have given back all coins.\n\tres = banktest.Render(cross(cur), \"\")\n\tprintln(res)\n}\n\n// Output:\n// main before: 100000000ugnot\n// Deposit(): returned!\n// main after: 50000000ugnot\n// ## recent activity\n//\n//  * g1tnpdmvrmtgql8fmxgsq9rwtst5hsxahk3f05dk 100000000ugnot sent, 50000000ugnot returned, at 2009-02-13 11:31pm UTC\n//\n// ## total deposits\n// 50000000ugnot\n"},{"name":"z_1_filetest.gno","body":"// PKGPATH: gno.land/r/demo/bank1\n\npackage bank1\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\t\"gno.land/r/archive/banktest\"\n)\n\nfunc main(cur realm) {\n\tbanktestAddr := chain.PackageAddress(\"gno.land/r/archive/banktest\")\n\tmainaddr := chain.PackageAddress(\"gno.land/r/demo/bank1\")\n\n\t// simulate a Deposit call.\n\ttesting.IssueCoins(banktestAddr, chain.Coins{{\"ugnot\", 100000000}})\n\ttesting.SetOriginSend(chain.Coins{{\"ugnot\", 100000000}})\n\t// The -send envelope lands on banktest, so name banktest as the payee.\n\t// Must come before the user-realm line below: after it, cur is a code\n\t// realm and banker.NewBanker(BankerTypeOriginSend, cur) is refused.\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/archive/banktest\"))\n\ttesting.SetRealm(testing.NewUserRealm(mainaddr))\n\n\tres := banktest.Deposit(cross(cur), \"ugnot\", 101000000)\n\tprintln(res)\n}\n\n// Error:\n// cannot send \"101000000ugnot\", limit \"100000000ugnot\" exceeded with \"\" already spent\n"},{"name":"z_2_filetest.gno","body":"// PKGPATH: gno.land/r/demo/bank1\n\npackage bank1\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n\n\t\"gno.land/r/archive/banktest\"\n)\n\nfunc main(cur realm) {\n\tbanktestAddr := chain.PackageAddress(\"gno.land/r/archive/banktest\")\n\tmainaddr := chain.PackageAddress(\"gno.land/r/demo/bank1\")\n\n\t// print main balance before.\n\ttesting.SetOriginCaller(mainaddr)\n\n\tbanker_ := banker.NewReadonlyBanker()\n\tmainbal := banker_.GetCoins(mainaddr)\n\tprintln(\"main before:\", mainbal) // plus OriginSend equals 300.\n\n\t// simulate a Deposit call.\n\ttesting.IssueCoins(banktestAddr, chain.Coins{{\"ugnot\", 100000000}})\n\ttesting.SetOriginSend(chain.Coins{{\"ugnot\", 100000000}})\n\t// The -send envelope lands on banktest, so name banktest as the payee.\n\t// Must come before the user-realm line below: after it, cur is a code\n\t// realm and banker.NewBanker(BankerTypeOriginSend, cur) is refused.\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/archive/banktest\"))\n\ttesting.SetRealm(testing.NewUserRealm(mainaddr))\n\tres := banktest.Deposit(cross(cur), \"ugnot\", 55000000)\n\tprintln(\"Deposit():\", res)\n\n\t// print main balance after.\n\tmainbal = banker_.GetCoins(mainaddr)\n\tprintln(\"main after:\", mainbal) // now 255.\n\n\t// simulate a Render().\n\tres = banktest.Render(cross(cur), \"\")\n\tprintln(res)\n}\n\n// Output:\n// main before:\n// Deposit(): returned!\n// main after: 55000000ugnot\n// ## recent activity\n//\n//  * g1tnpdmvrmtgql8fmxgsq9rwtst5hsxahk3f05dk 100000000ugnot sent, 55000000ugnot returned, at 2009-02-13 11:31pm UTC\n//\n// ## total deposits\n// 45000000ugnot\n"},{"name":"z_3_filetest.gno","body":"// PKGPATH: gno.land/r/demo/bank1\n\npackage bank1\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n)\n\nfunc main(cur realm) {\n\tbanktestAddr := chain.PackageAddress(\"gno.land/r/archive/banktest\")\n\n\tmainaddr := chain.PackageAddress(\"gno.land/r/demo/bank1\")\n\ttesting.SetOriginCaller(mainaddr)\n\n\tbanker_ := banker.NewBanker(banker.BankerTypeRealmSend, cur)\n\tsend := chain.Coins{{\"ugnot\", 123}}\n\tbanker_.SendCoins(banktestAddr, mainaddr, send)\n}\n\n// Error:\n// can only send coins from realm that created banker \"g1tnpdmvrmtgql8fmxgsq9rwtst5hsxahk3f05dk\", not \"g1hsf4l9pxp4vklr6udmr29ztgqvhswjl8mt78fh\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"YDcZ9iQHubCUQh/Nr+3QON5RVUkSnVIUJuFVIc98UfVXriL1NKUrdmXLhIu5LDrZGpiZM0zT4gQ5OTcryYAMJQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"groups","path":"gno.land/r/archive/groups","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/archive/groups\"\ngno = \"0.9\"\n"},{"name":"group.gno","body":"package groups\n\nimport (\n\t\"strconv\"\n\t\"time\"\n\n\t\"gno.land/p/nt/avl/v0\"\n)\n\ntype GroupID uint64\n\nfunc (gid GroupID) String() string {\n\treturn strconv.Itoa(int(gid))\n}\n\ntype Group struct {\n\tid           GroupID\n\turl          string\n\tname         string\n\tlastMemberID MemberID\n\tmembers      avl.Tree\n\tcreator      address\n\tcreatedAt    time.Time\n}\n\nfunc newGroup(url string, name string, creator address) *Group {\n\tif !reName.MatchString(name) {\n\t\tpanic(\"invalid name: \" + name)\n\t}\n\tif gGroupsByName.Has(name) {\n\t\tpanic(\"Group with such name already exists\")\n\t}\n\treturn \u0026Group{\n\t\tid:        incGetGroupID(),\n\t\turl:       url,\n\t\tname:      name,\n\t\tcreator:   creator,\n\t\tmembers:   avl.Tree{},\n\t\tcreatedAt: time.Now(),\n\t}\n}\n\nfunc (group *Group) newMember(id MemberID, address_XXX address, weight int, metadata string) *Member {\n\tif group.members.Has(address_XXX.String()) {\n\t\tpanic(\"this member for this group already exists\")\n\t}\n\treturn \u0026Member{\n\t\tid:          id,\n\t\taddress_XXX: address_XXX,\n\t\tweight:      weight,\n\t\tmetadata:    metadata,\n\t\tcreatedAt:   time.Now(),\n\t}\n}\n\nfunc (group *Group) HasPermission(addr address, perm Permission) bool {\n\tif group.creator != addr {\n\t\treturn false\n\t}\n\treturn isValidPermission(perm)\n}\n\nfunc (group *Group) RenderGroup() string {\n\tstr := \"Group ID: \" + groupIDKey(group.id) + \"\\n\\n\" +\n\t\t\"Group Name: \" + group.name + \"\\n\\n\" +\n\t\t\"Group Creator: \" + usernameOf(group.creator) + \"\\n\\n\" +\n\t\t\"Group createdAt: \" + group.createdAt.String() + \"\\n\\n\" +\n\t\t\"Group Last MemberID: \" + memberIDKey(group.lastMemberID) + \"\\n\\n\"\n\n\tstr += \"Group Members: \\n\\n\"\n\tgroup.members.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\tmember := value.(*Member)\n\t\tstr += member.getMemberStr()\n\t\treturn false\n\t})\n\treturn str\n}\n\nfunc (group *Group) deleteGroup() {\n\tgidkey := groupIDKey(group.id)\n\t_, gGroupsRemoved := gGroups.Remove(gidkey)\n\tif !gGroupsRemoved {\n\t\tpanic(\"group does not exist with id \" + group.id.String())\n\t}\n\tgGroupsByName.Remove(group.name)\n}\n\nfunc (group *Group) deleteMember(mid MemberID) {\n\tgidkey := groupIDKey(group.id)\n\tif !gGroups.Has(gidkey) {\n\t\tpanic(\"group does not exist with id \" + group.id.String())\n\t}\n\n\tg := getGroup(group.id)\n\tmidkey := memberIDKey(mid)\n\tg.members.Remove(midkey)\n}\n"},{"name":"groups.gno","body":"package groups\n\nimport (\n\t\"regexp\"\n\n\t\"gno.land/p/nt/avl/v0\"\n)\n\n//----------------------------------------\n// Realm (package) state\n\nvar (\n\tgGroups       avl.Tree // id -\u003e *Group\n\tgGroupsCtr    int      // increments Group.id\n\tgGroupsByName avl.Tree // name -\u003e *Group\n)\n\n//----------------------------------------\n// Constants\n\nvar reName = regexp.MustCompile(`^[a-z]+[_a-z0-9]{2,29}$`)\n"},{"name":"member.gno","body":"package groups\n\nimport (\n\t\"strconv\"\n\t\"time\"\n)\n\ntype MemberID uint64\n\ntype Member struct {\n\tid          MemberID\n\taddress_XXX address\n\tweight      int\n\tmetadata    string\n\tcreatedAt   time.Time\n}\n\nfunc (mid MemberID) String() string {\n\treturn strconv.Itoa(int(mid))\n}\n\nfunc (member *Member) getMemberStr() string {\n\tmemberDataStr := \"\"\n\tmemberDataStr += \"\\t\\t\\t[\" + memberIDKey(member.id) + \", \" + member.address_XXX.String() + \", \" + strconv.Itoa(member.weight) + \", \" + member.metadata + \", \" + member.createdAt.String() + \"],\\n\\n\"\n\treturn memberDataStr\n}\n"},{"name":"misc.gno","body":"package groups\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 getGroup(gid GroupID) *Group {\n\tgidkey := groupIDKey(gid)\n\tgroup_ := gGroups.Get(gidkey)\n\tif group_ == nil {\n\t\tpanic(\"group id (\" + gid.String() + \") does not exists\")\n\t}\n\tgroup := group_.(*Group)\n\treturn group\n}\n\nfunc incGetGroupID() GroupID {\n\tgGroupsCtr++\n\treturn GroupID(gGroupsCtr)\n}\n\nfunc padLeft(str string, length int) string {\n\tif len(str) \u003e= length {\n\t\treturn str\n\t}\n\treturn strings.Repeat(\" \", length-len(str)) + str\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}\n\treturn strings.Repeat(\"0\", length-len(str)) + str\n}\n\nfunc groupIDKey(gid GroupID) string {\n\treturn padZero(uint64(gid), 10)\n}\n\nfunc memberIDKey(mid MemberID) string {\n\treturn padZero(uint64(mid), 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}\n\treturn \"[@\" + user.Name() + \"](/u/\" + user.Name() + \")\"\n}\n\nfunc usernameOf(addr address) string {\n\tuser := users.ResolveAddress(addr)\n\tif user == nil {\n\t\tpanic(\"user not found\")\n\t}\n\treturn user.Name()\n}\n\nfunc isValidPermission(perm Permission) bool {\n\treturn perm == EditPermission || perm == DeletePermission\n}\n"},{"name":"public.gno","body":"package groups\n\nimport (\n\t\"chain/runtime\"\n\t\"chain/runtime/unsafe\"\n\n\t\"gno.land/r/sys/users\"\n)\n\n//----------------------------------------\n// Public facing functions\n\nfunc GetGroupIDFromName(name string) (GroupID, bool) {\n\tgroupI := gGroupsByName.Get(name)\n\tif groupI == nil {\n\t\treturn 0, false\n\t}\n\treturn groupI.(*Group).id, true\n}\n\nfunc CreateGroup(cur realm, name string) GroupID {\n\truntime.AssertOriginCall()\n\tcaller := unsafe.OriginCaller()\n\tusernameOf(caller)\n\turl := \"/r/demo/groups:\" + name\n\tgroup := newGroup(url, name, caller)\n\tgidkey := groupIDKey(group.id)\n\tgGroups.Set(gidkey, group)\n\tgGroupsByName.Set(name, group)\n\treturn group.id\n}\n\nfunc AddMember(cur realm, gid GroupID, address_XXX string, weight int, metadata string) MemberID {\n\truntime.AssertOriginCall()\n\tcaller := unsafe.OriginCaller()\n\tusernameOf(caller)\n\tgroup := getGroup(gid)\n\tif !group.HasPermission(caller, EditPermission) {\n\t\tpanic(\"unauthorized to edit group\")\n\t}\n\tuser := users.ResolveAddress(address(address_XXX))\n\tif user == nil {\n\t\tpanic(\"unknown address \" + address_XXX)\n\t}\n\tmid := group.lastMemberID\n\tmember := group.newMember(mid, address(address_XXX), weight, metadata)\n\tmidkey := memberIDKey(mid)\n\tgroup.members.Set(midkey, member)\n\tmid++\n\tgroup.lastMemberID = mid\n\treturn member.id\n}\n\nfunc DeleteGroup(cur realm, gid GroupID) {\n\truntime.AssertOriginCall()\n\tcaller := unsafe.OriginCaller()\n\tgroup := getGroup(gid)\n\tif !group.HasPermission(caller, DeletePermission) {\n\t\tpanic(\"unauthorized to delete group\")\n\t}\n\tgroup.deleteGroup()\n}\n\nfunc DeleteMember(cur realm, gid GroupID, mid MemberID) {\n\truntime.AssertOriginCall()\n\tcaller := unsafe.OriginCaller()\n\tgroup := getGroup(gid)\n\tif !group.HasPermission(caller, DeletePermission) {\n\t\tpanic(\"unauthorized to delete member\")\n\t}\n\tgroup.deleteMember(mid)\n}\n"},{"name":"render.gno","body":"package groups\n\nimport (\n\t\"strings\"\n)\n\n//----------------------------------------\n// Render functions\n\nfunc RenderGroup(gid GroupID) string {\n\tgroup := getGroup(gid)\n\tif group == nil {\n\t\treturn \"missing Group\"\n\t}\n\treturn group.RenderGroup()\n}\n\nfunc Render(path string) string {\n\tif path == \"\" {\n\t\tstr := \"List of all Groups:\\n\\n\"\n\t\tgGroups.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\t\tgroup := value.(*Group)\n\t\t\tstr += \" * [\" + group.name + \"](\" + group.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/demo/groups:Group_NAME\n\t\tname := parts[0]\n\t\tgroupI := gGroupsByName.Get(name)\n\t\tif groupI == nil {\n\t\t\treturn \"Group does not exist: \" + name\n\t\t}\n\t\treturn groupI.(*Group).RenderGroup()\n\t} else {\n\t\treturn \"unrecognized path \" + path\n\t}\n}\n"},{"name":"role.gno","body":"package groups\n\ntype Permission string\n\nconst (\n\tDeletePermission Permission = \"role:delete\"\n\tEditPermission   Permission = \"role:edit\"\n)\n"},{"name":"z_0_a_filetest.gno","body":"// PKGPATH: gno.land/r/archive/groups/filetests/z_0_a\npackage z_0_a\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"gno.land/r/archive/groups\"\n)\n\nvar gid groups.GroupID\n\nfunc main(cur realm) {\n\tgid = groups.CreateGroup(cross(cur), \"test_group\")\n\tprintln(gid)\n\tprintln(groups.Render(\"\"))\n}\n\n// Error:\n// user not found\n"},{"name":"z_0_c_filetest.gno","body":"// PKGPATH: gno.land/r/archive/groups/filetests/z_0_c\npackage z_0_c\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/archive/groups\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar gid groups.GroupID\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\tgid = groups.CreateGroup(cross(cur), \"test_group\")\n\tprintln(gid)\n\tprintln(groups.Render(\"\"))\n}\n\n// Output:\n// 1\n// List of all Groups:\n//\n//  * [test_group](/r/demo/groups:test_group)\n"},{"name":"z_1_a_filetest.gno","body":"// PKGPATH: gno.land/r/archive/groups/filetests/z_1_a\npackage z_1_a\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"chain/runtime/unsafe\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/r/archive/groups\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar gid groups.GroupID\n\nconst admin = address(\"g1manfred47kzduec920z88wfr64ylksmdcedlf5\")\n\nfunc main(cur realm) {\n\tcaller := unsafe.OriginCaller() // main\n\ttesting.SetRealm(testing.NewUserRealm(caller))\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"main123\", caller)\n\ttesting.SetHeight(123)\n\n\ttest1 := testutils.TestAddress(\"gnouser1\")\n\ttesting.SetOriginCaller(test1)\n\ttesting.SetRealm(testing.NewUserRealm(test1))\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"test123\", test1)\n\ttesting.SetHeight(123)\n\n\ttest2 := testutils.TestAddress(\"gnouser2\")\n\ttesting.SetOriginCaller(test2)\n\ttesting.SetRealm(testing.NewUserRealm(test2))\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"test223\", test2)\n\ttesting.SetHeight(123)\n\n\ttest3 := testutils.TestAddress(\"gnouser3\")\n\ttesting.SetOriginCaller(test3)\n\ttesting.SetRealm(testing.NewUserRealm(test3))\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"test323\", test3)\n\ttesting.SetHeight(123)\n\n\ttesting.SetOriginCaller(caller)\n\ttesting.SetRealm(testing.NewUserRealm(caller))\n\n\tgid = groups.CreateGroup(cross(cur), \"test_group\")\n\tprintln(gid)\n\n\tgroups.AddMember(cross(cur), gid, test3.String(), 32, \"i am from UAE\")\n\tprintln(groups.Render(\"test_group\"))\n}\n\n// Output:\n// 1\n// Group ID: 0000000001\n//\n// Group Name: test_group\n//\n// Group Creator: main123\n//\n// Group createdAt: 2009-02-13 23:31:30 +0000 UTC\n//\n// Group Last MemberID: 0000000001\n//\n// Group Members:\n//\n// \t\t\t[0000000000, g1vahx7atnv4erxh6lta047h6lta047h6ll85gpy, 32, i am from UAE, 2009-02-13 23:31:30 +0000 UTC],\n"},{"name":"z_1_b_filetest.gno","body":"// PKGPATH: gno.land/r/archive/groups/filetests/z_1_b\npackage z_1_b\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"chain/runtime/unsafe\"\n\t\"testing\"\n\n\t\"gno.land/r/archive/groups\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar gid groups.GroupID\n\nfunc main(cur realm) {\n\tcaller := unsafe.OriginCaller()\n\ttesting.SetRealm(testing.NewUserRealm(caller))\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", caller)\n\ttesting.SetHeight(123)\n\n\tgid = groups.CreateGroup(cross(cur), \"test_group\")\n\tprintln(gid)\n\tgroups.AddMember(cross(cur), 2, \"g1vahx7atnv4erxh6lta047h6lta047h6ll85gpy\", 55, \"metadata3\")\n\tprintln(groups.Render(\"\"))\n}\n\n// Error:\n// group id (2) does not exists\n"},{"name":"z_1_c_filetest.gno","body":"// PKGPATH: gno.land/r/archive/groups/filetests/z_1_c\npackage z_1_c\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/r/archive/groups\"\n)\n\nvar gid groups.GroupID\n\nfunc main(cur realm) {\n\tgid = groups.CreateGroup(cross(cur), \"test_group\")\n\tprintln(gid)\n\n\t// add member via anon user\n\ttest2 := testutils.TestAddress(\"test2\")\n\ttesting.SetOriginCaller(test2)\n\ttesting.SetOriginSend(chain.Coins{{\"ugnot\", 9000000}})\n\n\tgroups.AddMember(cross(cur), gid, test2.String(), 42, \"metadata3\")\n}\n\n// Error:\n// user not found\n"},{"name":"z_2_a_filetest.gno","body":"// PKGPATH: gno.land/r/archive/groups/filetests/z_2_a\npackage z_2_a\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"chain/runtime/unsafe\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/r/archive/groups\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar gid groups.GroupID\n\nconst admin = address(\"g1manfred47kzduec920z88wfr64ylksmdcedlf5\")\n\nfunc main(cur realm) {\n\tcaller := unsafe.OriginCaller() // main123\n\ttesting.SetRealm(testing.NewUserRealm(caller))\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"main123\", caller)\n\ttesting.SetHeight(123)\n\n\ttest1 := testutils.TestAddress(\"gnouser1\")\n\ttesting.SetOriginCaller(test1)\n\ttesting.SetRealm(testing.NewUserRealm(test1))\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"test123\", test1)\n\ttesting.SetHeight(123)\n\n\ttest2 := testutils.TestAddress(\"gnouser2\")\n\ttesting.SetOriginCaller(test2)\n\ttesting.SetRealm(testing.NewUserRealm(test2))\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"test223\", test2)\n\ttesting.SetHeight(123)\n\n\ttest3 := testutils.TestAddress(\"gnouser3\")\n\ttesting.SetOriginCaller(test3)\n\ttesting.SetRealm(testing.NewUserRealm(test3))\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"test323\", test3)\n\ttesting.SetHeight(123)\n\n\ttesting.SetOriginCaller(caller)\n\ttesting.SetRealm(testing.NewUserRealm(caller))\n\n\tgid = groups.CreateGroup(cross(cur), \"test_group\")\n\tprintln(groups.Render(\"test_group\"))\n\n\tgroups.AddMember(cross(cur), gid, test2.String(), 42, \"metadata3\")\n\n\tgroups.DeleteMember(cross(cur), gid, 0)\n\tprintln(groups.RenderGroup(gid))\n}\n\n// Output:\n// Group ID: 0000000001\n//\n// Group Name: test_group\n//\n// Group Creator: main123\n//\n// Group createdAt: 2009-02-13 23:31:30 +0000 UTC\n//\n// Group Last MemberID: 0000000000\n//\n// Group Members:\n//\n//\n// Group ID: 0000000001\n//\n// Group Name: test_group\n//\n// Group Creator: main123\n//\n// Group createdAt: 2009-02-13 23:31:30 +0000 UTC\n//\n// Group Last MemberID: 0000000001\n//\n// Group Members:\n"},{"name":"z_2_b_filetest.gno","body":"// PKGPATH: gno.land/r/archive/groups/filetests/z_2_b\npackage z_2_b\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/archive/groups\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar gid groups.GroupID\n\nfunc main(cur realm) {\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\tgid = groups.CreateGroup(cross(cur), \"test_group\")\n\tprintln(gid)\n\tgroups.DeleteMember(cross(cur), 2, 0)\n\tprintln(groups.Render(\"\"))\n}\n\n// Output:\n// 1\n\n// Error:\n// group id (2) does not exists\n"},{"name":"z_2_d_filetest.gno","body":"// PKGPATH: gno.land/r/archive/groups/filetests/z_2_d\npackage z_2_d\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/r/archive/groups\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar gid groups.GroupID\n\nfunc main(cur realm) {\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\tgid = groups.CreateGroup(cross(cur), \"test_group\")\n\tprintln(gid)\n\n\t// delete member via anon user\n\ttest2 := testutils.TestAddress(\"test2\")\n\ttesting.SetOriginCaller(test2)\n\ttesting.SetOriginSend(chain.Coins{{\"ugnot\", 9000000}})\n\n\tgroups.DeleteMember(cross(cur), gid, 0)\n\tprintln(groups.Render(\"\"))\n}\n\n// Output:\n// 1\n\n// Error:\n// unauthorized to delete member\n"},{"name":"z_2_e_filetest.gno","body":"// PKGPATH: gno.land/r/archive/groups/filetests/z_2_e\npackage z_2_e\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/r/archive/groups\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar gid groups.GroupID\n\nfunc main(cur realm) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\ttesting.SetHeight(123)\n\tgid = groups.CreateGroup(cross(cur), \"test_group\")\n\tprintln(gid)\n\tgroups.DeleteGroup(cross(cur), gid)\n\tprintln(groups.Render(\"\"))\n}\n\n// Output:\n// 1\n// List of all Groups:\n"},{"name":"z_2_f_filetest.gno","body":"// PKGPATH: gno.land/r/archive/groups/filetests/z_2_f\npackage z_2_f\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"chain/runtime/unsafe\"\n\t\"testing\"\n\n\t\"gno.land/r/archive/groups\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar gid groups.GroupID\n\nfunc main(cur realm) {\n\tcaller := unsafe.OriginCaller()\n\ttesting.SetRealm(testing.NewUserRealm(caller))\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", caller)\n\ttesting.SetHeight(123)\n\n\tgid = groups.CreateGroup(cross(cur), \"test_group\")\n\n\tprintln(gid)\n\tgroups.DeleteGroup(cross(cur), 20)\n\tprintln(groups.Render(\"\"))\n}\n\n// Error:\n// group id (20) does not exists\n"},{"name":"z_2_g_filetest.gno","body":"// PKGPATH: gno.land/r/archive/groups/filetests/z_2_g\npackage z_2_g\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"chain\"\n\t\"chain/runtime/unsafe\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/r/archive/groups\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nvar gid groups.GroupID\n\nfunc main(cur realm) {\n\tcaller := unsafe.OriginCaller()\n\ttesting.SetRealm(testing.NewUserRealm(caller))\n\ttesting.SetHeight(0)\n\tuinit.RegisterUser(cross(cur), \"gnouser123\", caller)\n\ttesting.SetHeight(123)\n\n\tgid = groups.CreateGroup(cross(cur), \"test_group\")\n\tprintln(gid)\n\n\t// delete group via anon user\n\ttest2 := testutils.TestAddress(\"test2\")\n\ttesting.SetOriginCaller(test2)\n\ttesting.SetOriginSend(chain.Coins{{\"ugnot\", 9000000}})\n\n\tgroups.DeleteGroup(cross(cur), gid)\n\tprintln(groups.Render(\"\"))\n}\n\n// Error:\n// unauthorized to delete group\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"4307KQsnS29MFP4PSPIZxVtACMbXAeF/TY12F6gLMaY+0yUrFITN4KquHkQHzU8oL+6E9ksAPgIi/ySTAn6t6w=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"keystore","path":"gno.land/r/archive/keystore","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/archive/keystore\"\ngno = \"0.9\"\n"},{"name":"keystore.gno","body":"package keystore\n\nimport (\n\t\"chain/runtime/unsafe\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nvar data avl.Tree\n\nconst (\n\tBaseURL               = \"/r/demo/keystore\"\n\tStatusOK              = \"ok\"\n\tStatusNoUser          = \"user not found\"\n\tStatusNotFound        = \"key not found\"\n\tStatusNoWriteAccess   = \"no write access\"\n\tStatusCouldNotExecute = \"could not execute\"\n\tStatusNoDatabases     = \"no databases\"\n)\n\nfunc init() {\n\tdata = avl.Tree{} // user -\u003e avl.Tree\n}\n\n// KeyStore stores the owner-specific avl.Tree\ntype KeyStore struct {\n\tOwner address\n\tData  avl.Tree\n}\n\n// Set will set a value to a key\n// requires write-access (original caller must be caller)\nfunc Set(_ realm, k, v string) string {\n\torigOwner := unsafe.OriginCaller()\n\treturn set(origOwner.String(), k, v)\n}\n\n// set (private) will set a key to value\n// requires write-access (original caller must be caller)\nfunc set(owner, k, v string) string {\n\torigOwner := unsafe.OriginCaller()\n\tif origOwner.String() != owner {\n\t\treturn StatusNoWriteAccess\n\t}\n\tvar keystore *KeyStore\n\tkeystoreInterface := data.Get(owner)\n\tif keystoreInterface == nil {\n\t\tkeystore = \u0026KeyStore{\n\t\t\tOwner: origOwner,\n\t\t\tData:  avl.Tree{},\n\t\t}\n\t\tdata.Set(owner, keystore)\n\t} else {\n\t\tkeystore = keystoreInterface.(*KeyStore)\n\t}\n\tkeystore.Data.Set(k, v)\n\treturn StatusOK\n}\n\n// Remove removes a key\n// requires write-access (original owner must be caller)\nfunc Remove(_ realm, k string) string {\n\torigOwner := unsafe.OriginCaller()\n\treturn remove(origOwner.String(), k)\n}\n\n// remove (private) removes a key\n// requires write-access (original owner must be caller)\nfunc remove(owner, k string) string {\n\torigOwner := unsafe.OriginCaller()\n\tif origOwner.String() != owner {\n\t\treturn StatusNoWriteAccess\n\t}\n\tvar keystore *KeyStore\n\tkeystoreInterface := data.Get(owner)\n\tif keystoreInterface == nil {\n\t\tkeystore = \u0026KeyStore{\n\t\t\tOwner: origOwner,\n\t\t\tData:  avl.Tree{},\n\t\t}\n\t\tdata.Set(owner, keystore)\n\t} else {\n\t\tkeystore = keystoreInterface.(*KeyStore)\n\t}\n\t_, removed := keystore.Data.Remove(k)\n\tif !removed {\n\t\treturn StatusCouldNotExecute\n\t}\n\treturn StatusOK\n}\n\n// Get returns a value for a key\n// read-only\nfunc Get(k string) string {\n\torigOwner := unsafe.OriginCaller()\n\treturn remove(origOwner.String(), k)\n}\n\n// get (private) returns a value for a key\n// read-only\nfunc get(owner, k string) string {\n\tkeystoreInterface := data.Get(owner)\n\tif keystoreInterface == nil {\n\t\treturn StatusNoUser\n\t}\n\tkeystore := keystoreInterface.(*KeyStore)\n\tval := keystore.Data.Get(k)\n\tif val == nil {\n\t\treturn StatusNotFound\n\t}\n\treturn val.(string)\n}\n\n// Size returns size of database\n// read-only\nfunc Size() string {\n\torigOwner := unsafe.OriginCaller()\n\treturn size(origOwner.String())\n}\n\nfunc size(owner string) string {\n\tkeystoreInterface := data.Get(owner)\n\tif keystoreInterface == nil {\n\t\treturn StatusNoUser\n\t}\n\tkeystore := keystoreInterface.(*KeyStore)\n\treturn ufmt.Sprintf(\"%d\", keystore.Data.Size())\n}\n\n// Render provides read-only url access to the functions of the keystore\n// \"\" -\u003e show all keystores listed by owner\n// \"owner\" -\u003e show all keys for that owner's keystore\n// \"owner:size\" -\u003e returns size of owner's keystore\n// \"owner:get:key\" -\u003e show value for that key in owner's keystore\nfunc Render(p string) string {\n\tvar response string\n\targs := strings.Split(p, \":\")\n\tnumArgs := len(args)\n\tif p == \"\" {\n\t\tnumArgs = 0\n\t}\n\tswitch numArgs {\n\tcase 0:\n\t\tif data.Size() == 0 {\n\t\t\treturn StatusNoDatabases\n\t\t}\n\t\tdata.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\t\tks := value.(*KeyStore)\n\t\t\tresponse += ufmt.Sprintf(\"- [%s](%s:%s) (%d keys)\\n\", ks.Owner, BaseURL, ks.Owner, ks.Data.Size())\n\t\t\treturn false\n\t\t})\n\tcase 1:\n\t\towner := args[0]\n\t\tkeystoreInterface := data.Get(owner)\n\t\tif keystoreInterface == nil {\n\t\t\treturn StatusNoUser\n\t\t}\n\t\tks := keystoreInterface.(*KeyStore)\n\t\ti := 0\n\t\tresponse += ufmt.Sprintf(\"# %s database\\n\\n\", ks.Owner)\n\t\tks.Data.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\t\tresponse += ufmt.Sprintf(\"- %d [%s](%s:%s:get:%s)\\n\", i, key, BaseURL, ks.Owner, key)\n\t\t\ti++\n\t\t\treturn false\n\t\t})\n\tcase 2:\n\t\towner := args[0]\n\t\tcmd := args[1]\n\t\tif cmd == \"size\" {\n\t\t\treturn size(owner)\n\t\t}\n\tcase 3:\n\t\towner := args[0]\n\t\tcmd := args[1]\n\t\tkey := args[2]\n\t\tif cmd == \"get\" {\n\t\t\treturn get(owner, key)\n\t\t}\n\t}\n\n\treturn response\n}\n"},{"name":"keystore_test.gno","body":"package keystore\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nfunc TestRender(cur realm, t *testing.T) {\n\tvar (\n\t\tauthor1 address = testutils.TestAddress(\"author1\")\n\t\tauthor2 address = testutils.TestAddress(\"author2\")\n\t)\n\n\ttt := []struct {\n\t\tcaller address\n\t\towner  address\n\t\tps     []string\n\t\texp    string\n\t}{\n\t\t// can set database if the owner is the caller\n\t\t{author1, author1, []string{\"set\", \"hello\", \"gno\"}, StatusOK},\n\t\t{author1, author1, []string{\"size\"}, \"1\"},\n\t\t{author1, author1, []string{\"set\", \"hello\", \"world\"}, StatusOK},\n\t\t{author1, author1, []string{\"size\"}, \"1\"},\n\t\t{author1, author1, []string{\"set\", \"hi\", \"gno\"}, StatusOK},\n\t\t{author1, author1, []string{\"size\"}, \"2\"},\n\t\t// only owner can remove\n\t\t{author1, author1, []string{\"remove\", \"hi\"}, StatusOK},\n\t\t{author1, author1, []string{\"get\", \"hi\"}, StatusNotFound},\n\t\t{author1, author1, []string{\"size\"}, \"1\"},\n\t\t// add back\n\t\t{author1, author1, []string{\"set\", \"hi\", \"gno\"}, StatusOK},\n\t\t{author1, author1, []string{\"size\"}, \"2\"},\n\n\t\t// different owner has different database\n\t\t{author2, author2, []string{\"set\", \"hello\", \"universe\"}, StatusOK},\n\t\t// either author can get the other info\n\t\t{author1, author2, []string{\"get\", \"hello\"}, \"universe\"},\n\t\t// either author can get the other info\n\t\t{author2, author1, []string{\"get\", \"hello\"}, \"world\"},\n\t\t{author1, author2, []string{\"get\", \"hello\"}, \"universe\"},\n\t\t// anyone can view the databases\n\t\t{author1, author2, []string{}, `- [g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6](/r/demo/keystore:g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6) (2 keys)\n- [g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00](/r/demo/keystore:g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00) (1 keys)`},\n\t\t// anyone can view the keys in a database\n\t\t{author1, author2, []string{\"\"}, `# g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00 database\n\n- 0 [hello](/r/demo/keystore:g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00:get:hello)`},\n\t}\n\tfor _, tc := range tt {\n\t\tp := \"\"\n\t\tif len(tc.ps) \u003e 0 {\n\t\t\tp = tc.owner.String()\n\t\t\tfor _, psv := range tc.ps {\n\t\t\t\tp += \":\" + psv\n\t\t\t}\n\t\t}\n\t\tp = strings.TrimSuffix(p, \":\")\n\t\tt.Run(p, func(t *testing.T) {\n\t\t\ttesting.SetOriginCaller(tc.caller)\n\t\t\tvar act string\n\t\t\tif len(tc.ps) \u003e 0 \u0026\u0026 tc.ps[0] == \"set\" {\n\t\t\t\tact = strings.TrimSpace(Set(cross(cur), tc.ps[1], tc.ps[2]))\n\t\t\t} else if len(tc.ps) \u003e 0 \u0026\u0026 tc.ps[0] == \"remove\" {\n\t\t\t\tact = strings.TrimSpace(Remove(cross(cur), tc.ps[1]))\n\t\t\t} else {\n\t\t\t\tact = strings.TrimSpace(Render(p))\n\t\t\t}\n\n\t\t\tuassert.Equal(t, tc.exp, act, ufmt.Sprintf(\"%v -\u003e '%s'\", tc.ps, p))\n\t\t})\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"V9LnCbHe0UjHEnPge9IawQ/Buc/Psbh5WOlIHgWciXlkQGTuurw7ObV3ieLyzKNQyanxKtZzuQyCUKoEuHW2SQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"evaluation","path":"gno.land/r/archive/nir1218_evaluation_proposal/evaluation","files":[{"name":"EVALUATION.md","body":"# `Contribution Evaluation`\n\n## Abstract\n\nThis document describes general ideas regarding contributions evaluation. The principles laid out are intended to be part of the Evaluation DAO.\n\n## Contents\n\n- [Concepts](#concepts)\n\n  - [Committee](#committee)\n  - [Evaluation](#evaluation)\n  - [Contribution](#contribution)\n  - [Pull Request](#pull-request)\n  - [Vote](#vote)\n\n- [Future Improvements](#future-improvements)\n\n- [Implementation](#implementation)\n\n## Concepts\n\n### General Ideas\n\nContributors DAO will designate members of a committee. In the beginning, the evaluation committee members will be the core development team members or any other trusted entity.\nA committee will be given the mandate to evaluate a certain set of contributions.\nFor example, the first committee will evaluate code contributions inside Gno central repository.\nA contribution will be associated with a pull request managed in Git.\nA Committee as a trusted entity can decide on a category and its corresponding evaluation criteria.\nA member can propose to add a category and its corresponding evaluation criteria.\nA member can propose a contribution for evaluation. However, the pull request category must be from the list of approved categories.\nAt the time of writing, a member can vote based on as set of options either \"YES\" or \"NO\", all members need to approve a category or a contribution.\n\n### Committee\n\nA group of designated members who are given a mandate to act as an evaluation authority.\nA DAO may elect a committee and designate its members based on contributions or merits of the members.\nA committee member can propose a contribution to avoid spam and confirm viable contributions will be evaluated.\n\n### Evaluation\n\nA logical entity to group a certain types of contributions.\n\n#### Category\n\nA group of contributions that should be evaluated based on the same principles and guide lines.\nAn example of a category is a bounty, a chore, a defect, or a document.\n\n### Contribution\n\nA contribution is associated with a pull request.\nA contribution has an evaluation life cycle.\nA submission time is set when a contribution is added.\nA last evaluation time is set when a contribution is evaluated and approved by a member.\nAn approval time is set when a contribution is approved by all members (or when a future threshold is reached)\n\n#### Submission\n\nAny committee member can submit a contribution.\n\n#### Status\n\nWhen a contribution is submitted its status is set to \"proposed\", its status will change to \"approved\" once approved by the committee or to \"declined\" otherwise.\nIntermediate status options such as \"negotiation\", \"discussion\", \"evaluation\" are TBD.\nA further discussion around the idea of deleting a contribution is required as it raises questions regarding record keeping, double evaluations, and the motive.\n\n#### Approval\n\nA contribution is approved once it reaches a certain threshold.\n\n### Pull Request\n\nA pull request from a source control tool, namely GitHub.\n\n### Vote\n\n#### Voters\n\nVoters are committee members, all committee members have the right and obligation to vote on a contribution.\n\n#### Voting Options\n\nThe voting options available to a voter.\nA committee may set voting options for its evaluation categories.\nThe initial option set includes the following options:\n\n- `YES`\n- `NO`\n\n#### Voting Period\n\nVoting period is set by the committee, all committee members are obligated to vote within the voting period.\n\n#### Threshold\n\nThreshold is the minimum percentage of `YES` votes from the total votes.\n\n#### Tally Votes\n\n## Future Improvements\n\nThe current documentation describes the basic ideas as expressed in the code.\nFuture improvements listed below will be decided based on future discussions and peer reviews.\n\n- Committee negotiates contributions\nFIXME Next line is unfinished:\n- A committee may set voting options for its categories and evaluated contributions, otherwise; the Contributors DAO may set a global\n- A committee may set a threshold required for a category or a contribution to be approved, otherwise; the Contributors DAO may set a global threshold and quorum.\n- A committee sets evaluation criteria scoring range (1-10), scoring a contribution is essential when there are competing contributions (Game of Realm). Otherwise, the evaluation is a binary decision. Moreover, scoring should be translated to rewards of any sort, or become discussion points durning negotiation about the viability of a contribution.\n- Committee members assess contributions based on the evaluation criteria and vote accordingly.\n\n## Implementation\n\nThe implementation written is to express the ideas described above using code. Not all ideas have been implemented.\n"},{"name":"category.gno","body":"package evaluation\n\nimport (\n\t\"gno.land/p/nt/avl/v0\"\n)\n\ntype Category struct {\n\tname        string\n\tcriteria    []string\n\tstatus      string\n\tvotes       avl.Tree\n\ttallyResult TallyResult\n}\n\nfunc NewCategory(name string, criteria []string) *Category {\n\ttallyResult := TallyResult{}\n\ttallyResult.results.Set(VoteYes, 0)\n\ttallyResult.results.Set(VoteNo, 0)\n\n\tc := \u0026Category{\n\t\tname:        name,\n\t\tcriteria:    criteria,\n\t\tstatus:      \"Proposed\",\n\t\tvotes:       avl.Tree{},\n\t\ttallyResult: tallyResult,\n\t}\n\treturn c\n}\n\nfunc (c *Category) Approve() {\n\t// TODO error handling\n\tc.status = \"Approved\"\n}\n\nfunc (c Category) Status() string {\n\treturn c.status\n}\n\nfunc (c *Category) Tally() {\n\t// TODO error handling\n\tc.votes.Iterate(\"\", \"\", func(address_XXX string, vote any) bool {\n\t\tv := vote.(Vote)\n\t\tvalue := c.tallyResult.results.Get(v.option)\n\t\tif value == nil {\n\t\t\treturn false\n\t\t}\n\t\tcount := value.(int)\n\t\tc.tallyResult.results.Set(v.option, count+1)\n\t\treturn true\n\t})\n}\n"},{"name":"committee.gno","body":"package evaluation\n\nimport (\n\t\"chain/runtime/unsafe\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\ntype Committee struct {\n\tmembers    []address // TODO - use avl tree or address set?\n\tcategories avl.Tree  // A category is mapped to a list of evaluation criteria\n\tevaluation *Evaluation\n}\n\nconst ApprovedStatus = \"Approved\"\n\nfunc NewCommittee() *Committee {\n\tc := \u0026Committee{\n\t\tmembers:    []address{},\n\t\tcategories: avl.Tree{},\n\t\tevaluation: NewEvalutaion(),\n\t}\n\treturn c\n}\n\nfunc (c *Committee) DesignateMembers(members []address) []address {\n\tc.members = append(c.members, members...)\n\treturn c.members\n}\n\nfunc (c *Committee) DismissMembers(members []address) []address {\n\t// TODO\n\treturn []address{}\n}\n\nfunc (c *Committee) AddCategory(name string, criteria []string) bool {\n\t// TODO error handling\n\tif !c.isMember(unsafe.OriginCaller()) {\n\t\treturn false\n\t}\n\tcategory := NewCategory(name, criteria)\n\tc.categories.Set(name, category)\n\treturn true\n}\n\nfunc (c *Committee) ApproveCategory(name string, option string) bool {\n\tif !c.isMember(unsafe.OriginCaller()) {\n\t\treturn false\n\t}\n\n\tvalue := c.categories.Get(name)\n\tif value == nil {\n\t\treturn false\n\t}\n\tcategory := value.(*Category)\n\tif category.Status() == ApprovedStatus {\n\t\treturn false\n\t}\n\n\tvote := NewVote(unsafe.OriginCaller(), option)\n\tcategory.votes.Set(unsafe.OriginCaller().String(), vote)\n\tcategory.Tally()\n\n\t// TODO Add threshold factor for a category approval\n\t// TODO Add quorum factor for a category approval\n\t// Current assumption is all members voted YES so category is approved\n\n\tresult := category.tallyResult.results.Get(VoteYes)\n\tif result == nil {\n\t\treturn false\n\t}\n\n\tif result.(int) == len(c.members) {\n\t\tcategory.Approve()\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// TODO error handling\nfunc (c *Committee) AddContribution(pr *PullRequest, contributor address) (contributionId int, ok bool) {\n\tif !c.isMember(unsafe.OriginCaller()) {\n\t\treturn -1, false\n\t}\n\t// Check the category of the PR matches a category this committee evaluates\n\t// TODO check the category is an approved category\n\tif c.categories.Has(pr.category) {\n\t\treturn c.evaluation.AddContribution(pr, contributor)\n\t}\n\n\treturn -1, false\n}\n\n// TODO error handling\nfunc (c *Committee) ApproveContribution(id int, option string) bool {\n\tif !c.isMember(unsafe.OriginCaller()) {\n\t\treturn false\n\t}\n\n\tvalue := c.evaluation.contributions.Get(ufmt.Sprintf(\"%d\", id))\n\tif value == nil {\n\t\treturn false\n\t}\n\tcontribution := value.(*Contribution)\n\t// Already approved\n\tif contribution.status == ApprovedStatus {\n\t\treturn false\n\t}\n\n\tvote := NewVote(unsafe.OriginCaller(), option)\n\tcontribution.votes = append(contribution.votes, vote)\n\tcontribution.Tally()\n\n\t// TODO Add threshold factor for a contribution approval\n\t// TODO Add quorum factor for a contribution approval\n\t// Current assumption is all members voted YES so contribution is approved\n\n\tresult := contribution.tallyResult.results.Get(VoteYes)\n\tif result == nil {\n\t\treturn false\n\t}\n\n\tif result.(int) == len(c.members) {\n\t\tcontribution.Approve()\n\t\treturn true\n\t}\n\n\treturn false\n}\n\nfunc (c *Committee) isMember(m address) bool {\n\tfor _, member := range c.members {\n\t\tif m == member {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"},{"name":"committee_test.gno","body":"package evaluation\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n)\n\nfunc TestCommitteeMembers(t *testing.T) {\n\tmembers := []address{testutils.TestAddress(\"member1\"), testutils.TestAddress(\"member2\"), testutils.TestAddress(\"member3\")}\n\tc := NewCommittee()\n\n\tt.Run(\"Designate Committee Members\", func(t *testing.T) {\n\t\tc.DesignateMembers(members)\n\t\tif !isEqualAddressSlice(c.members, members) {\n\t\t\tt.Errorf(\"Designated Committee members got %v expcted %v\", members, c.members)\n\t\t}\n\t})\n\n\tt.Run(\"Dismiss Committee Members\", func(t *testing.T) {\n\t\tc.DismissMembers(members)\n\t})\n}\n\nfunc TestCategoryEvaluationCriteria(t *testing.T) {\n\tmember := testutils.TestAddress(\"member\")\n\tcategory := \"document\"\n\tcriteria := []string{\"clarity\", \"usage\"}\n\tcategory2 := \"bounty\"\n\tcriteria2 := []string{\"complexity\"}\n\texpectedGategory := NewCategory(category, criteria)\n\texpectedGategory2 := NewCategory(category2, criteria2)\n\n\tc := NewCommittee()\n\tc.DesignateMembers([]address{member})\n\n\tt.Run(\"Add First Committee Category and Evaluation Criteria\", func(t *testing.T) {\n\t\ttesting.SetOriginCaller(member)\n\t\tc.AddCategory(category, criteria)\n\t\tvalue := c.categories.Get(category)\n\t\tif value == nil {\n\t\t\tt.Errorf(\"Add first category %s failed\", category)\n\t\t}\n\t\tgotCategory := value.(*Category)\n\t\tif gotCategory.name != expectedGategory.name {\n\t\t\tt.Errorf(\"First Committee category got %s expected %s\", gotCategory.name, expectedGategory.name)\n\t\t}\n\t})\n\n\tt.Run(\"Add Second Committee Category and Evaluation Criteria\", func(t *testing.T) {\n\t\ttesting.SetOriginCaller(member)\n\t\tc.AddCategory(category2, criteria2)\n\t\tvalue2 := c.categories.Get(category2)\n\t\tif value2 == nil {\n\t\t\tt.Errorf(\"Add second category %s failed\", category2)\n\t\t}\n\t\tgotCategory2 := value2.(*Category)\n\t\tif gotCategory2.name != expectedGategory2.name {\n\t\t\tt.Errorf(\"Second Committee category got %s expected %s\", gotCategory2.name, expectedGategory2.name)\n\t\t}\n\t})\n\n\tt.Run(\"Approve First Committee Category\", func(t *testing.T) {\n\t\ttesting.SetOriginCaller(member)\n\t\tapproved := c.ApproveCategory(category, VoteYes)\n\t\tif !approved {\n\t\t\tvalue := c.categories.Get(category)\n\t\t\tgotCategory := value.(*Category)\n\t\t\tt.Errorf(\"Approved First Committee category got %s expected %s\", gotCategory.status, \"Approved\")\n\t\t}\n\t})\n}\n\nfunc isEqualStringSlice(a, b []string) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i := range a {\n\t\tif a[i] != b[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc isEqualAddressSlice(a, b []address) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i := range a {\n\t\tif a[i] != b[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n"},{"name":"contribution.gno","body":"package evaluation\n\nimport (\n\t\"time\"\n)\n\nvar contributionStatus = map[string]string{}\n\ntype Contribution struct {\n\tid               int\n\tcontributor      address\n\tstatus           string // approved, proposed, negotiation, discussion, evaluation, etc.\n\tvotes            []Vote\n\ttallyResult      TallyResult\n\tsubmitTime       time.Time\n\tlastEvaluateTime time.Time\n\tapproveTime      time.Time\n}\n\nfunc init() {\n\tcontributionStatus = make(map[string]string)\n\tcontributionStatus[\"Proposed\"] = \"Proposed\"\n\tcontributionStatus[\"Approved\"] = \"Approved\"\n\tcontributionStatus[\"Evaluated\"] = \"Evaluated\"\n\tcontributionStatus[\"Negotiated\"] = \"Negotiated\"\n}\n\nfunc NewContribution(id int, contributor address) *Contribution {\n\tc := \u0026Contribution{\n\t\tid:          id,\n\t\tcontributor: contributor,\n\t\tstatus:      contributionStatus[\"Proposed\"],\n\t\tvotes:       []Vote{},\n\t\ttallyResult: TallyResult{},\n\t}\n\treturn c\n}\n\nfunc (c Contribution) Id() int {\n\treturn c.id\n}\n\nfunc (c Contribution) Status() string {\n\treturn c.status\n}\n\nfunc (c *Contribution) UpdateStatus(status string) bool {\n\tif c.status == contributionStatus[\"Approved\"] {\n\t\treturn false\n\t}\n\tc.status = status\n\treturn true\n}\n\nfunc (c *Contribution) Approve() {\n\t// TODO error handling\n\tc.status = \"Approved\"\n}\n\nfunc (c *Contribution) Tally() {\n\t// TODO error handling\n\tfor _, v := range c.votes {\n\t\tif c.tallyResult.results.Has(v.option) {\n\t\t\tvalue := c.tallyResult.results.Get(v.option)\n\t\t\tcount := value.(int)\n\t\t\tc.tallyResult.results.Set(v.option, count+1)\n\t\t}\n\t}\n}\n"},{"name":"contribution_test.gno","body":"package evaluation\n\nimport \"testing\"\n\nfunc TestContributionUpdateStatus(t *testing.T) {\n\tc := NewContribution(1, \"contributor\")\n\n\tt.Run(\"Status Update Negotiated\", func(t *testing.T) {\n\t\tok := c.UpdateStatus(\"Negotiated\")\n\t\tif !ok {\n\t\t\tt.Error(\"Expected Successful Status Update but failed\")\n\t\t}\n\t})\n\n\tt.Run(\"Status Update Evaluated\", func(t *testing.T) {\n\t\tok := c.UpdateStatus(\"Evaluated\")\n\t\tif !ok {\n\t\t\tt.Error(\"Expected Successful Status Update but failed\")\n\t\t}\n\t})\n\n\tt.Run(\"Status Update Approved\", func(t *testing.T) {\n\t\tok := c.UpdateStatus(\"Approved\")\n\t\tif !ok {\n\t\t\tt.Error(\"Expected Successful Status Update but failed\")\n\t\t}\n\t})\n\n\tt.Run(\"Status Update Approved Invalid\", func(t *testing.T) {\n\t\tok := c.UpdateStatus(\"Approved\")\n\t\tif ok {\n\t\t\tt.Error(\"Expected Failed Status Update but succeded\")\n\t\t}\n\t})\n}\n"},{"name":"evaluation.gno","body":"package evaluation\n\nimport (\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\ntype Evaluation struct {\n\tcontributions avl.Tree\n\tpullrequests  avl.Tree\n}\n\ntype Evaluator interface {\n\tEvaluate() Points\n}\n\nfunc NewEvalutaion() *Evaluation {\n\te := \u0026Evaluation{\n\t\tcontributions: avl.Tree{},\n\t\tpullrequests:  avl.Tree{},\n\t}\n\treturn e\n}\n\nfunc (e *Evaluation) AddContribution(pr *PullRequest, contributor address) (int, bool) {\n\tid := pr.Id()\n\te.pullrequests.Set(ufmt.Sprintf(\"%d\", id), pr)\n\tc := NewContribution(id, contributor)\n\te.contributions.Set(ufmt.Sprintf(\"%d\", id), c)\n\treturn id, true\n}\n\nfunc (e *Evaluation) UpdateContribution(id int, status string) bool {\n\tc := e.contributions.Get(ufmt.Sprintf(\"%d\", id))\n\tif c != nil {\n\t\tcontribtution := c.(*Contribution)\n\t\treturn contribtution.UpdateStatus(status)\n\t}\n\treturn false\n}\n"},{"name":"evaluation_test.gno","body":"package evaluation\n\n/*\n\t1. At what stage of the PR a contribution should be evaluated?\n\t\tShould the PR be approved first?\n\t2. Can a contribution be re-evaluated before approved (current assumption is once a contribution is approved its state is final)?\n\t3. Can an evaluation criteria change up until it is approved (current assumption is that the evaluation criteria is set when the contribution is added)?\n*/\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nvar (\n\te = NewEvalutaion()\n\n\tid          = 792\n\tname        = \"Evaluation DAO Kick Off\"\n\tdescription = \"The PR is to initiate a discussion regarding the evaluation DAO\"\n\tstatus      = \"Draft\"\n\tcategory    = \"feat\"\n\tcriteria    = map[string]int32{\"simplicity\": 1, \"usefullnes\": 1, \"quality\": 1}\n\taddress_XXX = testutils.TestAddress(\"contributor\")\n)\n\nfunc TestEvaluationAddContribution(t *testing.T) {\n\tpr := NewPullRequest(id, name, description, status, category)\n\tcontributionId, _ := e.AddContribution(pr, address_XXX)\n\n\tt.Run(\"\", func(t *testing.T) {\n\t\tif contributionId != id {\n\t\t\tt.Errorf(\"Got Contribution Id %d expected %d\", contributionId, id)\n\t\t}\n\t})\n\n\tt.Run(\"Contribution added using the pull request id\", func(t *testing.T) {\n\t\tc := e.contributions.Get(ufmt.Sprintf(\"%d\", id))\n\t\tcontribtution := c.(*Contribution)\n\t\tif contribtution.Id() != id {\n\t\t\tt.Errorf(\"Got Contribution Id %d expected %d\", contribtution.Id(), id)\n\t\t}\n\t})\n\n\tt.Run(\"Pull Request added using the pull request id\", func(t *testing.T) {\n\t\tpr := e.pullrequests.Get(ufmt.Sprintf(\"%d\", id))\n\t\tpullrequest := pr.(*PullRequest)\n\t\tif pullrequest.Id() != id {\n\t\t\tt.Errorf(\"Got Pull Request Id %d expected %d\", pullrequest.Id(), id)\n\t\t}\n\t})\n}\n\nfunc TestEvaluationUpdateContribution(t *testing.T) {\n\tt.Run(\"\", func(t *testing.T) {\n\t\tstatus := \"Negotiated\"\n\t\tok := e.UpdateContribution(id, status)\n\t\tif !ok {\n\t\t\tt.Error(\"Expected evaluation to update contribution's status successfully but failed\")\n\t\t}\n\t})\n\n\tt.Run(\"Contribution doesn't exist\", func(t *testing.T) {\n\t\tid := 1\n\t\tstatus := \"Negotiated\"\n\t\tok := e.UpdateContribution(id, status)\n\t\tif ok {\n\t\t\tt.Error(\"Expected evaluation to fail but pass\")\n\t\t}\n\t})\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/archive/nir1218_evaluation_proposal/evaluation\"\ngno = \"0.9\"\n"},{"name":"points.gno","body":"package evaluation\n\n// Points could be converted to rewards\ntype Points struct {\n\ttotal   int64\n\tfactors map[string]int32\n}\n\nfunc NewPoints(t int64, f map[string]int32) Points {\n\treturn Points{\n\t\ttotal:   t,\n\t\tfactors: f,\n\t}\n}\n"},{"name":"pull_request.gno","body":"package evaluation\n\nvar pullRequestStatus map[string]struct{}\n\ntype PullRequest struct {\n\tid          int\n\tname        string\n\tdescription string\n\tstatus      string // Draft, Review required, Changes requested, Approved\n\tcategory    string // bounty, chore, defect, document etc.\n}\n\nfunc init() {\n\tpullRequestStatus = make(map[string]struct{})\n\tpullRequestStatus[\"Draft\"] = struct{}{}\n\tpullRequestStatus[\"Approved\"] = struct{}{}\n\tpullRequestStatus[\"Changes requested\"] = struct{}{}\n\tpullRequestStatus[\"Review required\"] = struct{}{}\n}\n\nfunc NewPullRequest(id int, name string, description string, status string, category string) *PullRequest {\n\tpr := \u0026PullRequest{\n\t\tid:          id,\n\t\tname:        name,\n\t\tdescription: description,\n\t\tstatus:      status,\n\t\tcategory:    category,\n\t}\n\treturn pr\n}\n\nfunc (pr PullRequest) Id() int {\n\treturn pr.id\n}\n\nfunc (pr PullRequest) Status() string {\n\treturn pr.status\n}\n\nfunc (pr *PullRequest) UpdateName(name string) {\n\tpr.name = name\n}\n\nfunc (pr *PullRequest) UpdateDescription(description string) {\n\tpr.description = description\n}\n\nfunc (pr *PullRequest) UpdateStatus(status string) bool {\n\tif validateStatus(status) {\n\t\tpr.status = status\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc validateStatus(status string) bool {\n\t_, ok := pullRequestStatus[status]\n\treturn ok\n}\n"},{"name":"pull_request_test.gno","body":"package evaluation\n\nimport \"testing\"\n\nfunc TestPullRequestUpdateStatus(t *testing.T) {\n\tvar (\n\t\tid          = 792\n\t\tname        = \"Evaluation DAO Kick Off\"\n\t\tdescription = \"The PR is to initiate a discussion regarding the evaluation DAO\"\n\t\tstatus      = \"Draft\"\n\t\tcategory    = \"feat\"\n\t)\n\n\tvalidPR := NewPullRequest(id, name, description, status, category)\n\n\tt.Run(\"Valid Status Approved\", func(t *testing.T) {\n\t\tstatus := \"Approved\"\n\t\tif !validPR.UpdateStatus(status) {\n\t\t\tt.Error(\"expected validation to pass\")\n\t\t}\n\t})\n\n\tt.Run(\"Valid Status Draft\", func(t *testing.T) {\n\t\tstatus := \"Draft\"\n\t\tif !validPR.UpdateStatus(status) {\n\t\t\tt.Error(\"expected validation to pass\")\n\t\t}\n\t})\n\n\tt.Run(\"Valid Status Changes requested\", func(t *testing.T) {\n\t\tstatus := \"Changes requested\"\n\t\tif !validPR.UpdateStatus(status) {\n\t\t\tt.Error(\"expected validation to pass\")\n\t\t}\n\t})\n\n\tt.Run(\"Valid Status Review required\", func(t *testing.T) {\n\t\tstatus := \"Review required\"\n\t\tif !validPR.UpdateStatus(status) {\n\t\t\tt.Error(\"expected validation to pass\")\n\t\t}\n\t})\n\n\tt.Run(\"Invalid Status\", func(t *testing.T) {\n\t\tstatus := \"Junk\"\n\t\tif validPR.UpdateStatus(status) {\n\t\t\tt.Error(\"expected validation to fail\")\n\t\t}\n\t})\n}\n"},{"name":"tally.gno","body":"package evaluation\n\nimport \"gno.land/p/nt/avl/v0\"\n\ntype TallyResult struct {\n\tresults avl.Tree\n}\n\ntype Tally interface {\n\tTally()\n}\n"},{"name":"task.gno","body":"package evaluation\n\n// Maybe a task in the project management system\ntype Task struct {\n\tid          int\n\tname        string\n\tdescription string\n\tstatus      string\n}\n"},{"name":"vote.gno","body":"package evaluation\n\nconst (\n\tVoteYes = \"YES\"\n\tVoteNo  = \"NO\"\n)\n\ntype Vote struct {\n\tvoter  address\n\toption string\n}\n\nfunc NewVote(voter address, option string) Vote {\n\tv := Vote{\n\t\tvoter:  voter,\n\t\toption: option,\n\t}\n\treturn v\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"qBUPX1b+V1ISFc6cfiFH1UheCnyvCSt/y1SmJjofxcYfDbljmuq75AQ0gk3p2csPn+37M+fRK458COu3ZjZEGw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"ui","path":"gno.land/r/archive/ui","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/archive/ui\"\ngno = \"0.9\"\n"},{"name":"ui.gno","body":"package ui\n\nimport \"gno.land/p/archive/ui\"\n\nfunc Render(path string) string {\n\t// TODO: build this realm as a demo one with one page per feature.\n\n\t// TODO: pagination\n\t// TODO: non-standard markdown\n\t// TODO: error, warn\n\t// TODO: header\n\t// TODO: HTML\n\t// TODO: toc\n\t// TODO: forms\n\t// TODO: comments\n\n\tdom := ui.DOM{\n\t\tPrefix: \"r/demo/ui:\",\n\t}\n\n\tdom.Title = \"UI Demo\"\n\n\tdom.Header.Append(ui.Breadcrumb{\n\t\tui.Link{Text: \"foo\", Path: \"foo\"},\n\t\tui.Link{Text: \"bar\", Path: \"foo/bar\"},\n\t})\n\n\tdom.Body.Append(\n\t\tui.Paragraph(\"Simple UI demonstration.\"),\n\t\tui.BulletList{\n\t\t\tui.Text(\"a text\"),\n\t\t\tui.Link{Text: \"a relative link\", Path: \"foobar\"},\n\t\t\tui.Text(\"another text\"),\n\t\t\t// ui.H1(\"a H1 text\"),\n\t\t\tui.Bold(\"a bold text\"),\n\t\t\tui.Italic(\"italic text\"),\n\t\t\tui.Text(\"raw markdown with **bold** text in the middle.\"),\n\t\t\tui.Code(\"some inline code\"),\n\t\t\tui.Link{Text: \"a remote link\", URL: \"https://gno.land\"},\n\t\t},\n\t)\n\n\tdom.Footer.Append(ui.Text(\"I'm the footer.\"))\n\tdom.Body.Append(ui.Text(\"another string.\"))\n\tdom.Body.Append(ui.Paragraph(\"a paragraph.\"), ui.HR{})\n\n\treturn dom.String()\n}\n"},{"name":"ui_test.gno","body":"package ui\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nfunc TestRender(t *testing.T) {\n\tgot := Render(\"\")\n\texpected := \"# UI Demo\\n\\n[foo](r/demo/ui:foo) / [bar](r/demo/ui:foo/bar)\\n\\n\\nSimple UI demonstration.\\n\\n- a text\\n- [a relative link](r/demo/ui:foobar)\\n- another text\\n- **a bold text**\\n- _italic text_\\n- raw markdown with **bold** text in the middle.\\n- `some inline code`\\n- [a remote link](https://gno.land)\\n\\nanother string.\\n\\na paragraph.\\n\\n\\n---\\n\\n\\nI'm the footer.\\n\\n\"\n\tuassert.Equal(t, expected, got)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"B8Tt+YjwpGuq0VA158reaLQZ5Xt/BWIMHiaBknEL6Ul+BWVdlDyHUXLgTnhTyBzGEZEew5Fc1MzN2oS/hvM4Zw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"userbook","path":"gno.land/r/archive/userbook","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/archive/userbook\"\ngno = \"0.9\"\n"},{"name":"render.gno","body":"// Package userbook demonstrates a small userbook system working with gnoweb\npackage userbook\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/sys/users\"\n\n\t\"gno.land/p/moul/txlink/v0\"\n\t\"gno.land/p/nt/avl/pager/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nconst usersLink = \"/r/sys/users\"\n\nfunc Render(path string) string {\n\tp := pager.NewPager(signupsTree, 20, true)\n\tpage := p.MustGetPageByPath(path)\n\n\tout := \"# Welcome to UserBook!\\n\\n\"\n\n\tout += ufmt.Sprintf(\"## [Click here to sign up!](%s)\\n\\n\", txlink.Call(\"SignUp\"))\n\tout += \"---\\n\\n\"\n\n\tfor _, item := range page.Items {\n\t\tsignup := item.Value.(*Signup)\n\t\tuser := signup.address_XXX.String()\n\n\t\tif data := users.ResolveAddress(signup.address_XXX); data != nil {\n\t\t\tuser = ufmt.Sprintf(\"[%s](%s:%s)\", data.Name(), usersLink, data.Name())\n\t\t}\n\n\t\tout += ufmt.Sprintf(\"- **User #%d - %s - signed up on %s**\\n\\n\", signup.ordinal, user, signup.timestamp.Format(\"January 2 2006, 03:04:04 PM\"))\n\t}\n\n\tout += \"---\\n\\n\"\n\tout += \"**Page \" + strconv.Itoa(page.PageNumber) + \" of \" + strconv.Itoa(page.TotalPages) + \"**\\n\\n\"\n\tout += page.Picker(path)\n\treturn out\n}\n"},{"name":"userbook.gno","body":"// Package userbook demonstrates a small userbook system working with gnoweb\npackage userbook\n\nimport (\n\t\"chain\"\n\t\"time\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\ntype Signup struct {\n\taddress_XXX address\n\tordinal     int\n\ttimestamp   time.Time\n}\n\nvar (\n\tsignupsTree = avl.NewTree()\n\ttracker     = avl.NewTree()\n\tidCounter   seqid.ID\n)\n\nconst signUpEvent = \"SignUp\"\n\nfunc init(cur realm) {\n\tSignUp(cross(cur)) // Sign up the deployer\n}\n\nfunc SignUp(cur realm) string {\n\t// Get transaction caller\n\tcaller := cur.Previous().Address()\n\n\t// Check if the user is already signed up\n\tif tracker.Has(caller.String()) {\n\t\tpanic(caller.String() + \" is already signed up!\")\n\t}\n\n\tnow := time.Now()\n\n\t// Sign up the user\n\tsignupsTree.Set(idCounter.Next().String(), \u0026Signup{\n\t\tcaller,\n\t\tsignupsTree.Size(),\n\t\tnow,\n\t})\n\n\ttracker.Set(caller.String(), struct{}{})\n\n\tchain.Emit(signUpEvent, \"account\", caller.String())\n\n\treturn ufmt.Sprintf(\"%s added to userbook! Timestamp: %s\", caller.String(), now.Format(time.RFC822Z))\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"eSPgBwlyMzy5J3eCd4aBDFuQABRpZMSFiQ6DAONa45RWcPtx4gUJ2lJWPQBGXnSCNRJDkDTVv3vGR9S0+AHUyA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"gnoface","path":"gno.land/r/demo/art/gnoface","files":[{"name":"gnoface.gno","body":"package gnoface\n\nimport (\n\t\"math/rand\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/entropy\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nfunc Render(path string) string {\n\tseed := uint64(entropy.New().Value())\n\n\tpath = strings.TrimSpace(path)\n\tif path != \"\" {\n\t\ts, err := strconv.Atoi(path)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tseed = uint64(s)\n\t}\n\n\toutput := ufmt.Sprintf(\"Gnoface #%d\\n\", seed)\n\toutput += \"```\\n\" + Draw(seed) + \"```\\n\"\n\treturn output\n}\n\nfunc Draw(seed uint64) string {\n\tvar (\n\t\thairs = []string{\n\t\t\t\"     s\",\n\t\t\t\"  .......\",\n\t\t\t\"   s s s\",\n\t\t\t\"   /\\\\ /\\\\\",\n\t\t\t\"  |||||||\",\n\t\t}\n\t\theadtop = []string{\n\t\t\t\" /-------\\\\\",\n\t\t\t\" /~~~~~~~\\\\\",\n\t\t\t\" /|||||||\\\\\",\n\t\t\t\" ////////\\\\\",\n\t\t\t\" |||||||||\",\n\t\t\t\" /\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\",\n\t\t}\n\t\theadspace = []string{\n\t\t\t\" |       |\",\n\t\t}\n\t\teyebrow = []string{\n\t\t\t\"~\",\n\t\t\t\"*\",\n\t\t\t\"_\",\n\t\t\t\".\",\n\t\t}\n\t\tear = []string{\n\t\t\t\"o\",\n\t\t\t\" \",\n\t\t\t\"D\",\n\t\t\t\"O\",\n\t\t\t\"\u003c\",\n\t\t\t\"\u003e\",\n\t\t\t\".\",\n\t\t\t\"|\",\n\t\t\t\")\",\n\t\t\t\"(\",\n\t\t}\n\t\teyesmiddle = []string{\n\t\t\t\"| o   o |\",\n\t\t\t\"| o   _ |\",\n\t\t\t\"| _   o |\",\n\t\t\t\"| .   . |\",\n\t\t\t\"| O   O |\",\n\t\t\t\"| v   v |\",\n\t\t\t\"| X   X |\",\n\t\t\t\"| x   X |\",\n\t\t\t\"| X   D |\",\n\t\t\t\"| ~   ~ |\",\n\t\t}\n\t\tnose = []string{\n\t\t\t\" |   o   |\",\n\t\t\t\" |   O   |\",\n\t\t\t\" |   V   |\",\n\t\t\t\" |   L   |\",\n\t\t\t\" |   C   |\",\n\t\t\t\" |   ~   |\",\n\t\t\t\" |  . .  |\",\n\t\t\t\" |   .   |\",\n\t\t}\n\t\tmouth = []string{\n\t\t\t\" |  __/  |\",\n\t\t\t\" |  \\\\_/  |\",\n\t\t\t\" |   .   |\",\n\t\t\t\" |  ___  |\",\n\t\t\t\" |  ~~~  |\",\n\t\t\t\" |  ===  |\",\n\t\t\t\" |  \u003c=\u003e  |\",\n\t\t}\n\t\theadbottom = []string{\n\t\t\t\" \\\\-------/\",\n\t\t\t\" \\\\~~~~~~~/\",\n\t\t\t\" \\\\_______/\",\n\t\t}\n\t)\n\n\tr := rand.New(rand.NewPCG(seed, 0xdeadbeef))\n\n\treturn pick(r, hairs) + \"\\n\" +\n\t\tpick(r, headtop) + \"\\n\" +\n\t\tpick(r, headspace) + \"\\n\" +\n\t\t\" | \" + pick(r, eyebrow) + \"   \" + pick(r, eyebrow) + \" |\\n\" +\n\t\tpick(r, ear) + pick(r, eyesmiddle) + pick(r, ear) + \"\\n\" +\n\t\tpick(r, headspace) + \"\\n\" +\n\t\tpick(r, nose) + \"\\n\" +\n\t\tpick(r, headspace) + \"\\n\" +\n\t\tpick(r, mouth) + \"\\n\" +\n\t\tpick(r, headspace) + \"\\n\" +\n\t\tpick(r, headbottom) + \"\\n\"\n}\n\nfunc pick(r *rand.Rand, slice []string) string {\n\treturn slice[r.IntN(len(slice))]\n}\n\n// based on https://github.com/moul/pipotron/blob/master/dict/ascii-face.yml\n"},{"name":"gnoface_test.gno","body":"package gnoface\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nfunc TestDraw(t *testing.T) {\n\tcases := []struct {\n\t\tseed     uint64\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tseed: 42,\n\t\t\texpected: `\n  |||||||\n |||||||||\n |       |\n | .   ~ |\n)| v   v |O\n |       |\n |   L   |\n |       |\n |  ___  |\n |       |\n \\~~~~~~~/\n`[1:],\n\t\t},\n\t\t{\n\t\t\tseed: 1337,\n\t\t\texpected: `\n  .......\n |||||||||\n |       |\n | .   _ |\nD| x   X |O\n |       |\n |   ~   |\n |       |\n |  ~~~  |\n |       |\n \\~~~~~~~/\n`[1:],\n\t\t},\n\t\t{\n\t\t\tseed: 123456789,\n\t\t\texpected: `\n  .......\n ////////\\\n |       |\n | ~   * |\n|| x   X |o\n |       |\n |   V   |\n |       |\n |   .   |\n |       |\n \\-------/\n`[1:],\n\t\t},\n\t}\n\tfor _, tc := range cases {\n\t\tname := ufmt.Sprintf(\"%d\", tc.seed)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot := Draw(tc.seed)\n\t\t\tuassert.Equal(t, string(tc.expected), got)\n\t\t})\n\t}\n}\n\nfunc TestRender(t *testing.T) {\n\tcases := []struct {\n\t\tpath     string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tpath: \"42\",\n\t\t\texpected: \"Gnoface #42\\n```\" + `\n  |||||||\n |||||||||\n |       |\n | .   ~ |\n)| v   v |O\n |       |\n |   L   |\n |       |\n |  ___  |\n |       |\n \\~~~~~~~/\n` + \"```\\n\",\n\t\t},\n\t\t{\n\t\t\tpath: \"1337\",\n\t\t\texpected: \"Gnoface #1337\\n```\" + `\n  .......\n |||||||||\n |       |\n | .   _ |\nD| x   X |O\n |       |\n |   ~   |\n |       |\n |  ~~~  |\n |       |\n \\~~~~~~~/\n` + \"```\\n\",\n\t\t},\n\t\t{\n\t\t\tpath: \"123456789\",\n\t\t\texpected: \"Gnoface #123456789\\n```\" + `\n  .......\n ////////\\\n |       |\n | ~   * |\n|| x   X |o\n |       |\n |   V   |\n |       |\n |   .   |\n |       |\n \\-------/\n` + \"```\\n\",\n\t\t},\n\t}\n\tfor _, tc := range cases {\n\t\tt.Run(tc.path, func(t *testing.T) {\n\t\t\tgot := Render(tc.path)\n\t\t\tuassert.Equal(t, tc.expected, got)\n\t\t})\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/demo/art/gnoface\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"mKAF2Yhx/4VR13sWzeS0UsiRrUPX3q1hclxVWp3/uBIX9QZ7UtRNhhWIM5WJRx3LACwcfRqm835RUq5W8wqjHw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"millipede","path":"gno.land/r/demo/art/millipede","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/demo/art/millipede\"\ngno = \"0.9\"\n"},{"name":"millipede.gno","body":"package millipede\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nconst (\n\tminSize     = 1\n\tdefaultSize = 20\n\tmaxSize     = 100\n)\n\nfunc Draw(size int) string {\n\tif size \u003c minSize || size \u003e maxSize {\n\t\tpanic(\"invalid millipede size\")\n\t}\n\tpaddings := []string{\"  \", \" \", \"\", \" \", \"  \", \"   \", \"    \", \"    \", \"   \"}\n\tvar b strings.Builder\n\tb.WriteString(\"    ╚⊙ ⊙╝\\n\")\n\tfor i := 0; i \u003c size; i++ {\n\t\tb.WriteString(paddings[i%9] + \"╚═(███)═╝\\n\")\n\t}\n\treturn b.String()\n}\n\nfunc Render(path string) string {\n\tsize := defaultSize\n\n\tpath = strings.TrimSpace(path)\n\tif path != \"\" {\n\t\tvar err error\n\t\tsize, err = strconv.Atoi(path)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\toutput := \"```\\n\" + Draw(size) + \"```\\n\"\n\tif size \u003e minSize {\n\t\toutput += ufmt.Sprintf(\"[%d](/r/demo/art/millipede:%d)\u003c \", size-1, size-1)\n\t}\n\tif size \u003c maxSize {\n\t\toutput += ufmt.Sprintf(\" \u003e[%d](/r/demo/art/millipede:%d)\", size+1, size+1)\n\t}\n\treturn output\n}\n\n// based on https://github.com/getmillipede/millipede-go/blob/977f046c39c35a650eac0fd30245e96b22c7803c/main.go\n"},{"name":"millipede_test.gno","body":"package millipede\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nfunc TestRender(t *testing.T) {\n\tcases := []struct {\n\t\tpath     string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tpath: \"\",\n\t\t\texpected: \"```\" + `\n    ╚⊙ ⊙╝\n  ╚═(███)═╝\n ╚═(███)═╝\n╚═(███)═╝\n ╚═(███)═╝\n  ╚═(███)═╝\n   ╚═(███)═╝\n    ╚═(███)═╝\n    ╚═(███)═╝\n   ╚═(███)═╝\n  ╚═(███)═╝\n ╚═(███)═╝\n╚═(███)═╝\n ╚═(███)═╝\n  ╚═(███)═╝\n   ╚═(███)═╝\n    ╚═(███)═╝\n    ╚═(███)═╝\n   ╚═(███)═╝\n  ╚═(███)═╝\n ╚═(███)═╝\n` + \"```\\n[19](/r/demo/art/millipede:19)\u003c  \u003e[21](/r/demo/art/millipede:21)\",\n\t\t},\n\t\t{\n\t\t\tpath: \"4\",\n\t\t\texpected: \"```\" + `\n    ╚⊙ ⊙╝\n  ╚═(███)═╝\n ╚═(███)═╝\n╚═(███)═╝\n ╚═(███)═╝\n` + \"```\\n[3](/r/demo/art/millipede:3)\u003c  \u003e[5](/r/demo/art/millipede:5)\",\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.path, func(t *testing.T) {\n\t\t\tgot := Render(tc.path)\n\t\t\tuassert.Equal(t, tc.expected, got)\n\t\t})\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"/zyZHWwUDL4kBuekVKQJiBeDxSx5Iz3ufJSvcpv5oKoHJhqHJjZBgXJ0iEQpzD9jU+uW07+10YRWGyKRhwSvsw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"btree_dao","path":"gno.land/r/demo/btree_dao","files":[{"name":"btree_dao.gno","body":"package btree_dao\n\nimport (\n\t\"chain/runtime/unsafe\"\n\t\"errors\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/demo/btree\"\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/nt/grc721/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// RegistrationDetails holds the details of a user's registration in the BTree DAO.\n// It stores the user's address, registration time, their B-Tree if they planted one,\n// and their NFT ID.\ntype RegistrationDetails struct {\n\tAddress   address\n\tRegTime   time.Time\n\tUserBTree *btree.BTree\n\tNFTID     string\n}\n\n// Less implements the btree.Record interface for RegistrationDetails.\n// It compares two RegistrationDetails based on their registration time.\n// Returns true if the current registration time is before the other registration time.\nfunc (rd *RegistrationDetails) Less(than btree.Record) bool {\n\tother := than.(*RegistrationDetails)\n\treturn rd.RegTime.Before(other.RegTime)\n}\n\nvar (\n\tdao       *grc721.Token\n\tdaoLedger *grc721.PrivateLedger\n\ttokenID   = 0\n\tmembers   = btree.New()\n)\n\nfunc init(cur realm) {\n\tdao, daoLedger = grc721.NewToken(\"BTree DAO\", \"BTDAO\", 0, cur)\n}\n\n// PlantTree allows a user to plant their B-Tree in the DAO forest.\n// It mints an NFT to the user and registers their tree in the DAO.\n// Returns an error if the tree is already planted, empty, or if NFT minting fails.\nfunc PlantTree(_ realm, userBTree *btree.BTree) error {\n\treturn plantImpl(userBTree, \"\")\n}\n\n// PlantSeed allows a user to register as a seed in the DAO with a message.\n// It mints an NFT to the user and registers them as a seed member.\n// Returns an error if the message is empty or if NFT minting fails.\nfunc PlantSeed(_ realm, message string) error {\n\treturn plantImpl(nil, message)\n}\n\n// plantImpl is the internal implementation that handles both tree planting and seed registration.\n// For tree planting (userBTree != nil), it verifies the tree isn't already planted and isn't empty.\n// For seed planting (userBTree == nil), it verifies the seed message isn't empty.\n// In both cases, it mints an NFT to the user and adds their registration details to the members tree.\n// Returns an error if any validation fails or if NFT minting fails.\nfunc plantImpl(userBTree *btree.BTree, seedMessage string) error {\n\t// Get the caller's address\n\tuserAddress := unsafe.OriginCaller()\n\n\tvar nftID string\n\tvar regDetails *RegistrationDetails\n\n\tif userBTree != nil {\n\t\t// Handle tree planting\n\t\tvar treeExists bool\n\t\tmembers.Ascend(func(record btree.Record) bool {\n\t\t\tregDetails := record.(*RegistrationDetails)\n\t\t\tif regDetails.UserBTree == userBTree {\n\t\t\t\ttreeExists = true\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn true\n\t\t})\n\t\tif treeExists {\n\t\t\treturn errors.New(\"tree is already planted in the forest\")\n\t\t}\n\n\t\tif userBTree.Len() == 0 {\n\t\t\treturn errors.New(\"cannot plant an empty tree\")\n\t\t}\n\n\t\tnftID = ufmt.Sprintf(\"%d\", tokenID)\n\t\tregDetails = \u0026RegistrationDetails{\n\t\t\tAddress:   userAddress,\n\t\t\tRegTime:   time.Now(),\n\t\t\tUserBTree: userBTree,\n\t\t\tNFTID:     nftID,\n\t\t}\n\t} else {\n\t\t// Handle seed planting\n\t\tif seedMessage == \"\" {\n\t\t\treturn errors.New(\"seed message cannot be empty\")\n\t\t}\n\t\tnftID = \"seed_\" + ufmt.Sprintf(\"%d\", tokenID)\n\t\tregDetails = \u0026RegistrationDetails{\n\t\t\tAddress:   userAddress,\n\t\t\tRegTime:   time.Now(),\n\t\t\tUserBTree: nil,\n\t\t\tNFTID:     nftID,\n\t\t}\n\t}\n\n\t// Mint an NFT to the user\n\terr := daoLedger.Mint(userAddress, grc721.TokenID(nftID))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tmembers.Insert(regDetails)\n\ttokenID++\n\treturn nil\n}\n\n// Render generates a Markdown representation of the DAO members.\n// It displays:\n// - Total number of NFTs minted\n// - Total number of members\n// - Size of the biggest planted tree\n// - The first 3 members (OGs)\n// - The latest 10 members\n// Each member entry includes their address and owned NFTs (🌳 for trees, 🌱 for seeds).\n// The path parameter is currently unused.\n// Returns a formatted Markdown string.\nfunc Render(path string) string {\n\tvar latestMembers []string\n\tvar ogMembers []string\n\n\t// Get total size and first member\n\ttotalSize := members.Len()\n\tbiggestTree := 0\n\tif maxMember := members.Max(); maxMember != nil {\n\t\tif userBTree := maxMember.(*RegistrationDetails).UserBTree; userBTree != nil {\n\t\t\tbiggestTree = userBTree.Len()\n\t\t}\n\t}\n\n\t// Collect the latest 10 members\n\tmembers.Descend(func(record btree.Record) bool {\n\t\tif len(latestMembers) \u003c 10 {\n\t\t\tregDetails := record.(*RegistrationDetails)\n\t\t\taddr := regDetails.Address\n\t\t\tnftList := \"\"\n\t\t\tbalance, err := dao.BalanceOf(addr)\n\t\t\tif err == nil \u0026\u0026 balance \u003e 0 {\n\t\t\t\tnftList = \" (NFTs: \"\n\t\t\t\tfor i := int64(0); i \u003c balance; i++ {\n\t\t\t\t\tif i \u003e 0 {\n\t\t\t\t\t\tnftList += \", \"\n\t\t\t\t\t}\n\t\t\t\t\tif regDetails.UserBTree == nil {\n\t\t\t\t\t\tnftList += \"🌱#\" + regDetails.NFTID\n\t\t\t\t\t} else {\n\t\t\t\t\t\tnftList += \"🌳#\" + regDetails.NFTID\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tnftList += \")\"\n\t\t\t}\n\t\t\tlatestMembers = append(latestMembers, string(addr)+nftList)\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t})\n\n\t// Collect the first 3 members (OGs)\n\tmembers.Ascend(func(record btree.Record) bool {\n\t\tif len(ogMembers) \u003c 3 {\n\t\t\tregDetails := record.(*RegistrationDetails)\n\t\t\taddr := regDetails.Address\n\t\t\tnftList := \"\"\n\t\t\tbalance, err := dao.BalanceOf(addr)\n\t\t\tif err == nil \u0026\u0026 balance \u003e 0 {\n\t\t\t\tnftList = \" (NFTs: \"\n\t\t\t\tfor i := int64(0); i \u003c balance; i++ {\n\t\t\t\t\tif i \u003e 0 {\n\t\t\t\t\t\tnftList += \", \"\n\t\t\t\t\t}\n\t\t\t\t\tif regDetails.UserBTree == nil {\n\t\t\t\t\t\tnftList += \"🌱#\" + regDetails.NFTID\n\t\t\t\t\t} else {\n\t\t\t\t\t\tnftList += \"🌳#\" + regDetails.NFTID\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tnftList += \")\"\n\t\t\t}\n\t\t\togMembers = append(ogMembers, string(addr)+nftList)\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t})\n\n\tvar sb strings.Builder\n\n\tsb.WriteString(md.H1(\"B-Tree DAO Members\"))\n\tsb.WriteString(md.H2(\"Total NFTs Minted\"))\n\tsb.WriteString(ufmt.Sprintf(\"Total NFTs minted: %d\\n\\n\", dao.TotalSupply()))\n\tsb.WriteString(md.H2(\"Member Stats\"))\n\tsb.WriteString(ufmt.Sprintf(\"Total members: %d\\n\", totalSize))\n\tif biggestTree \u003e 0 {\n\t\tsb.WriteString(ufmt.Sprintf(\"Biggest tree size: %d\\n\", biggestTree))\n\t}\n\tsb.WriteString(md.H2(\"OG Members\"))\n\tsb.WriteString(md.BulletList(ogMembers))\n\tsb.WriteString(md.H2(\"Latest Members\"))\n\tsb.WriteString(md.BulletList(latestMembers))\n\n\treturn sb.String()\n}\n"},{"name":"btree_dao_test.gno","body":"package btree_dao\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/demo/btree\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc setupTest() {\n\ttesting.SetOriginCaller(address(\"g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y\"))\n\tmembers = btree.New()\n}\n\ntype TestElement struct {\n\tvalue int\n}\n\nfunc (te *TestElement) Less(than btree.Record) bool {\n\treturn te.value \u003c than.(*TestElement).value\n}\n\nfunc TestPlantTree(cur realm, t *testing.T) {\n\tsetupTest()\n\n\ttree := btree.New()\n\telements := []int{30, 10, 50, 20, 40}\n\tfor _, val := range elements {\n\t\ttree.Insert(\u0026TestElement{value: val})\n\t}\n\n\terr := PlantTree(cross(cur), tree)\n\turequire.NoError(t, err)\n\n\tfound := false\n\tmembers.Ascend(func(record btree.Record) bool {\n\t\tregDetails := record.(*RegistrationDetails)\n\t\tif regDetails.UserBTree == tree {\n\t\t\tfound = true\n\t\t\treturn false\n\t\t}\n\t\treturn true\n\t})\n\tuassert.True(t, found)\n\n\terr = PlantTree(cross(cur), tree)\n\tuassert.Error(t, err)\n\n\temptyTree := btree.New()\n\terr = PlantTree(cross(cur), emptyTree)\n\tuassert.Error(t, err)\n}\n\nfunc TestPlantSeed(cur realm, t *testing.T) {\n\tsetupTest()\n\n\terr := PlantSeed(cross(cur), \"Hello DAO!\")\n\turequire.NoError(t, err)\n\n\tfound := false\n\tmembers.Ascend(func(record btree.Record) bool {\n\t\tregDetails := record.(*RegistrationDetails)\n\t\tif regDetails.UserBTree == nil {\n\t\t\tfound = true\n\t\t\tuassert.NotEmpty(t, regDetails.NFTID)\n\t\t\tuassert.True(t, strings.Contains(regDetails.NFTID, \"seed_\"))\n\t\t\treturn false\n\t\t}\n\t\treturn true\n\t})\n\tuassert.True(t, found)\n\n\terr = PlantSeed(cross(cur), \"\")\n\tuassert.Error(t, err)\n}\n\nfunc TestRegistrationDetailsOrdering(t *testing.T) {\n\tsetupTest()\n\n\trd1 := \u0026RegistrationDetails{\n\t\tAddress: address(\"test1\"),\n\t\tRegTime: time.Now(),\n\t\tNFTID:   \"0\",\n\t}\n\trd2 := \u0026RegistrationDetails{\n\t\tAddress: address(\"test2\"),\n\t\tRegTime: time.Now().Add(time.Hour),\n\t\tNFTID:   \"1\",\n\t}\n\n\tuassert.True(t, rd1.Less(rd2))\n\tuassert.False(t, rd2.Less(rd1))\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/demo/btree_dao\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"JR3CQb2a+9x8UE7oRenNqZ1b6+KrQqC0Kb6m1AbSTDt4DF+dbjP1VMNVk9zdRxo9ZU1jVWzN7JzSh60p6dkslQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"bar20","path":"gno.land/r/demo/defi/bar20","files":[{"name":"bar20.gno","body":"// Package bar20 is similar to gno.land/r/demo/foo20.\n//\n// It used to export its teller value as a \"safe object\" for `maketx run` and\n// for other contracts to call. That is no longer possible, and the reason is\n// worth knowing before copying the shape: a teller is a spend capability, and\n// the frame-relative one resolves the account to debit from whatever frame\n// invokes it. Exported, it debits whoever crossed into the realm that read the\n// var — chosen by that realm, not by its owner. grc20 now confines it to the\n// token's own realm, so an exported value is inert everywhere else, including\n// in a `maketx run` script, whose own pkgpath is not this realm's.\n//\n// The teller is therefore unexported here and reached only through the narrow\n// wrappers below, which is the pattern to copy.\npackage bar20\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/nt/grc20/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/nt/grc20reg/v0\"\n)\n\nvar (\n\tToken *grc20.Token\n\tadm   *grc20.PrivateLedger\n\t// userTeller stays unexported: it is a spend capability, and it only\n\t// functions inside this realm. The wrappers below are the public surface.\n\tuserTeller grc20.Teller\n)\n\nfunc init(cur realm) {\n\t// bar20 only ever creates this one token, so id 0 can't collide.\n\tToken, adm = grc20.NewToken(\"Bar\", \"BAR\", 4, 0, cur)\n\tuserTeller = adm.CallerTeller()\n\tgrc20reg.Register(cross(cur), Token, \"\")\n}\n\n// Transfer moves the caller's own tokens. userTeller debits cur.Previous(), so\n// a direct user call spends that user's balance and a realm cross-call spends\n// the calling realm's.\nfunc Transfer(cur realm, to address, amount int64) {\n\tcheckErr(userTeller.Transfer(0, cur, to, amount))\n}\n\n// Approve grants spender an allowance over the caller's balance.\nfunc Approve(cur realm, spender address, amount int64) {\n\tcheckErr(userTeller.Approve(0, cur, spender, amount))\n}\n\n// TransferFrom spends an allowance the owner granted to the caller.\nfunc TransferFrom(cur realm, owner, to address, amount int64) {\n\tcheckErr(userTeller.TransferFrom(0, cur, owner, to, amount))\n}\n\nfunc checkErr(err error) {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc Faucet(cur realm) string {\n\tcaller := cur.Previous().Address()\n\tif err := adm.Mint(caller, 1_000_000); err != nil {\n\t\treturn \"error: \" + err.Error()\n\t}\n\treturn \"OK\"\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\\n\", balance)\n\tdefault:\n\t\treturn \"404\\n\"\n\t}\n}\n"},{"name":"bar20_test.gno","body":"package bar20\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestPackage(cur realm, t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\ttesting.SetOriginCaller(alice)\n\n\turequire.Equal(t, Token.BalanceOf(alice), int64(0))\n\turequire.Equal(t, Faucet(cross(cur)), \"OK\")\n\turequire.Equal(t, Token.BalanceOf(alice), int64(1_000_000))\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/demo/defi/bar20\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"F5Wbqed3jps3iv8LmoduMvcsR8E04F4zAJMK77tZs0de4Wj0orFqvx8HdP1X6E86hRH9/+t6ll64buxAY0ZSXQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"foo1155","path":"gno.land/r/demo/foo1155","files":[{"name":"foo1155.gno","body":"package foo1155\n\nimport (\n\t\"chain/runtime/unsafe\"\n\n\t\"gno.land/p/nt/grc1155/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nvar (\n\tdummyURI         = \"ipfs://xyz\"\n\tadmin    address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\" // govdao t1 multisig\n\tfoo              = grc1155.NewBasicGRC1155Token(dummyURI)\n)\n\nfunc init() {\n\tmintGRC1155Token(admin) // @administrator (10)\n}\n\nfunc mintGRC1155Token(owner address) {\n\tfor i := 1; i \u003c= 10; i++ {\n\t\ttid := grc1155.TokenID(ufmt.Sprintf(\"%d\", i))\n\t\tfoo.SafeMint(owner, tid, 100)\n\t}\n}\n\n// Getters\n\nfunc BalanceOf(user address, tid grc1155.TokenID) int64 {\n\tbalance, err := foo.BalanceOf(user, tid)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn balance\n}\n\nfunc BalanceOfBatch(ul []address, batch []grc1155.TokenID) []int64 {\n\tbalanceBatch, err := foo.BalanceOfBatch(ul, batch)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn balanceBatch\n}\n\nfunc IsApprovedForAll(owner, user address) bool {\n\treturn foo.IsApprovedForAll(owner, user)\n}\n\n// Setters\n\nfunc SetApprovalForAll(_ realm, user address, approved bool) {\n\terr := foo.SetApprovalForAll(user, approved)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc TransferFrom(_ realm, from, to address, tid grc1155.TokenID, amount int64) {\n\terr := foo.SafeTransferFrom(from, to, tid, amount)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc BatchTransferFrom(_ realm, from, to address, batch []grc1155.TokenID, amounts []int64) {\n\terr := foo.SafeBatchTransferFrom(from, to, batch, amounts)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Admin\n\nfunc Mint(_ realm, to address, tid grc1155.TokenID, amount int64) {\n\tcaller := unsafe.OriginCaller()\n\tassertIsAdmin(caller)\n\terr := foo.SafeMint(to, tid, amount)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc MintBatch(_ realm, to address, batch []grc1155.TokenID, amounts []int64) {\n\tcaller := unsafe.OriginCaller()\n\tassertIsAdmin(caller)\n\terr := foo.SafeBatchMint(to, batch, amounts)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc Burn(_ realm, from address, tid grc1155.TokenID, amount int64) {\n\tcaller := unsafe.OriginCaller()\n\tassertIsAdmin(caller)\n\terr := foo.Burn(from, tid, amount)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc BurnBatch(_ realm, from address, batch []grc1155.TokenID, amounts []int64) {\n\tcaller := unsafe.OriginCaller()\n\tassertIsAdmin(caller)\n\terr := foo.BatchBurn(from, batch, amounts)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Render\n\nfunc Render(path string) string {\n\tswitch {\n\tcase path == \"\":\n\t\treturn foo.RenderHome()\n\tdefault:\n\t\treturn \"404\\n\"\n\t}\n}\n\n// Util\n\nfunc assertIsAdmin(address_XXX address) {\n\tif address_XXX != admin {\n\t\tpanic(\"restricted access\")\n\t}\n}\n"},{"name":"foo1155_test.gno","body":"package foo1155\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/grc1155/v0\"\n)\n\nfunc TestFoo721(t *testing.T) {\n\tadmin := address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\")\n\tbob := address(\"g1ze6et22ces5atv79y4xh38s4kuraey4y2fr6tw\")\n\ttid1 := grc1155.TokenID(\"1\")\n\n\tfor _, tc := range []struct {\n\t\tname     string\n\t\texpected any\n\t\tfn       func() any\n\t}{\n\t\t{\"BalanceOf(admin, tid1)\", int64(100), func() any { return BalanceOf(admin, tid1) }},\n\t\t{\"BalanceOf(bob, tid1)\", int64(0), func() any { return BalanceOf(bob, tid1) }},\n\t\t{\"IsApprovedForAll(admin, bob)\", false, func() any { return IsApprovedForAll(admin, bob) }},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot := tc.fn()\n\t\t\tif tc.expected != got {\n\t\t\t\tt.Errorf(\"expected: %v got: %v\", tc.expected, got)\n\t\t\t}\n\t\t})\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/demo/foo1155\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"hpOgK8NuSZ8T2MxzVi3wKPsKAyUGkO7autqf8VaHIRtU3ZqwV6fc2DxPAJ/tjy1r44jp65+jL+MAXRHNbfJ5Gg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"foo721","path":"gno.land/r/demo/foo721","files":[{"name":"foo721.gno","body":"// Realm foo721 is the reference GRC721 consumer.\npackage foo721\n\nimport (\n\t\"gno.land/p/nt/grc721/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nconst admin address = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\" // govdao t1 multisig\n\nvar (\n\ttoken      *grc721.Token\n\tledger     *grc721.PrivateLedger\n\tcallerTelr grc721.Teller\n)\n\nfunc init(cur realm) {\n\ttoken, ledger = grc721.NewToken(\"Foo NFT\", \"FNFT\", 0, cur)\n\t// Minted from the ledger: the frame-relative teller is confined to this\n\t// realm, which is where it is used.\n\tcallerTelr = ledger.CallerTeller()\n\n\tmintN(admin, 10)\n}\n\nfunc mintN(owner address, n int64) {\n\tcount := token.TotalSupply()\n\tfor i := count; i \u003c count+n; i++ {\n\t\ttid := grc721.TokenID(ufmt.Sprintf(\"%d\", i))\n\t\tif err := ledger.Mint(owner, tid); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n}\n\n// Getters\n\nfunc BalanceOf(user address) int64 {\n\tbalance, err := token.BalanceOf(user)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn balance\n}\n\nfunc OwnerOf(tid grc721.TokenID) address {\n\towner, err := token.OwnerOf(tid)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn owner\n}\n\nfunc IsApprovedForAll(owner, user address) bool { return token.IsApprovedForAll(owner, user) }\n\nfunc GetApproved(tid grc721.TokenID) address {\n\taddr, err := token.GetApproved(tid)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn addr\n}\n\n// Setters\n\n// The Approve / SetApprovalForAll / TransferFrom wrappers are the\n// owning realm's trusted boundary: they hand `cur` to the caller-scoped\n// Teller, which derives `caller` from `cur.Previous().Address()`\n// (unforgeable per-frame identity) and applies it to the unexported\n// `ledger`. The Teller rejects an rlm that is not the live crossing\n// frame, so a caller cannot act as an account it does not control.\n// See `p/nt/grc721/v0/tellers.gno` for the Teller doctrine.\nfunc Approve(cur realm, to address, tid grc721.TokenID) {\n\tif err := callerTelr.Approve(0, cur, to, tid); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc SetApprovalForAll(cur realm, operator address, approved bool) {\n\tif err := callerTelr.SetApprovalForAll(0, cur, operator, approved); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc TransferFrom(cur realm, from, to address, tid grc721.TokenID) {\n\tif err := callerTelr.TransferFrom(0, cur, from, to, tid); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Admin\n\nfunc Mint(cur realm, to address, tid grc721.TokenID) {\n\tassertIsAdmin(cur.Previous().Address())\n\tif err := ledger.Mint(to, tid); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc Burn(cur realm, tid grc721.TokenID) {\n\tassertIsAdmin(cur.Previous().Address())\n\tif err := ledger.Burn(tid); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Render\n\nfunc Render(path string) string {\n\tswitch path {\n\tcase \"\":\n\t\treturn token.RenderHome()\n\tdefault:\n\t\treturn \"404\\n\"\n\t}\n}\n\n// Util\n\nfunc assertIsAdmin(address_XXX address) {\n\tif address_XXX != admin {\n\t\tpanic(\"restricted access\")\n\t}\n}\n"},{"name":"foo721_test.gno","body":"package foo721\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/grc721/v0\"\n)\n\nfunc TestFoo721(t *testing.T) {\n\tadmin := address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\")\n\n\tfor _, tc := range []struct {\n\t\tname     string\n\t\texpected any\n\t\tfn       func() any\n\t}{\n\t\t{\"BalanceOf(admin)\", int64(10), func() any { return BalanceOf(admin) }},\n\t\t{\"OwnerOf(0)\", admin, func() any { return OwnerOf(grc721.TokenID(\"0\")) }},\n\t\t{\"IsApprovedForAll(admin, admin)\", false, func() any { return IsApprovedForAll(admin, admin) }},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot := tc.fn()\n\t\t\tif tc.expected != got {\n\t\t\t\tt.Errorf(\"expected: %v got: %v\", tc.expected, got)\n\t\t\t}\n\t\t})\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/demo/foo721\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"u6VqNfpgiter2WWGwHkKz/dsYRZcSa2yQ9glRwKFzeJnYHdX5oHZ0tzU3NpUt9i1EEg8gaSIfPC7D90pFBNNQg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"shifumi","path":"gno.land/r/demo/games/shifumi","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/demo/games/shifumi\"\ngno = \"0.9\"\n"},{"name":"shifumi.gno","body":"package shifumi\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n\n\t\"gno.land/r/sys/users\"\n)\n\nconst (\n\tempty = iota\n\trock\n\tpaper\n\tscissors\n\tlast\n)\n\ntype game struct {\n\tplayer1, player2 address // shifumi is a 2 players game\n\tmove1, move2     int     // can be empty, rock, paper, or scissors\n}\n\nvar games avl.Tree\nvar id seqid.ID\n\nfunc (g *game) play(player address, move int) error {\n\tif !(move \u003e empty \u0026\u0026 move \u003c last) {\n\t\treturn errors.New(\"invalid move\")\n\t}\n\tif player != g.player1 \u0026\u0026 player != g.player2 {\n\t\treturn errors.New(\"invalid player\")\n\t}\n\tif player == g.player1 \u0026\u0026 g.move1 == empty {\n\t\tg.move1 = move\n\t\treturn nil\n\t}\n\tif player == g.player2 \u0026\u0026 g.move2 == empty {\n\t\tg.move2 = move\n\t\treturn nil\n\t}\n\treturn errors.New(\"already played\")\n}\n\nfunc (g *game) winner() int {\n\tif g.move1 == empty || g.move2 == empty {\n\t\treturn -1\n\t}\n\tif g.move1 == g.move2 {\n\t\treturn 0\n\t}\n\tif g.move1 == rock \u0026\u0026 g.move2 == scissors ||\n\t\tg.move1 == paper \u0026\u0026 g.move2 == rock ||\n\t\tg.move1 == scissors \u0026\u0026 g.move2 == paper {\n\t\treturn 1\n\t}\n\treturn 2\n}\n\n// NewGame creates a new game where player1 is the caller and player2 the argument.\n// A new game index is returned.\nfunc NewGame(cur realm, player address) int {\n\tgames.Set(id.Next().String(), \u0026game{player1: cur.Previous().Address(), player2: player})\n\treturn int(id)\n}\n\n// Play executes a move for the game at index idx, where move can be:\n// 1 (rock), 2 (paper), 3 (scissors).\nfunc Play(cur realm, idx, move int) {\n\tv := games.Get(seqid.ID(idx).String())\n\tif v == nil {\n\t\tpanic(\"game not found\")\n\t}\n\tif err := v.(*game).play(cur.Previous().Address(), move); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc Render(path string) string {\n\tmov1 := []string{\"\", \" 🤜  \", \" 🫱  \", \" 👉  \"}\n\tmov2 := []string{\"\", \" 🤛  \", \" 🫲  \", \" 👈  \"}\n\twin := []string{\"pending\", \"draw\", \"player1\", \"player2\"}\n\n\toutput := `# 👊  ✋  ✌️  Shifumi\nActions:\n* [NewGame](shifumi$help\u0026func=NewGame) opponentAddress\n* [Play](shifumi$help\u0026func=Play) gameIndex move (1=rock, 2=paper, 3=scissors)\n\n game  | player1 |     | player2 |       | win \n --- | --- | --- | --- | --- | ---\n`\n\t// Output the 100 most recent games.\n\tmaxGames := 100\n\tfor n := int(id); n \u003e 0 \u0026\u0026 int(id)-n \u003c maxGames; n-- {\n\t\tv := games.Get(seqid.ID(n).String())\n\t\tif v == nil {\n\t\t\tcontinue\n\t\t}\n\t\tg := v.(*game)\n\t\toutput += strconv.Itoa(n) + \" | \" +\n\t\t\tshortName(g.player1) + \" | \" + mov1[g.move1] + \" | \" +\n\t\t\tshortName(g.player2) + \" | \" + mov2[g.move2] + \" | \" +\n\t\t\twin[g.winner()+1] + \"\\n\"\n\t}\n\treturn output\n}\n\nfunc shortName(addr address) string {\n\tuser := users.ResolveAddress(addr)\n\tif user != nil {\n\t\treturn user.Name()\n\t}\n\tif len(addr) \u003c 10 {\n\t\treturn string(addr)\n\t}\n\treturn string(addr)[:10] + \"...\"\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"tzP2UQPOwrgO3GNl2sDv/rvX+VjTx8iSAxMNBUb3Ek93DuWvQKgT2lYPEwP8EyFrOHw1PvEkdhpkjTzvb8UU6Q=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"math_eval","path":"gno.land/r/demo/math_eval","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/demo/math_eval\"\ngno = \"0.9\"\n"},{"name":"math_eval.gno","body":"// eval realm is capable of evaluating 32-bit integer\n// expressions as they would appear in Go. For example:\n// /r/demo/math_eval:(4+12)/2-1+11*15\npackage math_eval\n\nimport (\n\tevalint32 \"gno.land/p/demo/math_eval/int32\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nfunc Render(p string) string {\n\tif len(p) == 0 {\n\t\treturn `\nevaluates 32-bit integer expressions. for example:\n\t\t\n[(4+12)/2-1+11*15](/r/demo/math_eval:(4+12)/2-1+11*15)\n\n`\n\t}\n\texpr, err := evalint32.Parse(p)\n\tif err != nil {\n\t\treturn err.Error()\n\t}\n\tres, err := evalint32.Eval(expr, nil)\n\tif err != nil {\n\t\treturn err.Error()\n\t}\n\n\treturn ufmt.Sprintf(\"%s = %d\", p, res)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"81aKtzucDoE25oNZAm0h9P2yzg/INpzb5mz/W9wpTRcxYNHEjLqztOwdB5UA77Sxoo3vuW5f9mjpamI29V6ovA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"memeland","path":"gno.land/r/demo/memeland","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/demo/memeland\"\ngno = \"0.9\"\n"},{"name":"memeland.gno","body":"package memeland\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/demo/memeland\"\n)\n\nvar m *memeland.Memeland\n\nfunc init() {\n\tm = memeland.NewMemeland(\"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\")\n}\n\nfunc PostMeme(cur realm, data string, timestamp int64) string {\n\treturn m.PostMeme(0, cur, data, timestamp)\n}\n\nfunc Upvote(cur realm, id string) string {\n\treturn m.Upvote(0, cur, id)\n}\n\nfunc GetPostsInRange(startTimestamp, endTimestamp int64, page, pageSize int, sortBy string) string {\n\treturn m.GetPostsInRange(startTimestamp, endTimestamp, page, pageSize, sortBy)\n}\n\nfunc RemovePost(cur realm, id string) string {\n\treturn m.RemovePost(0, cur, id)\n}\n\nfunc GetOwner() address {\n\treturn m.Owner()\n}\n\nfunc TransferOwnership(cur realm, newOwner address) {\n\tif err := m.TransferOwnership(0, cur, newOwner); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc Render(path string) string {\n\tnumOfMemes := int(m.MemeCounter)\n\tif numOfMemes == 0 {\n\t\treturn \"No memes posted yet! :/\"\n\t}\n\n\t// Default render is get Posts since year 2000 to now\n\treturn m.GetPostsInRange(0, time.Now().Unix(), 1, 10, \"DATE_CREATED\")\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"VcrRxjed9J+pJk3kmeicSpmTl36dV4H6nrl+IIEdxjIs24o/ser/ZYxJAxb8eF4e94I7sZ4wNiX8kcoaCNm9aQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"microblog","path":"gno.land/r/demo/microblog","files":[{"name":"README.md","body":"# microblog realm\n\n## Getting started:\n\n(One-time) Add the microblog package:\n\n```\ngnokey maketx addpkg --pkgpath \"gno.land/p/demo/microblog\" --pkgdir \"examples/gno.land/p/demo/microblog\" \\\n    --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 2000000 --chainid dev --remote localhost:26657 \u003cYOURKEY\u003e\n```\n\n(One-time) Add the microblog realm:\n\n```\ngnokey maketx addpkg --pkgpath \"gno.land/r/demo/microblog\" --pkgdir \"examples/gno.land/r/demo/microblog\" \\\n    --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 2000000 --chainid dev --remote localhost:26657 \u003cYOURKEY\u003e\n```\n\nAdd a microblog post:\n\n```\ngnokey maketx call --pkgpath \"gno.land/r/demo/microblog\" --func \"NewPost\" --args \"hello, world\" \\\n    --gas-fee \"1000000ugnot\" --gas-wanted \"2000000\" --chainid dev --remote localhost:26657 \u003cYOURKEY\u003e\n```\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/demo/microblog\"\ngno = \"0.9\"\n"},{"name":"microblog.gno","body":"// Microblog is a website with shortform posts from users.\n// The API is simple - \"AddPost\" takes markdown and\n// adds it to the users site.\n// The microblog location is determined by the user address\n// /r/demo/microblog:\u003cYOUR-ADDRESS\u003e\npackage microblog\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/demo/microblog\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\tsusers \"gno.land/r/sys/users\"\n)\n\nvar (\n\ttitle  = \"gno-based microblog\"\n\tprefix = \"/r/demo/microblog:\"\n\tm      *microblog.Microblog\n)\n\nfunc init() {\n\tm = microblog.NewMicroblog(title, prefix)\n}\n\nfunc renderHome() string {\n\toutput := ufmt.Sprintf(\"# %s\\n\\n\", m.Title)\n\toutput += \"# pages\\n\\n\"\n\n\tfor _, page := range m.GetPages() {\n\t\tif u := susers.ResolveAddress(page.Author); u != nil {\n\t\t\toutput += ufmt.Sprintf(\"- [%s (%s)](%s%s)\\n\", u.Name(), page.Author.String(), m.Prefix, page.Author.String())\n\t\t} else {\n\t\t\toutput += ufmt.Sprintf(\"- [%s](%s%s)\\n\", page.Author.String(), m.Prefix, page.Author.String())\n\t\t}\n\t}\n\n\treturn output\n}\n\nfunc renderUser(user string) string {\n\tsilo := m.Pages.Get(user)\n\tif silo == nil {\n\t\treturn \"404\" // StatusNotFound\n\t}\n\n\treturn PageToString((silo.(*microblog.Page)))\n}\n\nfunc Render(path string) string {\n\tparts := strings.Split(path, \"/\")\n\n\tisHome := path == \"\"\n\tisUser := len(parts) == 1\n\n\tswitch {\n\tcase isHome:\n\t\treturn renderHome()\n\n\tcase isUser:\n\t\treturn renderUser(parts[0])\n\t}\n\n\treturn \"404\" // StatusNotFound\n}\n\nfunc PageToString(p *microblog.Page) string {\n\to := \"\"\n\tif u := susers.ResolveAddress(p.Author); u != nil {\n\t\to += ufmt.Sprintf(\"# [%s](/u/%s)\\n\\n\", u, u)\n\t}\n\to += ufmt.Sprintf(\"## [%s](/r/demo/microblog:%s)\\n\\n\", p.Author, p.Author)\n\n\to += ufmt.Sprintf(\"joined %s, last updated %s\\n\\n\", p.CreatedAt.Format(\"2006-02-01\"), p.LastPosted.Format(\"2006-02-01\"))\n\to += \"## feed\\n\\n\"\n\tfor _, u := range p.GetPosts() {\n\t\to += u.String() + \"\\n\\n\"\n\t}\n\treturn o\n}\n\n// NewPost takes a single argument (post markdown) and\n// adds a post to the address of the caller.\nfunc NewPost(cur realm, text string) string {\n\tif err := m.NewPost(0, cur, text); err != nil {\n\t\treturn \"unable to add new post\"\n\t}\n\treturn \"added new post\"\n}\n"},{"name":"microblog_test.gno","body":"package microblog\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\n// postAs threads the SetRealm'd cur into m.NewPost via cross(rlm).\n// Callers SetRealm in the enclosing crossing frame so the cur HIV\n// mutation lands where rlm captures it.\nfunc postAs(_ int, rlm realm, text string) error {\n\tvar err error\n\tfunc(cur realm) { err = m.NewPost(0, cur, text) }(cross(rlm))\n\treturn err\n}\n\nfunc TestMicroblog(cur realm, t *testing.T) {\n\tvar (\n\t\tauthor1 address = testutils.TestAddress(\"author1\")\n\t\tauthor2 address = testutils.TestAddress(\"author2\")\n\t)\n\n\turequire.Equal(t, \"404\", Render(\"/wrongpath\"), \"rendering not giving 404\")\n\turequire.NotEqual(t, \"404\", Render(\"\"), \"rendering / should not give 404\")\n\ttesting.SetRealm(testing.NewUserRealm(author1))\n\turequire.NoError(t, postAs(0, cur, \"goodbyte, web2\"), \"could not create post\")\n\n\t_, err := m.GetPage(author1.String())\n\turequire.NoError(t, err, \"silo should exist\")\n\n\t_, err = m.GetPage(\"no such author\")\n\turequire.Error(t, err, \"silo should not exist\")\n\n\ttesting.SetRealm(testing.NewUserRealm(author2))\n\turequire.NoError(t, postAs(0, cur, \"hello, web3\"), \"could not create post\")\n\turequire.NoError(t, postAs(0, cur, \"hello again, web3\"), \"could not create post\")\n\turequire.NoError(t, postAs(0, cur, \"hi again,\\n web4?\"), \"could not create post\")\n\n\tprintln(\"--- MICROBLOG ---\\n\\n\")\n\n\texpected := `# gno-based microblog\n\n# pages\n\n- [g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6](/r/demo/microblog:g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6)\n- [g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00](/r/demo/microblog:g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00)\n`\n\turequire.Equal(t, expected, Render(\"\"), \"incorrect rendering\")\n\n\texpected = `## [g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6](/r/demo/microblog:g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6)\n\njoined 2009-13-02, last updated 2009-13-02\n\n## feed\n\n\u003e goodbyte, web2\n\u003e\n\u003e *Fri, 13 Feb 2009 23:31:30 UTC*`\n\n\turequire.Equal(t, expected, strings.TrimSpace(Render(author1.String())), \"incorrect rendering\")\n\n\texpected = `## [g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00](/r/demo/microblog:g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00)\n\njoined 2009-13-02, last updated 2009-13-02\n\n## feed\n\n\u003e hi again,\n\u003e\n\u003e web4?\n\u003e\n\u003e *Fri, 13 Feb 2009 23:31:30 UTC*\n\n\u003e hello again, web3\n\u003e\n\u003e *Fri, 13 Feb 2009 23:31:30 UTC*\n\n\u003e hello, web3\n\u003e\n\u003e *Fri, 13 Feb 2009 23:31:30 UTC*`\n\n\turequire.Equal(t, expected, strings.TrimSpace(Render(author2.String())), \"incorrect rendering\")\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"O7lTssKDLp2HH8H30Bac0dUlgvqRYUkfhT715zzPuU4pS45zUgHor0p0/X6ZJRqdL8z0ruKr5AUkmeV9xlNPuw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"mirror","path":"gno.land/r/demo/mirror","files":[{"name":"doc.gno","body":"// Package mirror demonstrates that users can pass realm functions\n// as arguments to other realms.\npackage mirror\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/demo/mirror\"\ngno = \"0.9\"\n"},{"name":"mirror.gno","body":"package mirror\n\nimport (\n\t\"gno.land/p/nt/avl/v0\"\n)\n\nvar store avl.Tree\n\nfunc Register(_ realm, pkgpath string, rndr func(string) string) {\n\tif store.Has(pkgpath) {\n\t\treturn\n\t}\n\n\tif rndr == nil {\n\t\treturn\n\t}\n\n\tstore.Set(pkgpath, rndr)\n}\n\nfunc Render(path string) string {\n\tif raw := store.Get(path); raw != nil {\n\t\treturn raw.(func(string) string)(\"\")\n\t}\n\n\tif store.Size() == 0 {\n\t\treturn \"None are fair.\"\n\t}\n\n\treturn \"Mirror, mirror on the wall, which realm's the fairest of them all?\"\n}\n\n// Credits to @jeronimoalbi\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"Qji9jXcbu8rGNeCQFOXTvbdCxbooz4ppNeR6E+vTDxgWErdkTdyxgv4SOjtDTWVHfGsqWtV/T/Ra9EUw0V4rQg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"releases_example","path":"gno.land/r/demo/releases_example","files":[{"name":"dummy.gno","body":"package releases_example\n\nfunc init() {\n\t// dummy example data\n\tchangelog.NewRelease(\n\t\t\"v1\",\n\t\t\"r/demo/examples_example_v1\",\n\t\t\"initial release\",\n\t)\n\tchangelog.NewRelease(\n\t\t\"v2\",\n\t\t\"r/demo/examples_example_v2\",\n\t\t\"various improvements\",\n\t)\n}\n"},{"name":"example.gno","body":"// this package demonstrates a way to manage contract releases.\npackage releases_example\n\nimport (\n\t\"chain/runtime/unsafe\"\n\n\t\"gno.land/p/demo/releases\"\n)\n\nvar (\n\tchangelog = releases.NewChangelog(\"example_app\")\n\tadmin     = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n)\n\nfunc init() {\n\t// FIXME: admin = std.GetCreator()\n}\n\nfunc NewRelease(_ realm, name, url, notes string) {\n\tcaller := unsafe.OriginCaller()\n\tif caller != admin {\n\t\tpanic(\"restricted area\")\n\t}\n\tchangelog.NewRelease(name, url, notes)\n}\n\nfunc UpdateAdmin(_ realm, address_XXX address) {\n\tcaller := unsafe.OriginCaller()\n\tif caller != admin {\n\t\tpanic(\"restricted area\")\n\t}\n\tadmin = address_XXX\n}\n\nfunc Render(path string) string {\n\treturn changelog.Render(path)\n}\n"},{"name":"example0_test.gno","body":"package releases_example\n\nimport (\n\t\"gno.land/p/demo/releases\"\n)\n\nfunc ExampleNewChangelog() {\n\tprintln(\"-----------\")\n\tchangelog := releases.NewChangelog(\"example\")\n\tprint(changelog.Render(\"\"))\n\n\tprintln(\"-----------\")\n\tchangelog.NewRelease(\"v1\", \"r/blahblah\", \"* initial version\")\n\tprint(changelog.Render(\"\"))\n\n\tprintln(\"-----------\")\n\tchangelog.NewRelease(\"v2\", \"r/blahblah2\", \"* various improvements\\n* new shiny logo\")\n\tprint(changelog.Render(\"\"))\n\n\tprintln(\"-----------\")\n\tprint(changelog.Latest().Render())\n\n\t// Output:\n\t// -----------\n\t// # example\n\t//\n\t// -----------\n\t// # example\n\t//\n\t// ## [example v1 (latest)](r/blahblah)\n\t//\n\t// * initial version\n\t//\n\t// -----------\n\t// # example\n\t//\n\t// ## [example v2 (latest)](r/blahblah2)\n\t//\n\t// * various improvements\n\t// * new shiny logo\n\t//\n\t// ## [example v1](r/blahblah)\n\t//\n\t// * initial version\n\t//\n\t// -----------\n\t// ## [example v2 (latest)](r/blahblah2)\n\t//\n\t// * various improvements\n\t// * new shiny logo\n}\n"},{"name":"example1_test.gno","body":"package releases_example\n\nfunc ExampleRender() {\n\tprintln(\"-----------\")\n\tprint(Render(\"\"))\n\n\tprintln(\"-----------\")\n\tprint(Render(\"v1\"))\n\n\tprintln(\"-----------\")\n\tprint(Render(\"v42\"))\n\n\t// Output:\n\t// -----------\n\t// # example_app\n\t//\n\t// ## [example_app v2 (latest)](r/demo/examples_example_v2)\n\t//\n\t// various improvements\n\t//\n\t// ## [example_app v1](r/demo/examples_example_v1)\n\t//\n\t// initial release\n\t//\n\t// -----------\n\t// ## [example_app v1](r/demo/examples_example_v1)\n\t//\n\t// initial release\n\t//\n\t// -----------\n\t// no such release\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/demo/releases_example\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"b5FpjFIIOykkeDFGYuNT2qieia9BbXXhrKczx6kkTyxefSJdaTGqc0yuexI2+OlwWsjm7IxP78IkcIhdLTkRcQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"tamagotchi","path":"gno.land/r/demo/tamagotchi","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/demo/tamagotchi\"\ngno = \"0.9\"\n"},{"name":"realm.gno","body":"package tamagotchi\n\nimport (\n\t\"chain/runtime\"\n\n\t\"gno.land/p/demo/tamagotchi\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nvar t *tamagotchi.Tamagotchi\n\nfunc init(cur realm) {\n\tReset(cur, \"gnome#0\")\n}\n\nfunc Reset(cur realm, optionalName string) string {\n\tname := optionalName\n\tif name == \"\" {\n\t\theight := runtime.ChainHeight()\n\t\tname = ufmt.Sprintf(\"gnome#%d\", height)\n\t}\n\n\tt = tamagotchi.New(name)\n\n\treturn ufmt.Sprintf(\"A new tamagotchi is born. Their name is %s %s.\", t.Name(), t.Face())\n}\n\nfunc Feed(cur realm) string {\n\tt.Feed()\n\treturn t.Markdown()\n}\n\nfunc Play(cur realm) string {\n\tt.Play()\n\treturn t.Markdown()\n}\n\nfunc Heal(cur realm) string {\n\tt.Heal()\n\treturn t.Markdown()\n}\n\nfunc Render(path string) string {\n\ttama := t.Markdown()\n\tlinks := `Actions:\n* [Feed](/r/demo/tamagotchi$help\u0026func=Feed)\n* [Play](/r/demo/tamagotchi$help\u0026func=Play)\n* [Heal](/r/demo/tamagotchi$help\u0026func=Heal)\n* [Reset](/r/demo/tamagotchi$help\u0026func=Reset)\n`\n\n\treturn tama + \"\\n\\n\" + links\n}\n"},{"name":"z0_filetest.gno","body":"// PKGPATH: gno.land/r/demo/tamagotchi/filetests/z0_filetest\n\npackage z0_filetest\n\nimport (\n\t\"gno.land/r/demo/tamagotchi\"\n)\n\nfunc main(cur realm) {\n\ttamagotchi.Reset(cross(cur), \"tamagnotchi\")\n\tprintln(tamagotchi.Render(\"\"))\n}\n\n// Output:\n// # tamagnotchi 😃\n//\n// * age: 0\n// * hunger: 50\n// * happiness: 50\n// * health: 50\n// * sleepy: 0\n//\n// Actions:\n// * [Feed](/r/demo/tamagotchi$help\u0026func=Feed)\n// * [Play](/r/demo/tamagotchi$help\u0026func=Play)\n// * [Heal](/r/demo/tamagotchi$help\u0026func=Heal)\n// * [Reset](/r/demo/tamagotchi$help\u0026func=Reset)\n//\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"KI8KIK/hBHVOPkLN2APIDZeByInjkVblgnVG1Qiqjj50TC3roYQY1YDrDV9UfQM5r4GWkVToIjZyAO/HLqTxvw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"todolist","path":"gno.land/r/demo/todolist","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/demo/todolist\"\ngno = \"0.9\"\n"},{"name":"todolist.gno","body":"package todolist\n\nimport (\n\t\"bytes\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/todolist\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// State variables\nvar (\n\ttodolistTree *avl.Tree\n\ttlid         seqid.ID\n)\n\n// Constructor\nfunc init() {\n\ttodolistTree = avl.NewTree()\n}\n\nfunc NewTodoList(_ realm, title string) (int, string) {\n\t// Create new Todolist\n\ttl := todolist.NewTodoList(title)\n\t// Update AVL tree with new state\n\ttlid.Next()\n\ttodolistTree.Set(strconv.Itoa(int(tlid)), tl)\n\treturn int(tlid), \"created successfully\"\n}\n\nfunc AddTask(_ realm, todolistID int, title string) string {\n\t// Get Todolist from AVL tree\n\ttl := todolistTree.Get(strconv.Itoa(todolistID))\n\tif tl == nil {\n\t\tpanic(\"Todolist not found\")\n\t}\n\n\t// get the number of tasks in the todolist\n\tid := tl.(*todolist.TodoList).Tasks.Size()\n\n\t// create the task\n\ttask := todolist.NewTask(title)\n\n\t// Cast raw data from tree into Todolist struct\n\ttl.(*todolist.TodoList).AddTask(id, task)\n\n\treturn \"task added successfully\"\n}\n\nfunc ToggleTaskStatus(_ realm, todolistID int, taskID int) string {\n\t// Get Todolist from AVL tree\n\ttl := todolistTree.Get(strconv.Itoa(todolistID))\n\tif tl == nil {\n\t\tpanic(\"Todolist not found\")\n\t}\n\n\t// Get the task from the todolist\n\ttask := tl.(*todolist.TodoList).Tasks.Get(strconv.Itoa(taskID))\n\tif task == nil {\n\t\tpanic(\"Task not found\")\n\t}\n\n\t// Change the status of the task\n\ttodolist.ToggleTaskStatus(task.(*todolist.Task))\n\n\treturn \"task status changed successfully\"\n}\n\nfunc RemoveTask(_ realm, todolistID int, taskID int) string {\n\t// Get Todolist from AVL tree\n\ttl := todolistTree.Get(strconv.Itoa(todolistID))\n\tif tl == nil {\n\t\tpanic(\"Todolist not found\")\n\t}\n\n\t// Get the task from the todolist\n\tif !tl.(*todolist.TodoList).Tasks.Has(strconv.Itoa(taskID)) {\n\t\tpanic(\"Task not found\")\n\t}\n\n\t// Change the status of the task\n\ttl.(*todolist.TodoList).RemoveTask(strconv.Itoa(taskID))\n\n\treturn \"task status changed successfully\"\n}\n\nfunc RemoveTodoList(_ realm, todolistID int) string {\n\t// Get Todolist from AVL tree\n\tif !todolistTree.Has(strconv.Itoa(todolistID)) {\n\t\tpanic(\"Todolist not found\")\n\t}\n\n\t// Remove the todolist\n\ttodolistTree.Remove(strconv.Itoa(todolistID))\n\n\treturn \"Todolist removed successfully\"\n}\n\nfunc Render(path string) string {\n\tif path == \"\" {\n\t\treturn renderHomepage()\n\t}\n\n\treturn \"unknown page\"\n}\n\nfunc renderHomepage() string {\n\t// Define empty buffer\n\tvar b bytes.Buffer\n\n\tb.WriteString(\"# Welcome to ToDolist\\n\\n\")\n\n\t// If no todolists have been created\n\tif todolistTree.Size() == 0 {\n\t\tb.WriteString(\"### No todolists available currently!\")\n\t\treturn b.String()\n\t}\n\n\t// Iterate through AVL tree\n\ttodolistTree.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\t// cast raw data from tree into Todolist struct\n\t\ttl := value.(*todolist.TodoList)\n\n\t\t// Add Todolist name\n\t\tb.WriteString(\n\t\t\tufmt.Sprintf(\n\t\t\t\t\"## Todolist #%s: %s\\n\",\n\t\t\t\tkey, // Todolist ID\n\t\t\t\ttl.GetTodolistTitle(),\n\t\t\t),\n\t\t)\n\n\t\t// Add Todolist owner\n\t\tb.WriteString(\n\t\t\tufmt.Sprintf(\n\t\t\t\t\"#### Todolist owner : %s\\n\",\n\t\t\t\ttl.GetTodolistOwner(),\n\t\t\t),\n\t\t)\n\n\t\t// List all todos that are currently Todolisted\n\t\tif todos := tl.GetTasks(); len(todos) \u003e 0 {\n\t\t\tb.WriteString(\n\t\t\t\tufmt.Sprintf(\"Currently Todo tasks: %d\\n\\n\", len(todos)),\n\t\t\t)\n\n\t\t\tfor index, todo := range todos {\n\t\t\t\tb.WriteString(\n\t\t\t\t\tufmt.Sprintf(\"#%d - %s  \", index, todo.Title),\n\t\t\t\t)\n\t\t\t\t// displays a checked box if task is marked as done, an empty box if not\n\t\t\t\tif todo.Done {\n\t\t\t\t\tb.WriteString(\n\t\t\t\t\t\t\"☑\\n\\n\",\n\t\t\t\t\t)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tb.WriteString(\n\t\t\t\t\t\"☐\\n\\n\",\n\t\t\t\t)\n\t\t\t}\n\t\t} else {\n\t\t\tb.WriteString(\"No tasks in this list currently\\n\")\n\t\t}\n\n\t\tb.WriteString(\"\\n\")\n\t\treturn false\n\t})\n\n\treturn b.String()\n}\n"},{"name":"todolist_test.gno","body":"package todolist\n\nimport (\n\t\"chain/runtime/unsafe\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/todolist\"\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nvar (\n\tnode any\n\ttdl  *todolist.TodoList\n)\n\nfunc TestNewTodoList(cur realm, t *testing.T) {\n\ttitle := \"My Todo List\"\n\ttlid, _ := NewTodoList(cross(cur), title)\n\tuassert.Equal(t, 1, tlid, \"tlid does not match\")\n\n\t// get the todolist node from the tree\n\tnode = todolistTree.Get(strconv.Itoa(tlid))\n\t// convert the node to a TodoList struct\n\ttdl = node.(*todolist.TodoList)\n\n\tuassert.Equal(t, title, tdl.Title, \"title does not match\")\n\tuassert.Equal(t, 1, tlid, \"tlid does not match\")\n\tuassert.Equal(t, tdl.Owner.String(), unsafe.OriginCaller().String(), \"owner does not match\")\n\tuassert.Equal(t, 0, len(tdl.GetTasks()), \"Expected no tasks in the todo list\")\n}\n\nfunc TestAddTask(cur realm, t *testing.T) {\n\tAddTask(cross(cur), 1, \"Task 1\")\n\n\ttasks := tdl.GetTasks()\n\tuassert.Equal(t, 1, len(tasks), \"total task does not match\")\n\tuassert.Equal(t, \"Task 1\", tasks[0].Title, \"task title does not match\")\n\tuassert.False(t, tasks[0].Done, \"Expected task to be not done\")\n}\n\nfunc TestToggleTaskStatus(cur realm, t *testing.T) {\n\tToggleTaskStatus(cross(cur), 1, 0)\n\ttask := tdl.GetTasks()[0]\n\tuassert.True(t, task.Done, \"Expected task to be done, but it is not marked as done\")\n\n\tToggleTaskStatus(cross(cur), 1, 0)\n\tuassert.False(t, task.Done, \"Expected task to be not done, but it is marked as done\")\n}\n\nfunc TestRemoveTask(cur realm, t *testing.T) {\n\tRemoveTask(cross(cur), 1, 0)\n\ttasks := tdl.GetTasks()\n\tuassert.Equal(t, 0, len(tasks), \"Expected no tasks in the todo list\")\n}\n\nfunc TestRemoveTodoList(cur realm, t *testing.T) {\n\tRemoveTodoList(cross(cur), 1)\n\tuassert.Equal(t, 0, todolistTree.Size(), \"Expected no tasks in the todo list\")\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"f8RhPDh5nJDMCQv8noYTjz6brdnOyir+hzntIZDPXEBEFJOpqYKTXN0GW6UdC0a74Ev6CKY04mp6ETEJZtfREw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"config","path":"gno.land/r/devrels/config","files":[{"name":"config.gno","body":"package config\n\nimport (\n\t\"gno.land/p/moul/authz/v0\"\n)\n\nvar (\n\tGnoDevRels = authz.NewMemberAuthority(\n\t\taddress(\"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"), // @moul\n\t\taddress(\"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\"), // @leohhhn\n\t)\n\tAuth = authz.NewWithAuthority(GnoDevRels)\n)\n\nfunc Render(_ string) string {\n\treturn Auth.Authority().String()\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/devrels/config\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"EbD0ZezOCv6efSpRNU2OAUsmKFo4uu2V7QFa5GHXOoAgHzveToM/pmbiSciGLW4fBaWxW+4bdi+Ok1DIXBw8fw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"adder","path":"gno.land/r/docs/adder","files":[{"name":"adder.gno","body":"package adder\n\nimport (\n\t\"strconv\"\n\t\"time\"\n\n\t\"gno.land/p/moul/txlink/v0\"\n)\n\n// Global variables to store the current number and last update timestamp\nvar (\n\tnumber     int\n\tlastUpdate time.Time\n)\n\n// Add function to update the number and timestamp\nfunc Add(_ realm, n int) {\n\tnumber += n\n\tlastUpdate = time.Now()\n}\n\n// Render displays the current number value, last update timestamp, and a link to call Add with 42\nfunc Render(_ string) string {\n\t// Display the current number and formatted last update time\n\tresult := \"# Add Example\\n\\n\"\n\tresult += \"Current Number: \" + strconv.Itoa(number) + \"\\n\\n\"\n\tresult += \"Last Updated: \" + formatTimestamp(lastUpdate) + \"\\n\\n\"\n\n\t// Generate a transaction link to call Add with 42 as the default parameter\n\ttxLink := txlink.Call(\"Add\", \"n\", \"42\")\n\tresult += \"[Increase Number](\" + txLink + \")\\n\"\n\n\treturn result\n}\n\n// Helper function to format the timestamp for readability\nfunc formatTimestamp(timestamp time.Time) string {\n\tif timestamp.IsZero() {\n\t\treturn \"Never\"\n\t}\n\treturn timestamp.Format(\"2006-01-02 15:04:05\")\n}\n"},{"name":"adder_test.gno","body":"package adder\n\nimport (\n\t\"testing\"\n)\n\nfunc TestRenderAndAdd(cur realm, t *testing.T) {\n\t// Initial Render output\n\toutput := Render(\"\")\n\texpected := `# Add Example\n\nCurrent Number: 0\n\nLast Updated: Never\n\n[Increase Number](/r/docs/adder$help\u0026func=Add\u0026n=42)\n`\n\tif output != expected {\n\t\tt.Errorf(\"Initial Render failed, got:\\n%s\", output)\n\t}\n\n\t// Call Add with a value of 10\n\tAdd(cross(cur), 10)\n\n\t// Call Add again with a value of -5\n\tAdd(cross(cur), -5)\n\n\t// Render after two Add calls\n\tfinalOutput := Render(\"\")\n\n\t// Initial Render output\n\toutput = Render(\"\")\n\texpected = `# Add Example\n\nCurrent Number: 5\n\nLast Updated: 2009-02-13 23:31:30\n\n[Increase Number](/r/docs/adder$help\u0026func=Add\u0026n=42)\n`\n\tif output != expected {\n\t\tt.Errorf(\"Final Render failed, got:\\n%s\\nexpected:\\n%s\", output, finalOutput)\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/adder\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"srsZ5cEpbo7Jlvqp7OdV957QdrjIP7Z4dUvLfdx6ue4UYjAho2tSiSjwz1zkIsAFvWcansB5lYypq6cXvPIx+w=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"avl_pager","path":"gno.land/r/docs/avl_pager","files":[{"name":"avl_pager.gno","body":"package avl_pager\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/p/nt/avl/pager/v0\"\n\t\"gno.land/p/nt/avl/v0\"\n)\n\n// Tree instance for 100 items\nvar tree *avl.Tree\n\n// Initialize a tree with 100 items.\nfunc init() {\n\ttree = avl.NewTree()\n\tfor i := 1; i \u003c= 100; i++ {\n\t\tkey := \"Item\" + strconv.Itoa(i)\n\t\ttree.Set(key, \"Value of \"+key)\n\t}\n}\n\n// Render paginated content based on the given URL path.\n// URL format: `...?page=\u003cpage\u003e\u0026size=\u003csize\u003e` (default is page 1 and size 10).\nfunc Render(path string) string {\n\tp := pager.NewPager(tree, 10, false) // Default page size is 10\n\tpage := p.MustGetPageByPath(path)\n\n\t// Header and pagination info\n\tresult := \"# Paginated Items\\n\"\n\tresult += \"Page \" + strconv.Itoa(page.PageNumber) + \" of \" + strconv.Itoa(page.TotalPages) + \"\\n\\n\"\n\tresult += page.Picker(path) + \"\\n\\n\"\n\n\t// Display items on the current page\n\tfor _, item := range page.Items {\n\t\tresult += \"- \" + item.Key + \": \" + item.Value.(string) + \"\\n\"\n\t}\n\n\tresult += \"\\n\" + page.Picker(path) // Repeat page picker for ease of navigation\n\treturn result\n}\n"},{"name":"avl_pager_test.gno","body":"package avl_pager\n\nimport (\n\t\"testing\"\n)\n\nfunc TestRender(t *testing.T) {\n\treq := \"?sort=name\u0026order=asc\"\n\toutput := Render(req)\n\texpected := `# Paginated Items\nPage 1 of 10\n\n**1** | [2](?page=2\u0026order=asc\u0026sort=name) | [3](?page=3\u0026order=asc\u0026sort=name) | … | [10](?page=10\u0026order=asc\u0026sort=name)\n\n- Item1: Value of Item1\n- Item10: Value of Item10\n- Item100: Value of Item100\n- Item11: Value of Item11\n- Item12: Value of Item12\n- Item13: Value of Item13\n- Item14: Value of Item14\n- Item15: Value of Item15\n- Item16: Value of Item16\n- Item17: Value of Item17\n\n**1** | [2](?page=2\u0026order=asc\u0026sort=name) | [3](?page=3\u0026order=asc\u0026sort=name) | … | [10](?page=10\u0026order=asc\u0026sort=name)`\n\tif output != expected {\n\t\tt.Errorf(\"Render(%q) failed, got:\\n%s\\nwant:\\n%s\", req, output, expected)\n\t}\n}\n\nfunc TestRender_page2(t *testing.T) {\n\treq := \"?page=2\u0026size=10\u0026sort=name\u0026order=asc\"\n\toutput := Render(req)\n\texpected := `# Paginated Items\nPage 2 of 10\n\n[1](?page=1\u0026order=asc\u0026size=10\u0026sort=name) | **2** | [3](?page=3\u0026order=asc\u0026size=10\u0026sort=name) | [4](?page=4\u0026order=asc\u0026size=10\u0026sort=name) | … | [10](?page=10\u0026order=asc\u0026size=10\u0026sort=name)\n\n- Item18: Value of Item18\n- Item19: Value of Item19\n- Item2: Value of Item2\n- Item20: Value of Item20\n- Item21: Value of Item21\n- Item22: Value of Item22\n- Item23: Value of Item23\n- Item24: Value of Item24\n- Item25: Value of Item25\n- Item26: Value of Item26\n\n[1](?page=1\u0026order=asc\u0026size=10\u0026sort=name) | **2** | [3](?page=3\u0026order=asc\u0026size=10\u0026sort=name) | [4](?page=4\u0026order=asc\u0026size=10\u0026sort=name) | … | [10](?page=10\u0026order=asc\u0026size=10\u0026sort=name)`\n\tif output != expected {\n\t\tt.Errorf(\"Render(%q) failed, got:\\n%s\\nwant:\\n%s\", req, output, expected)\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/avl_pager\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"fU/yq/gPKbesfgk3D6jPvR/68p09FEBO5rxUW+i1ZcMtxmDTOAdU1diQI3HiClCA4lnB87SRZWDvuAEebkIhrw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"avl_pager_params","path":"gno.land/r/docs/avl_pager_params","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/avl_pager_params\"\ngno = \"0.9\"\n"},{"name":"render.gno","body":"package avl_pager_params\n\nimport (\n\t\"gno.land/p/moul/realmpath/v0\"\n\t\"gno.land/p/nt/avl/pager/v0\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// We'll keep some demo data in an AVL tree to showcase pagination.\nvar (\n\titems     *avl.Tree\n\tidCounter seqid.ID\n)\n\nfunc init() {\n\titems = avl.NewTree()\n\t// Populate the tree with 15 sample items for demonstration.\n\tfor i := 1; i \u003c= 15; i++ {\n\t\tid := idCounter.Next().String()\n\t\titems.Set(id, \"Some item value: \"+id)\n\t}\n}\n\nfunc Render(path string) string {\n\t// 1) Parse the incoming path to split route vs. query.\n\treq := realmpath.Parse(path)\n\t//    - req.Path contains everything *before* ? or $ (? - query params, $ - gnoweb params)\n\t//    - The remaining part (page=2, size=5, etc.) is not in req.Path.\n\n\t// 2) If no specific route is provided (req.Path == \"\"), we’ll show a “home” page\n\t//    that displays a list of configs in paginated form.\n\tif req.Path == \"\" {\n\t\treturn renderHome(path)\n\t}\n\n\t// 3) If a route *is* provided (e.g. :SomeKey),\n\t//    we will interpret it as a request for a specific page.\n\treturn renderConfigItem(req.Path)\n}\n\n// renderHome shows a paginated list of config items if route == \"\".\nfunc renderHome(fullPath string) string {\n\t// Create a Pager for our config tree, with a default page size of 5.\n\tp := pager.NewPager(items, 5, false)\n\n\t// MustGetPageByPath uses the *entire* path (including query parts: ?page=2, etc.)\n\tpage := p.MustGetPageByPath(fullPath)\n\n\t// Start building the output (plain text or markdown).\n\tout := \"# AVL Pager + Render paths\\n\\n\"\n\tout += `This realm showcases how to maintain a paginated list while properly parsing render paths. \nYou can see how a single page can include a paginated element (like the example below), and how clicking \nan item can take you to a dedicated page for that specific item.\n\nNo matter how you browse through the paginated list, the introductory text (this section) remains the same.\n\n`\n\n\tout += ufmt.Sprintf(\"Showing page %d of %d\\n\\n\", page.PageNumber, page.TotalPages)\n\n\t// List items for this page.\n\tfor _, item := range page.Items {\n\t\t// Link each item to a details page: e.g. \":Config01\"\n\t\tout += ufmt.Sprintf(\"- [Item %s](/r/docs/avl_pager_params:%s)\\n\", item.Key, item.Key)\n\t}\n\n\t// Insert pagination controls (previous/next links, etc.).\n\tout += \"\\n\" + page.Picker(fullPath) + \"\\n\\n\"\n\tout += \"### [Go back to r/docs](/r/docs/home)\"\n\n\treturn out\n}\n\n// renderConfigItem shows details for a single item, e.g. \":item001\".\nfunc renderConfigItem(itemName string) string {\n\tvalue := items.Get(itemName)\n\tif value == nil {\n\t\treturn ufmt.Sprintf(\"**No item found** for key: %s\", itemName)\n\t}\n\n\tout := ufmt.Sprintf(\"# Item %s\\n\\n%s\\n\\n\", itemName, value.(string))\n\tout += \"[Go back](/r/docs/avl_pager_params)\"\n\treturn out\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"qo3gz1gffawCFOzBIseWfJmNa++VBL5/LCwlKadTm8RG3536z03HwSFGBvN1yVJ7O+f05tJbW159eQPC8UaZDg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"buttons","path":"gno.land/r/docs/buttons","files":[{"name":"buttons.gno","body":"package buttons\n\nimport (\n\t\"gno.land/p/moul/txlink/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nvar (\n\tmotd       = \"The Initial Message\\n\\n\"\n\tlastCaller address\n)\n\nfunc UpdateMOTD(cur realm, newmotd string) {\n\tmotd = newmotd\n\tlastCaller = cur.Previous().Address()\n}\n\nfunc Render(path string) string {\n\tif path == \"motd\" {\n\t\tout := \"# Message of the Day:\\n\\n\"\n\t\tout += \"---\\n\\n\"\n\t\tout += \"# \" + motd + \"\\n\\n\"\n\t\tout += \"---\\n\\n\"\n\t\tlink := txlink.Call(\"UpdateMOTD\", \"newmotd\", \"Message!\") // \"/r/docs/buttons$help\u0026func=UpdateMOTD\u0026newmotd=Message!\"\n\t\tout += ufmt.Sprintf(\"Click **[here](%s)** to update the Message of The Day!\\n\\n\", link)\n\t\tout += \"[Go back to home page](/r/docs/buttons)\\n\\n\"\n\t\tout += \"Last updated by \" + lastCaller.String()\n\n\t\treturn out\n\t}\n\n\tout := `# Buttons\n\nUsers can create simple hyperlink buttons to view specific realm pages and\ndo specific realm actions, such as calling a specific function with some arguments.\n\nThe foundation for this functionality are markdown links; for example, you can\nclick...\n` + \"\\n## [here](/r/docs/buttons:motd)\\n\" + `...to view this realm's message of the day.`\n\n\treturn out\n}\n"},{"name":"buttons_test.gno","body":"package buttons\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestRenderMotdLink(t *testing.T) {\n\tres := Render(\"motd\")\n\tconst wantLink = \"/r/docs/buttons$help\u0026func=UpdateMOTD\u0026newmotd=Message%21\"\n\tif !strings.Contains(res, wantLink) {\n\t\tt.Fatalf(\"%s\\ndoes not contain correct help page link: %s\", res, wantLink)\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/buttons\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"xH1mLXnufQf78AjDV+nzDAAQiNgTflH89b27R49mHOBKFUq/XAQmxUhzrt8dnB7Rfpk7YIpJLxlouEVTDRbJGQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"charts","path":"gno.land/r/docs/charts","files":[{"name":"charts.gno","body":"package charts\n\nfunc Render(path string) string {\n\t// Handle subroutes for specific chart types\n\tswitch path {\n\tcase \"piechart\":\n\t\treturn RenderPieChart()\n\tcase \"gauge\":\n\t\treturn RenderGauge()\n\tdefault:\n\t\treturn RenderMainDocs()\n\t}\n}\n\n// RenderMainDocs renders the main charts documentation page\nfunc RenderMainDocs() string {\n\t// Generate live examples\n\tpieChartExample := RenderPieChartExample()\n\tgaugeExample := RenderGaugeExample()\n\n\treturn `\n# Chart Packages Documentation\n\n## Available Packages\n\n- **[Piechart](/p/samcrew/piechart/v0)** - SVG Pie Charts\n  - [View All Examples](/r/docs/charts:piechart) - Detailed documentation with multiple examples\n- **[Gauge](/p/samcrew/gauge)** - SVG Gauge Charts  \n  - [View All Examples](/r/docs/charts:gauge) - Detailed documentation with multiple examples\n\n## Examples\n\n### [Piechart](/p/samcrew/piechart/v0) - SVG Pie Charts\nGenerate pie charts with label as SVG images .\n\n` + pieChartExample + `\n\n[→ View all Piechart examples](/r/docs/charts:piechart)\n\n### [Gauge](/p/samcrew/gauge) - SVG Gauge Charts  \nGenerate progress bars and gauges as SVG images with customizable styling and labels.\n\n` + gaugeExample + `\n\n[→ View all Gauge examples](/r/docs/charts:gauge)\n\n---\n\n*For complete API documentation, visit the individual package pages.*\n`\n}\n"},{"name":"gauge.gno","body":"package charts\n\nimport (\n\t\"gno.land/p/samcrew/gauge\"\n)\n\n// RenderGaugeExample creates a simple gauge bar example\nfunc RenderGaugeExample() string {\n\treturn gauge.Render(75, 100, \"Progress\", \"#2ecc71\", gauge.DefaultConfig)\n}\n\n// Render renders the gauge documentation page\nfunc RenderGauge() string {\n\t// Multiple gauge examples\n\tbasicExample := RenderGaugeExample()\n\n\t// Custom config example\n\tcustomConfig := gauge.Config{\n\t\tPercentOnly:  true,\n\t\tWidth:        400,\n\t\tCanvasHeight: 35,\n\t\tFontSize:     14,\n\t\tPaddingH:     8,\n\t}\n\tcustomExample := gauge.Render(90, 100, \"Loading\", \"#3498db\", customConfig)\n\n\t// Different progress example\n\tprogressExample := gauge.Render(33, 50, \"Health\", \"#e74c3c\", gauge.DefaultConfig)\n\n\treturn `\n# [Gauge](/p/samcrew/gauge) Documentation\n\nGenerate progress bars and gauges as SVG images with customizable styling and labels.\n\n## Basic Usage\n\n` + \"```go\" + `\nimport \"gno.land/p/samcrew/gauge\"\n\n// Basic gauge with default config\ngauge := gauge.Render(75, 100, \"Progress\", \"#2ecc71\", gauge.DefaultConfig)\n` + \"```\" + `\n\n` + basicExample + `\n\n## Custom Configuration Example\n\n` + \"```go\" + `\nconfig := gauge.Config{\n    PercentOnly:  true, // Show only percentage\n    Width:        400,  // Custom width in pixels\n    CanvasHeight: 35,   // Custom height in pixels\n    FontSize:     14,   // Font size in pixels\n    PaddingH:     8,    // Horizontal padding in pixels\n}\ngauge := gauge.Render(90, 100, \"Loading\", \"#3498db\", config)\n` + \"```\" + `\n\n` + customExample + `\n\n## Different Progress Example\n\n` + \"```go\" + `\nhealthGauge := gauge.Render(33, 50, \"Health\", \"#e74c3c\", gauge.DefaultConfig)\n` + \"```\" + `\n\n` + progressExample + `\n\n## API Reference\n\n` + \"```go\" + `\ntype Config struct {\n    PercentOnly  bool // Only display the percentage on the right side\n    Width        int  // Width of the gauge in pixels\n    CanvasHeight int  // Height of the gauge in pixels\n    FontSize     int  // Font size of the text in pixels\n    PaddingH     int  // Horizontal padding (for the text) in pixels\n}\n\nvar DefaultConfig = Config{\n    PercentOnly:  false,\n    Width:        300,\n    CanvasHeight: 30,\n    FontSize:     16,\n    PaddingH:     6,\n}\n\n// value: Current value (must be ≤ total)\n// total: Maximum value (must be \u003e 0)\n// label: Text label displayed on the left\n// color: Fill color (hex format, e.g., \"#4caf50\")\n// config: Configuration options\n// Returns: SVG string as markdown image\nfunc Render(value int, total int, label string, color string, config Config) string\n` + \"```\" + `\n---\n\n[← Back to Charts](/r/docs/charts) | [View Package](/p/samcrew/gauge)\n`\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/charts\"\ngno = \"0.9\"\n"},{"name":"piechart.gno","body":"package charts\n\nimport (\n\t\"gno.land/p/samcrew/piechart/v0\"\n)\n\n// RenderPieChartExample creates a simple data distribution pie chart example\nfunc RenderPieChartExample() string {\n\tdata := []piechart.PieSlice{\n\t\t{Value: 35, Color: \"#e74c3c\", Label: \"Category A\"},\n\t\t{Value: 25, Color: \"#3498db\", Label: \"Category B\"},\n\t\t{Value: 20, Color: \"#2ecc71\", Label: \"Category C\"},\n\t\t{Value: 15, Color: \"#f39c12\", Label: \"Category D\"},\n\t\t{Value: 5, Color: \"#9b59b6\", Label: \"Category E\"},\n\t}\n\treturn piechart.Render(data, \"Data Distribution Example\")\n}\n\n// Render renders the piechart documentation page\nfunc RenderPieChart() string {\n\t// Multiple piechart examples\n\tbasicExample := RenderPieChartExample()\n\n\t// Custom example with different data\n\tcustomData := []piechart.PieSlice{\n\t\t{Value: 40, Color: \"#FF6B6B\", Label: \"Red Section\"},\n\t\t{Value: 30, Color: \"#4ECDC4\", Label: \"Teal Section\"},\n\t\t{Value: 20, Color: \"#45B7D1\", Label: \"Blue Section\"},\n\t\t{Value: 10, Color: \"#96CEB4\", Label: \"Green Section\"},\n\t}\n\tcustomExample := piechart.Render(customData, \"Custom Color Example\")\n\n\t// Small dataset example\n\tsmallData := []piechart.PieSlice{\n\t\t{Value: 70, Color: \"#FF9F43\", Label: \"Majority\"},\n\t\t{Value: 30, Color: \"#686DE0\", Label: \"Minority\"},\n\t}\n\tsmallExample := piechart.Render(smallData, \"Simple Split\")\n\n\treturn `\n# [Piechart](/p/samcrew/piechart/v0) Documentation\n\nGenerate pie charts with label as SVG images.\n\n## Basic Usage\n\n` + \"```go\" + `\nimport \"gno.land/p/samcrew/piechart/v0\"\n\ndata := []piechart.PieSlice{\n    {Value: 35, Color: \"#e74c3c\", Label: \"Category A\"},\n    {Value: 25, Color: \"#3498db\", Label: \"Category B\"},\n    {Value: 20, Color: \"#2ecc71\", Label: \"Category C\"},\n    {Value: 15, Color: \"#f39c12\", Label: \"Category D\"},\n    {Value: 5, Color: \"#9b59b6\", Label: \"Category E\"},\n}\nchart := piechart.Render(data, \"Data Distribution Example\")\n` + \"```\" + `\n\n` + basicExample + `\n\n---\n\n## Custom Colors Example\n\n` + \"```go\" + `\ncustomData := []piechart.PieSlice{\n    {Value: 40, Color: \"#FF6B6B\", Label: \"Red Section\"},\n    {Value: 30, Color: \"#4ECDC4\", Label: \"Teal Section\"},\n    {Value: 20, Color: \"#45B7D1\", Label: \"Blue Section\"},\n    {Value: 10, Color: \"#96CEB4\", Label: \"Green Section\"},\n}\nchart := piechart.Render(customData, \"Custom Color Example\")\n` + \"```\" + `\n\n` + customExample + `\n\n---\n\n## Simple Split Example\n\n` + \"```go\" + `\nsimpleData := []piechart.PieSlice{\n    {Value: 70, Color: \"#FF9F43\", Label: \"Majority\"},\n    {Value: 30, Color: \"#686DE0\", Label: \"Minority\"},\n}\nchart := piechart.Render(simpleData, \"Simple Split\")\n` + \"```\" + `\n\n` + smallExample + `\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---\n\n[← Back to Charts](/r/docs/charts) | [View Package](/p/samcrew/piechart/v0)\n`\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"LwowZOavBiq12yxK9WtS7ohLJbSr5UITcjvk6978FH1ZZVS4zNddYDBl9POCJ/03M2QuXGQohdxehOvt+eNEDQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"complexargs","path":"gno.land/r/docs/complexargs","files":[{"name":"complexargs.gno","body":"package complexargs\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Some example complex types, ie a slice, and a custom type\nvar (\n\tslice    = []int{1, 2, 3}\n\tmyObject = \u0026CustomType{\n\t\tName:    \"Alice\",\n\t\tNumbers: []int{4, 5, 6},\n\t}\n)\n\ntype CustomType struct {\n\tName    string\n\tNumbers []int\n}\n\n// SetSlice takes a complex argument that must be called using MsgRun or from another contract that imports this one.\nfunc SetSlice(_ realm, newSlice []int) {\n\tslice = newSlice\n}\n\n// SetMyObject takes a complex argument that must be called using MsgRun or from another contract that imports this one.\nfunc SetMyObject(_ realm, newCoolObject CustomType) {\n\tmyObject = \u0026newCoolObject\n}\n\nfunc Render(_ string) string {\n\tout := \"# Complex argument functions\\n\\n\"\n\tout += `Exposed realm functions and methods that take in complex arguments, such as slices, structs, pointers, etc,\ncannot be called via a standard \"MsgCall\" transaction. To call these functions, users need to use \"MsgRun\".\n\nCheck out the source code to see example functions and objects.\n\nIn this case, the following \"MsgRun\" code would be used to call the function:\n`\n\tout += ufmt.Sprintf(\"```go\\n%s\\n```\\n\\n\", msgrun)\n\n\tout += \"Value of int slice: \"\n\tfor i, v := range slice {\n\t\tif i \u003e 0 {\n\t\t\tout += \", \"\n\t\t}\n\t\tout += ufmt.Sprintf(\"%d\", v)\n\t}\n\tout += \"\\n\\n\"\n\n\tif myObject != nil {\n\t\ts := \"\"\n\t\tfor i, v := range myObject.Numbers {\n\t\t\tif i \u003e 0 {\n\t\t\t\ts += \",\"\n\t\t\t}\n\t\t\ts += ufmt.Sprintf(\"%d\", v)\n\t\t}\n\t\tout += ufmt.Sprintf(\"Value of myObject: `CustomObject{Name: %s, Numbers: %s}`\", myObject.Name, s)\n\t}\n\n\tout = strings.ReplaceAll(out, \"\\\"\", \"`\")\n\treturn out\n}\n\nconst msgrun = `package main\n\n// Import the realm you want to call\nimport \"gno.land/r/docs/complexargs\"\n\nfunc main(cur realm) {\n\t// Create the complex arguments to pass:\n\tslice := []int{1, 2, 3}\n\t// Call the function\n\tcomplexargs.SetSlice(cross(cur), slice)\n\n\t// The same can be done with custom types:\n\tobj := complexargs.CustomType{Name: \"whatever\", Numbers: []int{1, 10, 100}}\n\tcomplexargs.SetMyObject(obj)\n}`\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/complexargs\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"QMI+n8kwJR6BH/xebOjVGoXvxviHMFiQH4dt/k0df0gGgjP2zpQnJYzJxCovGmh8TQ9YEFF41dU1RP01AiVi2A=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"counter","path":"gno.land/r/docs/composition/counter","files":[{"name":"counter.gno","body":"// Package counter is a sub-component of /r/docs/composition.\n// It holds a counter that only the composition realm can change.\npackage counter\n\nimport (\n\t\"strconv\"\n)\n\nvar count int64\n\n// allowedCaller is the only realm allowed to modify the counter\nconst allowedCaller = \"gno.land/r/docs/composition\"\n\n// Increment increases the counter by 1 (restricted to composition realm)\nfunc Increment(cur realm) {\n\tassertAuthorized(cur)\n\tcount++\n}\n\n// Decrement decreases the counter by 1 (restricted to composition realm)\nfunc Decrement(cur realm) {\n\tassertAuthorized(cur)\n\tcount--\n}\n\n// Reset sets the counter back to zero (restricted to composition realm)\nfunc Reset(cur realm) {\n\tassertAuthorized(cur)\n\tcount = 0\n}\n\n// assertAuthorized ensures only the composition realm can modify state\nfunc assertAuthorized(cur realm) {\n\tif !cur.IsCurrent() {\n\t\tpanic(\"invalid realm\")\n\t}\n\tcaller := cur.Previous()\n\tif caller.PkgPath() != allowedCaller {\n\t\tpanic(\"unauthorized: only \" + allowedCaller + \" can modify counter\")\n\t}\n}\n\n// GetCountStr returns the current count as a string\nfunc GetCountStr() string {\n\treturn strconv.FormatInt(count, 10)\n}\n\n// RenderValue returns a markdown snippet showing the current value\nfunc RenderValue() string {\n\treturn \"**\" + strconv.FormatInt(count, 10) + \"**\"\n}\n\nfunc Render(_ string) string {\n\treturn \"# Counter Component\\n\\nCurrent value: \" + RenderValue() + \"\\n\\nThis is a sub-component of [/r/docs/composition](/r/docs/composition).\"\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/composition/counter\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"E/kGLQwXQ79Dck2CVy5XSuTmrBQGZJjjpfVMF/GWqi1tzD55rTHY+Fklag2QxUnJN4qaGHx5/S2VP1zQpzNGpg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"logger","path":"gno.land/r/docs/composition/logger","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/composition/logger\"\ngno = \"0.9\"\n"},{"name":"logger.gno","body":"// Package logger is a sub-component of /r/docs/composition.\n// It holds an append-only log that only the composition realm can change.\npackage logger\n\nimport (\n\t\"time\"\n)\n\nvar logs []string\n\n// allowedCaller is the only realm allowed to modify the logger\nconst allowedCaller = \"gno.land/r/docs/composition\"\n\n// Log adds a new entry to the log (restricted to composition realm)\nfunc Log(cur realm, message string) {\n\tassertAuthorized(cur)\n\ttimestamp := time.Now().Format(\"15:04:05\")\n\tlogs = append(logs, \"[\"+timestamp+\"] \"+message)\n}\n\n// Clear removes all log entries (restricted to composition realm)\nfunc Clear(cur realm) {\n\tassertAuthorized(cur)\n\tlogs = nil\n}\n\n// assertAuthorized ensures only the composition realm can modify state\nfunc assertAuthorized(cur realm) {\n\tif !cur.IsCurrent() {\n\t\tpanic(\"invalid realm\")\n\t}\n\tcaller := cur.Previous()\n\tif caller.PkgPath() != allowedCaller {\n\t\tpanic(\"unauthorized: only \" + allowedCaller + \" can modify logger\")\n\t}\n}\n\n// RenderLog returns a markdown snippet showing the log entries\nfunc RenderLog() string {\n\tif len(logs) == 0 {\n\t\treturn \"_No activity logged yet._\"\n\t}\n\n\tout := \"\"\n\t// Show last 10 entries, most recent first\n\tstart := len(logs) - 10\n\tif start \u003c 0 {\n\t\tstart = 0\n\t}\n\tfor i := len(logs) - 1; i \u003e= start; i-- {\n\t\tout += \"- \" + logs[i] + \"\\n\"\n\t}\n\treturn out\n}\n\nfunc Render(_ string) string {\n\treturn \"# Logger Component\\n\\n\" + RenderLog() + \"\\n\\nThis is a sub-component of [/r/docs/composition](/r/docs/composition).\"\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"EMrg6J6FN3ofwXCAWGKUPL32paWThWaoRtrDar39tL9wu8BGgdQyJFJ4TRZlc4uZBq98rbY4nw9lz43P7dfuZA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"composition","path":"gno.land/r/docs/composition","files":[{"name":"composition.gno","body":"// Package composition shows how to build a realm from smaller component realms.\n// It calls into the counter and logger sub-realms.\n// Each sub-realm restricts writes to this composition realm.\npackage composition\n\nimport (\n\t\"gno.land/r/docs/composition/counter\"\n\t\"gno.land/r/docs/composition/logger\"\n)\n\n// This realm demonstrates composition by combining functionality\n// from two sub-realms: a counter and a logger.\n\n// IncrementAndLog increments the counter and logs the action\nfunc IncrementAndLog(cur realm) {\n\tcounter.Increment(cross(cur))\n\tlogger.Log(cross(cur), \"Counter incremented to \"+counter.GetCountStr())\n}\n\n// DecrementAndLog decrements the counter and logs the action\nfunc DecrementAndLog(cur realm) {\n\tcounter.Decrement(cross(cur))\n\tlogger.Log(cross(cur), \"Counter decremented to \"+counter.GetCountStr())\n}\n\n// Reset resets the counter and clears logs\nfunc Reset(cur realm) {\n\tcounter.Reset(cross(cur))\n\tlogger.Clear(cross(cur))\n\tlogger.Log(cross(cur), \"System reset\")\n}\n\nfunc Render(_ string) string {\n\treturn `# Realm Composition\n\nAllows you to build complex realms by combining simpler ones,\nfollowing the \"separation of concerns\" principle.\n\n## Code Pattern\n\n` + \"```go\" + `\npackage mycomposite\n\nimport (\n    \"gno.land/r/component/a\"\n    \"gno.land/r/component/b\"\n)\n\nfunc ComposedAction(cur realm) {\n    // Pass cross(cur) when calling external realms\n    a.DoSomething(cross(cur))\n    b.DoSomethingElse(cross(cur))\n}\n` + \"```\" + `\n\n## Access Control Pattern\n\nTo prevent users from bypassing the composition realm and calling components directly,\neach component checks that the caller is the authorized composition realm:\n\n` + \"```go\" + `\n// In component realm\nconst allowedCaller = \"gno.land/r/my/composite\"\n\nfunc ModifyState(cur realm) {\n    if !cur.IsCurrent() {\n        panic(\"invalid realm\")\n    }\n    caller := cur.Previous()\n    if caller.PkgPath() != allowedCaller {\n        panic(\"unauthorized\")\n    }\n    // ... modify state\n}\n` + \"```\" + `\n\n**Alternative**: If components don't need their own persistent state,\nuse pure packages (` + \"`/p/...`\" + `) instead of realms (` + \"`/r/...`\" + `).\nPackages are stateless libraries that can only be imported, so all state\nlives in the parent realm, eliminating bypass concerns entirely.\n\n## This Example\n\nThis realm combines a counter and a logger. Each action increments or decrements\nthe counter while automatically logging the change.\n\nThe counter and logger can only be modified through this composition realm.\nDirect calls to ` + \"`counter.Increment()`\" + ` or ` + \"`logger.Log()`\" + ` will fail with an authorization error.\n\n## Components\n\n- **Counter** ([source](/r/docs/composition/counter))\n- **Logger** ([source](/r/docs/composition/logger))\n\n## Live Demo\n\n### Current Counter Value\n\n` + counter.RenderValue() + `\n\n### Activity Log\n\n` + logger.RenderLog() + `\n\n### Actions\n\n- [Increment and Log](/r/docs/composition$help\u0026func=IncrementAndLog)\n- [Decrement and Log](/r/docs/composition$help\u0026func=DecrementAndLog)\n- [Reset All](/r/docs/composition$help\u0026func=Reset)\n`\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/composition\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"SyMjZmEGnzSCl1X6V25CH33BgzeOvIS99DZbUZrIJm464nBxofJWjUqKQf8MzOJipNHwZbVGhXF6lDaHuRryEQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"crossing","path":"gno.land/r/docs/crossing","files":[{"name":"crossing.gno","body":"// Package crossing explains Gno's crossing mechanism.\n// Crossing tracks which realm is acting, and cross-calls move it from one realm to another.\npackage crossing\n\nimport (\n\t\"chain/runtime/unsafe\"\n)\n\nvar owner address\n\nfunc init(cur realm) {\n\t// init runs automatically only once on deployment. It can take a \"cur realm\"\n\t// handle, so \"owner\" is set from cur.Address(), the address of the realm\n\t// being deployed.\n\towner = cur.Address()\n}\n\n// PublicFunction's first parameter is a \"cur realm\", so the call crosses into\n// this realm. cur.Previous() is the idiomatic way to read the caller, but only\n// after cur.IsCurrent() confirms the handle is this frame's live realm.\nfunc PublicFunction(cur realm) string {\n\tif !cur.IsCurrent() {\n\t\tpanic(\"invalid realm\")\n\t}\n\tcaller := cur.Previous().Address()\n\treturn \"Called by: \" + caller.String()\n}\n\n// InternalHelper has no realm parameter, so the call does not cross. It runs\n// in the caller's context, like a pure package helper.\nfunc InternalHelper() string {\n\tcaller := unsafe.CurrentRealm().Address()\n\treturn \"I'm a helper function called by: \" + caller.String()\n}\n\n// InternalHelperWithRealm takes a realm but doesn't cross: only a first-position\n// realm crosses, so rlm is a plain value here. It lets shared code that can't be\n// a crossing function (e.g. a /p/ package) receive and verify the acting realm.\nfunc InternalHelperWithRealm(_ int, rlm realm) string {\n\t// The call didn't cross, so IsCurrent() holds for the caller's live realm\n\t// and rejects any other.\n\tif !rlm.IsCurrent() {\n\t\tpanic(\"rlm is not the current realm\")\n\t}\n\n\treturn \"I'm a helper function called by: \" + rlm.Address().String()\n}\n\n// GetOwner returns the owner address\nfunc GetOwner() address {\n\treturn owner\n}\n\nfunc Render(_ string) string {\n\treturn `# Crossing in Gno\n\nCrossing is how Gno tracks which realm is acting at any moment. Every realm is a\nsmart contract with its own storage, and a realm can only write to its own. When\none realm calls into another, the acting realm switches to the one being called.\nGno follows that switch so each change is saved to the realm that owns it.\n\n## What is Crossing?\n\nWhen Realm A calls a function that lives in Realm B, execution \"crosses\" from A\ninto B, and B becomes the realm that is acting. Inside that function, changes\nare saved to B's storage. Once it returns, the acting realm goes back to A.\n\nGno follows this chain of calls, so at any point the code can ask:\n\n- **Who started the transaction?** → ` + \"`unsafe.OriginCaller()`\" + `\n- **Who called me?** → ` + \"`unsafe.PreviousRealm()`\" + `\n- **Where am I?** → ` + \"`unsafe.CurrentRealm()`\" + `\n- **Whose state is being modified?** → Always the current realm's state\n\nTo let other realms cross into a function, give it a ` + \"`cur realm`\" + ` parameter.\n` + \"`cur`\" + ` is a handle to this realm's own context while the function runs:\n\n` + \"```go\" + `\nfunc ModifyState(cur realm, newValue string) {\n    state = newValue  // Modifies THIS realm's state\n}\n` + \"```\" + `\n\nThe parameter alone does not cross. The caller crosses by marking the call with\n` + \"`cross`\" + `, passing its own ` + \"`cur`\" + `:\n\n` + \"```go\" + `\nother.ModifyState(cross(cur), \"hello\")  // crosses into the \"other\" realm\n` + \"```\" + `\n\nCalling another realm's function without ` + \"`cross`\" + ` is rejected. The plain\n` + \"`fn(cur, ...)`\" + ` form is only for calls within the same realm, where nothing crosses.\n\n## The Call Chain\n\nWhen User → Realm A → Realm B → Realm C:\n\n` + \"```\" + `\n User Transaction                                            \n                                                             \n  User (EOA)                                                 \n    │                                                        \n    ▼ call                                                   \n  ┌─────────────┐  OriginCaller() = User                     \n  │   Realm A            │  PreviousRealm() = User (as realm)         \n  │                      │  CurrentRealm() = Realm A                  \n  └─────────────┘                                            \n    │                                                        \n    ▼ cross                                                  \n  ┌─────────────┐  OriginCaller() = User                     \n  │   Realm B            │  PreviousRealm() = Realm A                 \n  │                      │  CurrentRealm() = Realm B                  \n  └─────────────┘               \n    │                                                                                  \n    ▼ cross                                                  \n  ┌─────────────┐  OriginCaller() = User                     \n  │   Realm C            │  PreviousRealm() = Realm B                 \n  │                      │  CurrentRealm() = Realm C                  \n  └─────────────┘                                            \n                                                             \n` + \"```\" + `\n\n\n## Crossing in Packages (/p/)\n\nUnlike realms, packages (under ` + \"`/p/`\" + `) don't have their own persistent state.\nWhen a realm imports a package, any variables declared by that package are\nstored within the realm's state. This means each realm gets its\nown isolated instance of the package's data.\n\n## Crossing in [Tests](https://docs.gno.land/resources/gno-stdlibs/#testing)\n\n` + \"```go\" + `\nimport (\n    \"testing\"\n\n    \"gno.land/p/nt/testutils/v0\"\n)\n\nfunc TestWithCaller(t *testing.T) {\n    // Simulate PreviousRealm() being another realm\n    testing.SetRealm(testing.NewCodeRealm(\"gno.land/r/caller\"))\n\n    // Simulate OriginCaller() being an end user\n    testing.SetOriginCaller(testutils.TestAddress(\"user1\"))\n}\n` + \"```\" + `\n\n---\n\n### See also:\n- [Common Mistakes](/r/docs/mistakes) - OriginCaller vs cur.Previous()\n`\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/crossing\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"MvCCt6pLo0Zw9CHOrGahjhLEOiHFYG84sFjwhQpv6KNsjz7M5KBVINyWeBHrnjYl0CpXs7TpMzFhsLvszU9AaA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"events","path":"gno.land/r/docs/events","files":[{"name":"events.gno","body":"package events\n\nimport \"chain\"\n\nfunc Render(_ string) string {\n\tout := \"# Emitting events in Gno\\n\\n\"\n\n\tout += `Emitting events in blockchain systems is one of the ways to make off-chain life easier.\nTo emit an event, simply use the **Emit()** function found in the **std** package.\n\nThis function takes in string arguments as follows:\n\n`\n\tout += \"```\\nruntime.Emit(\\\"EventName\\\", \\\"key1\\\", \\\"value1\\\", \\\"key2\\\", \\\"value2\\\")\\n```\\n\"\n\tout += \"The function takes in a variadic number of key:value pairs after the event name.\\n\\n\"\n\tout += \"Events are stored in block results, and can be listened to via the [tx-indexer](https://github.com/gnolang/tx-indexer).\\n\\n\"\n\n\tout += \"Click [here](/r/docs/events$help\u0026func=Emit\u0026customEventValue=) to submit a transaction that will emit an event.\"\n\n\treturn out\n}\n\nfunc Emit(customEventValue string) {\n\tchain.Emit(\"ExampleEvent\", \"key\", customEventValue)\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/events\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"WTvP5GY9nezvRWgxSKkzM7EVvczPhstlae3DC/l5tmc+De/Zwk/xzpk3W0aFLuk/sWfOfyudn1k+1+ZnGGEj/Q=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"factory","path":"gno.land/r/docs/factory","files":[{"name":"factory.gno","body":"// Package factory demonstrates the factory pattern.\n// It creates different token types (fungible, non-fungible, soulbound) behind a single Token interface.\npackage factory\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n)\n\n// TokenType represents different types of tokens\ntype TokenType int\n\nconst (\n\tFungible TokenType = iota\n\tNonFungible\n\tSoulBound\n)\n\n// Token is the common interface for all token types\ntype Token interface {\n\tGetInfo() string\n\tGetType() string\n\tTransfer(to address, amount uint64) error\n}\n\n// FungibleToken implements divisible tokens (like coins)\ntype FungibleToken struct {\n\tname   string\n\tamount uint64\n\towner  address\n}\n\nfunc (f *FungibleToken) GetInfo() string {\n\treturn f.name + \": \" + strconv.FormatUint(f.amount, 10) + \" units\"\n}\n\nfunc (f *FungibleToken) GetType() string {\n\treturn \"Fungible\"\n}\n\nfunc (f *FungibleToken) Transfer(to address, amount uint64) error {\n\tif amount \u003e f.amount {\n\t\treturn errors.New(\"insufficient balance\")\n\t}\n\tf.amount -= amount\n\treturn nil\n}\n\n// NonFungibleToken implements unique tokens (like NFTs)\ntype NonFungibleToken struct {\n\tname  string\n\towner address\n}\n\nfunc (n *NonFungibleToken) GetInfo() string {\n\treturn \"NFT: \" + n.name\n}\n\nfunc (n *NonFungibleToken) GetType() string {\n\treturn \"Non-Fungible\"\n}\n\nfunc (n *NonFungibleToken) Transfer(to address, amount uint64) error {\n\tif amount != 1 {\n\t\treturn errors.New(\"NFTs can only be transferred as a whole (amount must be 1)\")\n\t}\n\tn.owner = to\n\treturn nil\n}\n\n// SoulBoundToken implements non-transferable tokens\ntype SoulBoundToken struct {\n\tname  string\n\towner address\n}\n\nfunc (s *SoulBoundToken) GetInfo() string {\n\treturn \"SoulBound: \" + s.name\n}\n\nfunc (s *SoulBoundToken) GetType() string {\n\treturn \"SoulBound\"\n}\n\nfunc (s *SoulBoundToken) Transfer(to address, amount uint64) error {\n\treturn errors.New(\"soulbound tokens cannot be transferred\")\n}\n\n// CreateToken is the factory function that creates different token types\nfunc CreateToken(tokenType TokenType, name string, owner address) Token {\n\tswitch tokenType {\n\tcase Fungible:\n\t\treturn \u0026FungibleToken{name: name, amount: 1000, owner: owner}\n\tcase NonFungible:\n\t\treturn \u0026NonFungibleToken{name: name, owner: owner}\n\tcase SoulBound:\n\t\treturn \u0026SoulBoundToken{name: name, owner: owner}\n\tdefault:\n\t\tpanic(\"unknown token type\")\n\t}\n}\n\n// Demo variables\nvar (\n\tlastToken Token\n)\n\n// CreateFungibleToken creates a fungible token\nfunc CreateFungibleToken(cur realm, name string) string {\n\tif !cur.IsCurrent() {\n\t\tpanic(\"invalid realm\")\n\t}\n\tcaller := cur.Previous().Address()\n\ttoken := CreateToken(Fungible, name, caller)\n\tlastToken = token\n\treturn \"Created: \" + token.GetInfo()\n}\n\n// CreateNFT creates a non-fungible token\nfunc CreateNFT(cur realm, name string) string {\n\tif !cur.IsCurrent() {\n\t\tpanic(\"invalid realm\")\n\t}\n\tcaller := cur.Previous().Address()\n\ttoken := CreateToken(NonFungible, name, caller)\n\tlastToken = token\n\treturn \"Created: \" + token.GetInfo()\n}\n\n// CreateSoulBound creates a soul-bound token\nfunc CreateSoulBound(cur realm, name string) string {\n\tif !cur.IsCurrent() {\n\t\tpanic(\"invalid realm\")\n\t}\n\tcaller := cur.Previous().Address()\n\ttoken := CreateToken(SoulBound, name, caller)\n\tlastToken = token\n\treturn \"Created: \" + token.GetInfo()\n}\n\nfunc Render(path string) string {\n\tout := `# Factory Pattern\n\nCreates different types of objects that share a common interface.\n\n## Code Pattern\n\n` + \"```go\" + `\n// Common interface\ntype Token interface {\n    GetInfo() string\n    GetType() string\n    Transfer(to address, amount uint64) error\n}\n\n// Concrete types\ntype FungibleToken struct { name string; amount uint64 }\ntype NonFungibleToken struct { name string }\ntype SoulBoundToken struct { name string }\n\n// Factory function\nfunc CreateToken(tokenType TokenType, name string) Token {\n    switch tokenType {\n    case Fungible:\n        return \u0026FungibleToken{name: name, amount: 1000}\n    case NonFungible:\n        return \u0026NonFungibleToken{name: name}\n    case SoulBound:\n        return \u0026SoulBoundToken{name: name}\n    default:\n        panic(\"unknown token type\")\n    }\n}\n\n// Work with interface, not concrete types\ntoken := CreateToken(Fungible, \"USDC\")\nif err := token.Transfer(recipientAddr, 100); err != nil {\n    panic(err)\n}\n` + \"```\" + `\n\n## Live Demo\n\n- [Create Fungible Token](/r/docs/factory$help\u0026func=CreateFungibleToken)\n- [Create NFT](/r/docs/factory$help\u0026func=CreateNFT)\n- [Create SoulBound Token](/r/docs/factory$help\u0026func=CreateSoulBound)\n\n`\n\n\tif lastToken != nil {\n\t\tout += \"### Last Created Token\\n\\n\"\n\t\tout += \"**Type:** \" + lastToken.GetType() + \"\\n\\n\"\n\t\tout += \"**Info:** \" + lastToken.GetInfo() + \"\\n\\n\"\n\t}\n\n\treturn out\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/factory\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"xUK7NFk1eVGi66y9zku/QLa54SygaBCU7AEBzgQIvekEle3W/Na2c+Z7VWygJAZjy1rrfVIbts/QxunkfT3sTA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"hello","path":"gno.land/r/docs/hello","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/hello\"\ngno = \"0.9\"\n"},{"name":"hello.gno","body":"// Package hello_world demonstrates basic usage of Render().\n// Try adding `:World` at the end of the URL, like `.../hello:World`.\npackage hello\n\n// Render outputs a greeting. It customizes the message based on the provided path.\nfunc Render(path string) string {\n\tif path == \"\" {\n\t\treturn \"# Hello, 世界！\"\n\t}\n\treturn \"# Hello, \" + path + \"!\"\n}\n"},{"name":"hello_test.gno","body":"package hello\n\nimport (\n\t\"testing\"\n)\n\nfunc TestHello(t *testing.T) {\n\texpected := \"# Hello, 世界！\"\n\tgot := Render(\"\")\n\tif got != expected {\n\t\tt.Fatalf(\"Expected %s, got %s\", expected, got)\n\t}\n\n\tgot = Render(\"world\")\n\texpected = \"# Hello, world!\"\n\tif got != expected {\n\t\tt.Fatalf(\"Expected %s, got %s\", expected, got)\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"dsoUaeVujEdKXuxlNg7JKVPcy+40abojwCsZmsGruk1DfHBSjGt6SwNt3Oibf3dNVgycbYSXHEDZvtwKl+YZZw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"home","path":"gno.land/r/docs/home","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/home\"\ngno = \"0.9\"\n"},{"name":"home.gno","body":"package home\n\nimport \"strings\"\n\nfunc Render(_ string) string {\n\tcontent := `# Gno Examples Documentation\n\nWelcome to the Gno examples documentation index.\nExplore various examples to learn more about Gno functionality and usage.\n\n## Getting Started\n- [Hello World](/r/docs/hello) - A simple introductory example.\n- [Adder](/r/docs/adder) - An interactive example to update a number with transactions.\n- [Source](/r/docs/source) - View realm source code.\n\n## Development \u0026 Testing\n- [Common Mistakes](/r/docs/mistakes) - Pitfalls to avoid: overflow, errors, OriginCaller attacks.\n- [Crossing](/r/docs/crossing) - Understanding realm boundary crossing.\n- [Calling non-primitive argument functions](/r/docs/complexargs) - Learn how to call functions that take in complex arguments.\n- [Test-Driven Development (TDD)](/r/docs/tdd) - Writing tests before code in Gno.\n\n## Data \u0026 Serialization\n- [AVL Pager](/r/docs/avl_pager) - Paginate through AVL tree items.\n- [AVL Pager + Render paths](/r/docs/avl_pager_params) - Handle render arguments with pagination.\n\n## Design Patterns\n- [Composition](/r/docs/composition) - Composing functionality from multiple realms.\n- [Factory Pattern](/r/docs/factory) - Create and manage multiple object instances.\n- [Registry Pattern](/r/docs/registry) - Register and discover entities in a central directory.\n- [Safe Objects](/r/docs/safeobjects) - A Gno-specific pattern allowing you to expose objects with admin privileges preserved.\n- [Common Solidity Patterns](/r/docs/soliditypatterns) - A list of common Solidity coding practices, converted to Gno equivalents.\n\n## Rendering \u0026 UI\n- [Buttons](/r/docs/buttons) - Add buttons to your realm's render.\n- [Transaction Links](/r/docs/txlink) - Create clickable transaction links in your realm's render!\n- [Optional Render](/r/docs/optional_render) - Render() is optional in realms.\n- [Routing for Render paths](/r/docs/routing) - Route Render paths with the ^p/demo/mux^ package.\n- [Embed images](/r/docs/img_embed) - Demonstrates how to embed an image in a realm render.\n- [Markdown](/r/docs/markdown) - Documentation for Gno Flavored Markdown syntax and features.\n- [p/moul/md Package Demo](/r/docs/moul_md) - Learn how to programmatically generate markdown using the p/moul/md package.\n- [Chart Packages Demo](/r/docs/charts) - Create charts to display data in your realm's render.\n\n## Users \u0026 Profiles\n- [Resolving usernames and addresses](/r/docs/resolveusers) - How to resolve usernames and addresses via the ^r/sys/users^ realm.\n- [User \u0026 Profile Integration](/r/docs/userprofile) - Using ^r/sys/users^ and ^r/demo/profile^ together.\n\n## Advanced Topics\n- [Emitting Gno Events](/r/docs/events) - Emit Gno Events to make life off-chain easier.\n- [Private Realms](/r/docs/private) - Documentation for private realms in Gno.\n- [Security Patterns](/r/docs/security_patterns) - Compact examples for authenticated mutators and safe Render output.\n- [MiniSocial](/r/docs/minisocial) - Minimalistic social media app for learning purposes.\n\n\u003c!-- meta issue with suggestions: https://github.com/gnolang/gno/issues/3292 --\u003e\n\n## Other resources\n\n- [Official documentation](https://docs.gno.land) \u003c!-- should be /docs with gnoweb embedding the docs/ folder. --\u003e\n`\n\treturn strings.ReplaceAll(content, \"^\", \"`\")\n}\n"},{"name":"home_test.gno","body":"package home\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestRenderHome(t *testing.T) {\n\toutput := Render(\"\")\n\n\t// Check for the presence of key sections\n\tif !contains(output, \"# Gno Examples Documentation\") {\n\t\tt.Errorf(\"Render output is missing the title.\")\n\t}\n\tif !contains(output, \"Official documentation\") {\n\t\tt.Errorf(\"Render output is missing the official documentation link.\")\n\t}\n}\n\nfunc contains(s, substr string) bool {\n\treturn strings.Index(s, substr) \u003e= 0\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"1RbPxh4cOerUJPK8y4HeklTotwfcKl1t2bBeReFwGw1QIKX0wKQZ5RVQ9oolK1eaCmjmkKkf0ogk4bJOiR8Hfw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"img_embed","path":"gno.land/r/docs/img_embed","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/img_embed\"\ngno = \"0.9\"\n"},{"name":"img_embed.gno","body":"package img_embed\n\n// Render displays a title and an embedded image from Imgur\nfunc Render(path string) string {\n\treturn `# Image Embed Example\n\nHere’s an example of embedding an image in a Gno realm:\n\n![Example Image](https://i.imgur.com/So4rBPB.jpeg)\n\nOnly a limited set of content providers is supported. Check the list out [here](/r/docs/markdown#images).`\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"Ktb4/2GGcLmiIEW8HQrkecLu7YmJOcMAmXlN9KYrUe40XDesr+oTAv/rotxa6CnQ+fi7vM6iX1rEPjs0d0PgpA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"markdown","path":"gno.land/r/docs/markdown","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/markdown\"\ngno = \"0.9\"\n"},{"name":"markdown.gno","body":"package markdown\n\nimport \"strings\"\n\n// this package can be used to test markdown rendering engines.\n\nfunc Render(path string) string {\n\toutput := `# Markdown on Gno\n\n## Introduction\n\nMarkdown on Gno is based on standard markdown, but also has some unique features, making it the Gno Flavored Markdown. This document describes the current markdown support in Gno, demonstrating both the syntax and its rendered output.\n\n\u003e [!NOTE]\n\u003e Markdown support in Gno is still evolving. New features and improvements will be added in future releases.\n\n## Basic Syntax\n\n### Headings\n\nHeadings are created using hash symbols (#). The number of hash symbols indicates the heading level.\n\n±±±markdown\n# Heading 1\n## Heading 2\n### Heading 3\n#### Heading 4\n##### Heading 5\n###### Heading 6\n±±±\n\n# Heading 1\n## Heading 2\n### Heading 3\n#### Heading 4\n##### Heading 5\n###### Heading 6\n\n### Text Formatting\n\nYou can format text using the following syntax:\n\n±±±markdown\n**Bold text**\n*Italic text*\n~~Strikethrough text~~\n**Bold and _nested italic_**\n***All bold and italic***\n±±±\n\n**Bold text**\n*Italic text*\n~~Strikethrough text~~\n**Bold and _nested italic_**\n***All bold and italic***\n\n### Links\n\nLinks can be created using the following syntax:\n\n±±±markdown\n[Link text](https://example.com)\n[Link with title](https://example.com \"Link title\")\n±±±\n\n[Link text](https://example.com)\n[Link with title](https://example.com \"Link title\")\n\n### Lists\n\nUnordered lists use asterisks, plus signs, or hyphens:\n\n±±±markdown\n* Item 1\n* Item 2\n  * Nested item 1\n  * Nested item 2\n±±±\n\n* Item 1\n* Item 2\n  * Nested item 1\n  * Nested item 2\n\nOrdered lists use numbers:\n\n±±±markdown\n1. First item\n2. Second item\n   1. Nested item 1\n   2. Nested item 2\n±±±\n\n1. First item\n2. Second item\n   1. Nested item 1\n   2. Nested item 2\n\n### Blockquotes\n\nBlockquotes are created using the \u003e character:\n\n±±±markdown\n\u003e This is a blockquote\n\u003e\n\u003e\u003e It can span multiple lines\n±±±\n\n\u003e This is a blockquote\n\u003e\n\u003e\u003e It can span multiple lines\n\n### Code\n\nInline code uses single backticks:\n\n±±±markdown\nUse ±func main()± to define the entry point.\n±±±\n\nUse ±func main()± to define the entry point.\n\nCode blocks use triple backticks with an optional language identifier:\n\n\u003c!-- XXX: make the example with 'gno' instead of 'go' --\u003e\n\n±±±markdown\n±±±go\npackage main\n\nfunc main() {\n    println(\"Hello, Gno!\")\n}\n±±±\n\n±±±go\npackage main\n\nfunc main() {\n    println(\"Hello, Gno!\")\n}\n±±±\n\n### Horizontal Rules\n\nHorizontal rules are created using three or more asterisks, hyphens, or underscores:\n\n±±±markdown\n---\n±±±\n\n---\n\n### Task Lists\n\nGno supports task lists for tracking to-do items:\n\n±±±markdown\n- [x] Completed task\n- [ ] Pending task\n- [ ] Another pending task\n- [x] Another completed task\n±±±\n\n- [x] Completed task\n- [ ] Pending task\n- [ ] Another pending task\n- [x] Another completed task\n\n### Footnotes\n\nGno supports footnotes for adding references and citations:\n\n±±±markdown\nHere is a sentence with a footnote[^1].\n\n[^1]: This is the footnote content.\n±±±\n\nHere is a sentence with a footnote[^1].\n\n[^1]: This is the footnote content.\n\nYou can also use multiple footnotes in the same document:\n\n±±±markdown\nThis is the first sentence with a footnote[^1].\nThis is another sentence with a different footnote[^2].\n\n[^1]: First footnote content.\n[^2]: Second footnote content with more details.\n±±±\n\nThis is the first sentence with a footnote[^1].\nThis is another sentence with a different footnote[^2].\n\n[^1]: First footnote content.\n[^2]: Second footnote content with more details.\n\n## Tables\n\nTables are created using pipes and hyphens:\n\n±±±markdown\n| Header 1 | Header 2 |\n| -------- | -------- |\n| Cell 1   | Cell 2   |\n| Cell 3   | Cell 4   |\n±±±\n\n| Header 1 | Header 2 |\n| -------- | -------- |\n| Cell 1   | Cell 2   |\n| Cell 3   | Cell 4   |\n\n## Images\n\nImages can be included using the following syntax:\n\n±±±markdown\n![Alt text](/public/imgs/gnoland.svg \"Optional title\")\n±±±\n\n![Alt text](/public/imgs/gnoland.svg \"Optional title\")\n\nCurrently, only a short list of content providers are supported:\n\n±±±markdown\n// Gno-related hosts\n\"https://gnolang.github.io\"\n\"https://sa.gno.services\"\n\n// Other providers should respect DMCA guidelines\n// NOTE: Feel free to open a PR to add more providers here :)\n\n// imgur\n\"https://imgur.com\"\n\"https://*.imgur.com\"\n\n// GitHub\n\"https://*.github.io\"\n\"https://github.com\"\n\"https://*.githubusercontent.com\"\n\n// IPFS\n\"https://ipfs.io\"\n\"https://cloudflare-ipfs.com\"\n±±±\n\n\n## Gno-Specific Features\n\n### HTML Support\n\nBy design, most typical HTML support is disabled in Gno's markdown implementation. This is an intentional decision for both security and ecosystem cohesion.\n\nWhile traditional markdown often allows arbitrary HTML tags, Gno Flavored Markdown takes a more controlled approach:\n\n- We may progressively whitelist certain HTML components or add custom ones over time\n- Our priority is to enhance our flavored markdown to natively support all essential components\n- We aim to eventually support all the initially HTML-supported features, but with syntax that is:\n  - More readable when viewing the source directly\n  - More integrable with custom browsers such as gnobro in CLI\n\nThis approach allows for a more consistent rendering experience across different Gno interfaces while maintaining security and readability as core principles.\n\n### Columns\n\nGno-Flavored Markdown introduces a column layout system that uses special tags. You can create columns with\n±\u003cgno-columns\u003e± blocks and separate the content with ±\u003cgno-columns-sep\u003e±.\n\n#### Basic Syntax\n±±±markdown\n\u003cgno-columns\u003e\nLeft content\n\n\u003cgno-columns-sep\u003e\n\nRight content\n\u003c/gno-columns\u003e\n±±±\n\n- Renders as:\n\n---\n\n\u003cgno-columns\u003e\nLeft content\n\n\u003cgno-columns-sep\u003e\n\nRight content\n\u003c/gno-columns\u003e\n\n---\n\n#### Key Features\n\n1. **Multiple Columns**:\n\nDepending on the screen size. A maximum of four rows can be displayed in\none row.\n\n***Note**: Any overflow will result in items being displayed in the next row. Also, keep in mind that on smaller screens\n(e.g., phones), this overflow may occur with fewer elements.*\n\n±±±markdown\n\u003cgno-columns\u003e\nFirst column\n\n\u003cgno-columns-sep\u003e\n\nSecond column\n\n\u003cgno-columns-sep\u003e\n\nThird column\n\n\u003cgno-columns-sep\u003e\n\nFourth column\n\n\u003cgno-columns-sep\u003e\n\nFifth column\n\u003c/gno-columns\u003e\n±±±\n\n- Renders as:\n\n---\n\n\u003cgno-columns\u003e\nFirst column\n\n\u003cgno-columns-sep\u003e\n\nSecond column\n\n\u003cgno-columns-sep\u003e\n\nThird column\n\n\u003cgno-columns-sep\u003e\n\nFourth column\n\n\u003cgno-columns-sep\u003e\n\nFifth column\n\u003c/gno-columns\u003e\n\n---\n\n2. **Mixed Content**:\n\nColumns can be mixed with any markdown content\n\n***Note**: Nested columns are currently not possible*\n\n±±±markdown\n\u003cgno-columns\u003e\n### Text Section\nParagraph with _formatting_\n\n\u003cgno-columns-sep\u003e\n\nMy Image ![Alt](/public/imgs/gnoland.svg)\n\u003c/gno-columns\u003e\n±±±\n\n- Renders as:\n\n---\n\n\u003cgno-columns\u003e\n### Text Section\nParagraph with _formatting_\n\n\u003cgno-columns-sep\u003e\n\nMy Image ![Alt](/public/imgs/gnoland.svg)\n\u003c/gno-columns\u003e\n\n---\n\n### Frames\n\nWrap any markdown in ±\u003cgno-frame\u003e± and ±\u003c/gno-frame\u003e± (each on its own line) to draw a thin rounded border\naround it, e.g. around a hero or a call to action.\n\n±±±markdown\n\u003cgno-frame\u003e\n## Build on gno.land\n\nWrite your first realm in minutes. [Get started](/r/docs/home)\n\u003c/gno-frame\u003e\n±±±\n\n- Renders as:\n\n---\n\n\u003cgno-frame\u003e\n## Build on gno.land\n\nWrite your first realm in minutes. [Get started](/r/docs/home)\n\u003c/gno-frame\u003e\n\n---\n\nA frame alone in a column renders as a card; cards in the same row share a height.\n\n±±±markdown\n\u003cgno-columns\u003e\n\u003cgno-frame\u003e\n**[Adena](https://adena.app)**\n\nBrowser wallet for gno.land.\n\u003c/gno-frame\u003e\n\u003cgno-columns-sep\u003e\n\u003cgno-frame\u003e\n**[Gnoscan](https://gnoscan.io)**\n\nBlock explorer for gno.land, with transactions, realms and accounts.\n\u003c/gno-frame\u003e\n\u003c/gno-columns\u003e\n±±±\n\n- Renders as:\n\n---\n\n\u003cgno-columns\u003e\n\u003cgno-frame\u003e\n**[Adena](https://adena.app)**\n\nBrowser wallet for gno.land.\n\u003c/gno-frame\u003e\n\u003cgno-columns-sep\u003e\n\u003cgno-frame\u003e\n**[Gnoscan](https://gnoscan.io)**\n\nBlock explorer for gno.land, with transactions, realms and accounts.\n\u003c/gno-frame\u003e\n\u003c/gno-columns\u003e\n\n---\n\nA frame can also hold a whole grid, cards included:\n\n±±±markdown\n\u003cgno-frame\u003e\n## Apps\n\u003cgno-columns\u003e\n\u003cgno-frame\u003e\n**[Adena](https://adena.app)**\n\u003c/gno-frame\u003e\n\u003cgno-columns-sep\u003e\n\u003cgno-frame\u003e\n**[Gnoscan](https://gnoscan.io)**\n\u003c/gno-frame\u003e\n\u003c/gno-columns\u003e\n\u003c/gno-frame\u003e\n±±±\n\n- Renders as:\n\n---\n\n\u003cgno-frame\u003e\n## Apps\n\u003cgno-columns\u003e\n\u003cgno-frame\u003e\n**[Adena](https://adena.app)**\n\u003c/gno-frame\u003e\n\u003cgno-columns-sep\u003e\n\u003cgno-frame\u003e\n**[Gnoscan](https://gnoscan.io)**\n\u003c/gno-frame\u003e\n\u003c/gno-columns\u003e\n\u003c/gno-frame\u003e\n\n---\n\n***Note**: Frames take no attributes and open only at the top level of the page or of a column: not inside a list, a quote, an alert or directly inside another frame. A grid inside a frame must close before the frame does; otherwise the frame ends just before the grid.*\n\n### Forms\n\nGno-Flavored Markdown introduces a secure form system that integrates directly with realms. The system uses custom HTML-like tags that are rendered with proper styling and validation.\n\n#### Basic Usage\n\nCreate a form using the ±\u003cgno-form\u003e± tag and add inputs with ±\u003cgno-input\u003e±:\n\n±±±markdown\n\u003cgno-form\u003e\n  \u003cgno-input name=\"name\" placeholder=\"Enter your name\" /\u003e\n  \u003cgno-input name=\"email\" placeholder=\"Enter your email\" /\u003e\n\u003c/gno-form\u003e\n±±±\n\n\u003cgno-form\u003e\n  \u003cgno-input name=\"name\" placeholder=\"Enter your name\" /\u003e\n  \u003cgno-input name=\"email\" placeholder=\"Enter your email\" /\u003e\n\u003c/gno-form\u003e\n\n#### Form Structure\n\n1. **Form Container**\n   - Must start with ±\u003cgno-form\u003e± and end with ±\u003c/gno-form\u003e±\n   - Optional attributes:\n     - ±path±: Render path after form submission (e.g. ±path=\"user\"± will redirect to ±:user?[params]±)\n     - ±exec±: Specify a realm function to execute (e.g. ±exec=\"CreatePost\"±).\n       When set, fields must be defined manually and their ±name± attributes must match the function's parameter names in the same order they appear in the function signature.\n   - Form data is always sent as query parameters\n   - Cannot be nested (forms inside forms are not allowed)\n   - Automatically includes a header showing the realm name\n\n2. **Input Fields**\n   - Created with ±\u003cgno-input\u003e± tag\n   - Attributes:\n     - ±name±: Unique identifier for the input (required)\n     - ±type±: Type of input (optional, defaults to \"text\")\n     - ±placeholder±: Hint text shown in the input (optional, defaults to \"Enter value\")\n     - ±value±: Default value for non-selectable inputs (optional)\n     - ±description±: Description of the input as title (optional - can be used as title for group of one or multiple inputs)\n     - ±required±: Set to \"true\" to mark the field as required (optional)\n     - ±readonly±: Set to \"true\" to make the field read-only (optional)\n   - Supported input types:\n     - ±type=\"text\"± (default): For text input\n     - ±type=\"number\"±: For numeric values only (with browser validation)\n     - ±type=\"email\"±: For email addresses (with basic browser validation)\n     - ±type=\"tel\"±: For phone numbers (no specific validation)\n     - ±type=\"password\"±: For password input (masked characters)\n     - ±type=\"radio\"±: For single selection from a group (requires ±value± attribute)\n     - ±type=\"checkbox\"±: For multiple selections (requires ±value± attribute)\n   - Additional attributes for radio/checkbox:\n     - ±value±: The value to submit when selected (required for radio/checkbox)\n     - ±checked±: Set to \"true\" to pre-select the option (optional)\n   - Note: Input validation is handled by the browser's HTML5 validation\n   - Any other type will default to \"text\"\n   - Each input must have a unique name\n   - Inputs are rendered with labels and proper styling\n\n3. **Textarea Fields**\n   - Created with ±\u003cgno-textarea\u003e± tag\n   - Required attributes:\n     - ±name±: Unique identifier for the textarea (required)\n     - ±placeholder±: Hint text shown in the textarea (optional, defaults to \"Enter value\")\n   - Optional attributes:\n     - ±value±: Default content for the textarea (optional)\n     - ±rows±: Number of visible rows (2-10, defaults to 4)\n     - ±required±: Set to \"true\" to mark the field as required (optional)\n     - ±readonly±: Set to \"true\" to make the field read-only (optional)\n   - Perfect for longer text input like messages, descriptions, or comments\n   - Each textarea must have a unique name\n   - Textareas are rendered with labels and proper styling\n\n4. **Select Fields**\n   - Created with ±\u003cgno-select\u003e± tag (similar to radio buttons)\n   - Required attributes:\n     - ±name±: Unique identifier for the select group (required). Use underscores to separate words.\n     - ±value±: The value to submit and display text (required)\n   - Optional attributes:\n     - ±description±: Description of the select group (optional)\n     - ±selected±: Set to \"true\" to pre-select the option (optional)\n     - ±required±: Set to \"true\" to mark the field as required (optional)\n   - Multiple ±\u003cgno-select\u003e± elements with the same ±name± form a select group\n   - All options with the same ±name± form a group where only one can be selected\n   - The ±value± attribute serves both as the submitted value and the displayed text\n\n#### Example Use Cases\n\n1. Form with Path and Query Parameters\n±±±markdown\n\u003cgno-form path=\"user\"\u003e\n  \u003cgno-input name=\"username\" type=\"text\" placeholder=\"Enter username\" /\u003e\n  \u003cgno-input name=\"age\" type=\"number\" placeholder=\"Your age\" /\u003e\n\u003c/gno-form\u003e\n±±±\n\n\u003cgno-form path=\"user\"\u003e\n  \u003cgno-input name=\"username\" type=\"text\" placeholder=\"Enter username\" /\u003e\n  \u003cgno-input name=\"age\" type=\"number\" placeholder=\"Your age\" /\u003e\n\u003c/gno-form\u003e\n\nThis form will submit to ±:user?username=value\u0026age=value± on the same realm.\n\n2. Form with Query Parameters Only\n±±±markdown\n\u003cgno-form\u003e\n  \u003cgno-input name=\"recipient\" type=\"text\" placeholder=\"Enter recipient address\" description=\"Recipient Information\" /\u003e\n  \u003cgno-input name=\"email\" type=\"email\" placeholder=\"Your email\" /\u003e\n  \u003cgno-input name=\"pswd\" type=\"password\" placeholder=\"Your password\" /\u003e\n\u003c/gno-form\u003e\n±±±\n\n\u003cgno-form\u003e\n  \u003cgno-input name=\"recipient\" type=\"text\" placeholder=\"Enter recipient address\" description=\"Recipient Information\" /\u003e\n  \u003cgno-input name=\"email\" type=\"email\" placeholder=\"Your email\" /\u003e\n  \u003cgno-input name=\"pswd\" type=\"password\" placeholder=\"Your password\" /\u003e\n\u003c/gno-form\u003e\n\nThis form will submit to ±?recipient=value\u0026email=value\u0026pswd=value± on the same realm.\n\n3. Form with Radio Buttons\n±±±markdown\n\u003cgno-form\u003e\n  \u003cgno-input name=\"color\" type=\"radio\" value=\"blue\" description=\"What is your favorite color?\" /\u003e\n  \u003cgno-input name=\"color\" type=\"radio\" value=\"red\" /\u003e\n  \u003cgno-input name=\"color\" type=\"radio\" value=\"green\" /\u003e\n\u003c/gno-form\u003e\n±±±\n\n\u003cgno-form\u003e\n  \u003cgno-input name=\"color\" type=\"radio\" value=\"blue\" description=\"What is your favorite color?\" /\u003e\n  \u003cgno-input name=\"color\" type=\"radio\" value=\"red\" /\u003e\n  \u003cgno-input name=\"color\" type=\"radio\" value=\"green\" /\u003e\n\u003c/gno-form\u003e\n\nRadio buttons allow users to select one option from a group. All radio buttons with the same ±name± form a group where only one can be selected.\n\n4. Form with Checkboxes\n±±±markdown\n\u003cgno-form\u003e\n  \u003cgno-input name=\"interests\" type=\"checkbox\" value=\"coding\" description=\"What do you like to do?\"/\u003e\n  \u003cgno-input name=\"interests\" type=\"checkbox\" value=\"gaming\" /\u003e\n  \u003cgno-input name=\"interests\" type=\"checkbox\" value=\"reading\" /\u003e\n  \u003cgno-input name=\"newsletter\" type=\"checkbox\" value=\"subscribe\" description=\"Subscribe to newsletter\" placeholder=\"To get the latest news\" checked=\"true\" /\u003e\n\u003c/gno-form\u003e\n±±±\n\n\u003cgno-form\u003e\n  \u003cgno-input name=\"interests\" type=\"checkbox\" value=\"coding\" description=\"What do you like to do?\" /\u003e\n  \u003cgno-input name=\"interests\" type=\"checkbox\" value=\"gaming\" /\u003e\n  \u003cgno-input name=\"interests\" type=\"checkbox\" value=\"reading\" /\u003e\n  \u003cgno-input name=\"newsletter\" type=\"checkbox\" value=\"subscribe\" description=\"Subscribe to newsletter\" placeholder=\"To get the latest news\" checked=\"true\" /\u003e\n\u003c/gno-form\u003e\n\nCheckboxes allow users to select multiple options. Use ±checked=\"true\"± to pre-select a checkbox.\n\n5. Form with Textarea\n±±±markdown\n\u003cgno-form\u003e\n  \u003cgno-input name=\"message\" type=\"text\" placeholder=\"Subject\" /\u003e\n  \u003cgno-textarea name=\"content\" placeholder=\"Enter your message here\" rows=\"6\" /\u003e\n\u003c/gno-form\u003e\n±±±\n\n\u003cgno-form\u003e\n  \u003cgno-input name=\"message\" type=\"text\" placeholder=\"Subject\" /\u003e\n  \u003cgno-textarea name=\"content\" placeholder=\"Enter your message here\" rows=\"6\" /\u003e\n\u003c/gno-form\u003e\n\nTextareas are perfect for longer text input. The ±rows± attribute controls the height (4-10 rows, default is 4).\n\n6. Form with Select Options\n±±±markdown\n\u003cgno-form\u003e\n  \u003cgno-select name=\"country\" value=\"United States\" description=\"Select your country\" /\u003e\n  \u003cgno-select name=\"country\" value=\"France\" /\u003e\n  \u003cgno-select name=\"country\" value=\"Germany\" /\u003e\n\u003c/gno-form\u003e\n±±±\n\n\u003cgno-form\u003e\n  \u003cgno-select name=\"country\" value=\"United States\" description=\"Select your country\" selected=\"true\" /\u003e\n  \u003cgno-select name=\"country\" value=\"France\" /\u003e\n  \u003cgno-select name=\"country\" value=\"Germany\" /\u003e\n\u003c/gno-form\u003e\n\nSelect options work like radio buttons but with a more semantic meaning. All ±\u003cgno-select\u003e± elements with the same ±name± form a group where only one can be selected. Use ±selected=\"true\"± to pre-select an option. The ±value± attribute serves both as the submitted value and the displayed text.\n\n7. Complex Form with Mixed Elements\n±±±markdown\n\u003cgno-form path=\"contact\"\u003e\n  \u003cgno-input name=\"name\" type=\"text\" placeholder=\"Your full name\" description=\"Your Information\" /\u003e\n  \u003cgno-input name=\"email\" type=\"email\" placeholder=\"Your email address\" /\u003e\n  \u003cgno-input name=\"age\" type=\"number\" placeholder=\"Your age\" /\u003e\n  \n  \u003cgno-input name=\"color\" type=\"radio\" value=\"blue\" description=\"What is your favorite color?\" /\u003e\n  \u003cgno-input name=\"color\" type=\"radio\" value=\"red\" /\u003e\n  \u003cgno-input name=\"color\" type=\"radio\" value=\"green\" /\u003e\n  \n  \u003cgno-input name=\"interests\" type=\"checkbox\" value=\"tech\" description=\"What do you like to do?\" /\u003e\n  \u003cgno-input name=\"interests\" type=\"checkbox\" value=\"sports\" /\u003e\n  \u003cgno-input name=\"interests\" type=\"checkbox\" value=\"music\"/\u003e\n  \n  \u003cgno-select name=\"country\" value=\"us\" text=\"United States\" description=\"Select your country\" /\u003e\n  \u003cgno-select name=\"country\" value=\"fr\" text=\"France\" /\u003e\n  \u003cgno-select name=\"country\" value=\"de\" text=\"Germany\" /\u003e\n  \n  \u003cgno-textarea name=\"message\" placeholder=\"Tell us about yourself\" rows=\"5\" /\u003e\n\u003c/gno-form\u003e\n±±±\n\n\u003cgno-form path=\"contact\"\u003e\n  \u003cgno-input name=\"name\" type=\"text\" placeholder=\"Your full name\" description=\"Your Information\" /\u003e\n  \u003cgno-input name=\"email\" type=\"email\" placeholder=\"Your email address\" /\u003e\n  \u003cgno-input name=\"age\" type=\"number\" placeholder=\"Your age\" /\u003e\n  \n  \u003cgno-input name=\"color\" type=\"radio\" value=\"blue\" description=\"What is your favorite color?\" /\u003e\n  \u003cgno-input name=\"color\" type=\"radio\" value=\"red\" /\u003e\n  \u003cgno-input name=\"color\" type=\"radio\" value=\"green\" /\u003e\n  \n  \u003cgno-input name=\"interests\" type=\"checkbox\" value=\"tech\" description=\"What do you like to do?\" /\u003e\n  \u003cgno-input name=\"interests\" type=\"checkbox\" value=\"sports\" /\u003e\n  \u003cgno-input name=\"interests\" type=\"checkbox\" value=\"music\"/\u003e\n  \n  \u003cgno-select name=\"country\" value=\"us\" text=\"United States\" description=\"Select your country\" /\u003e\n  \u003cgno-select name=\"country\" value=\"fr\" text=\"France\" /\u003e\n  \u003cgno-select name=\"country\" value=\"de\" text=\"Germany\" /\u003e\n  \n  \u003cgno-textarea name=\"message\" placeholder=\"Tell us about yourself\" rows=\"5\" /\u003e\n\u003c/gno-form\u003e\n\nThis example shows how to combine all form element types in a single form.\n\n8. Form with Default Values\n±±±markdown\n\u003cgno-form\u003e\n  \u003cgno-input name=\"username\" type=\"text\" placeholder=\"Enter username\" value=\"Alice\" /\u003e\n  \u003cgno-input name=\"age\" type=\"number\" placeholder=\"Your age\" value=\"42\" /\u003e\n  \u003cgno-textarea name=\"bio\" placeholder=\"Short bio\" rows=\"4\" value=\"Super builder\" /\u003e\n\u003c/gno-form\u003e\n±±±\n\n\u003cgno-form\u003e\n  \u003cgno-input name=\"username\" type=\"text\" placeholder=\"Enter username\" value=\"Alice\" /\u003e\n  \u003cgno-input name=\"age\" type=\"number\" placeholder=\"Your age\" value=\"42\" /\u003e\n  \u003cgno-textarea name=\"bio\" placeholder=\"Short bio\" rows=\"4\" value=\"Super builder\" /\u003e\n\u003c/gno-form\u003e\n\n9. Form with Readonly Fields\nThe ±readonly=\"true\"± attribute makes fields non-editable while displaying their values. This is useful for showing fixed values that users should see but cannot modify.\n\n±±±markdown\n\u003cgno-form\u003e\n  \u003cgno-input name=\"username\" value=\"alice\" readonly=\"true\" description=\"Your fixed username\" /\u003e\n  \u003cgno-input name=\"role\" type=\"text\" value=\"admin\" readonly=\"true\" /\u003e\n  \u003cgno-input name=\"status\" type=\"radio\" value=\"active\" checked=\"true\" readonly=\"true\" description=\"Account status\" /\u003e\n  \u003cgno-input name=\"status\" type=\"radio\" value=\"inactive\" readonly=\"true\" /\u003e\n  \u003cgno-input name=\"verified\" type=\"checkbox\" value=\"yes\" checked=\"true\" readonly=\"true\" /\u003e\n  \u003cgno-textarea name=\"terms\" value=\"Terms and conditions text...\" rows=\"3\" readonly=\"true\" /\u003e\n  \u003cgno-select name=\"plan\" value=\"premium\" selected=\"true\" readonly=\"true\" description=\"Your subscription\" /\u003e\n  \u003cgno-select name=\"plan\" value=\"basic\" readonly=\"true\" /\u003e\n\u003c/gno-form\u003e\n±±±\n\n\u003cgno-form\u003e\n  \u003cgno-input name=\"username\" value=\"alice\" readonly=\"true\" description=\"Your fixed username\" /\u003e\n  \u003cgno-input name=\"role\" type=\"text\" value=\"admin\" readonly=\"true\" /\u003e\n  \u003cgno-input name=\"status\" type=\"radio\" value=\"active\" checked=\"true\" readonly=\"true\" description=\"Account status\" /\u003e\n  \u003cgno-input name=\"status\" type=\"radio\" value=\"inactive\" readonly=\"true\" /\u003e\n  \u003cgno-input name=\"verified\" type=\"checkbox\" value=\"yes\" checked=\"true\" readonly=\"true\" /\u003e\n  \u003cgno-textarea name=\"terms\" value=\"Terms and conditions text...\" rows=\"3\" readonly=\"true\" /\u003e\n  \u003cgno-select name=\"plan\" value=\"premium\" selected=\"true\" readonly=\"true\" description=\"Your subscription\" /\u003e\n  \u003cgno-select name=\"plan\" value=\"basic\" readonly=\"true\" /\u003e\n\u003c/gno-form\u003e\n\n**Note**: For text-based inputs and textareas, the HTML ±readonly± attribute is used. For radio buttons, checkboxes, and select dropdowns, ±disabled± is used instead since ±readonly± doesn't work on these elements in HTML.\n\n10. Form with Required Fields\nThe ±required=\"true\"± attribute marks fields as mandatory. Required fields display a \"(required)\" badge next to their label and use HTML5 browser validation.\n\n±±±markdown\n\u003cgno-form\u003e\n  \u003cgno-input name=\"username\" placeholder=\"Enter username\" required=\"true\" /\u003e\n  \u003cgno-input name=\"email\" type=\"email\" placeholder=\"Your email\" required=\"true\" /\u003e\n  \u003cgno-textarea name=\"bio\" placeholder=\"Tell us about yourself\" rows=\"4\" required=\"true\" /\u003e\n  \u003cgno-select name=\"country\" value=\"USA\" required=\"true\" /\u003e\n  \u003cgno-select name=\"country\" value=\"France\" /\u003e\n  \u003cgno-select name=\"country\" value=\"Germany\" /\u003e\n\u003c/gno-form\u003e\n±±±\n\n\u003cgno-form\u003e\n  \u003cgno-input name=\"username\" placeholder=\"Enter username\" required=\"true\"/\u003e\n  \u003cgno-input name=\"email\" type=\"email\" placeholder=\"Your email\" required=\"true\" /\u003e\n  \u003cgno-textarea name=\"bio\" placeholder=\"Tell us about yourself\" rows=\"4\" required=\"true\" /\u003e\n  \u003cgno-select name=\"country\" value=\"USA\" required=\"true\" /\u003e\n  \u003cgno-select name=\"country\" value=\"France\" /\u003e\n  \u003cgno-select name=\"country\" value=\"Germany\" /\u003e\n\u003c/gno-form\u003e\n\nRequired fields are validated by the browser before submission. The form cannot be submitted until all required fields are filled.\n\n11. Exec Form with Manual Fields\nWhen using the ±exec± attribute, a command preview appears for the specified function. You must manually define form fields whose ±name± attributes match the function's parameter names, placed in the same order as they appear in the function signature.\n\n±±±markdown\n\u003cgno-form exec=\"CreatePost\"\u003e\n  \u003cgno-input name=\"title\" type=\"text\" placeholder=\"Post title\" /\u003e\n  \u003cgno-textarea name=\"content\" rows=\"5\" placeholder=\"Post content\"\u003e\u003c/gno-textarea\u003e\n  \u003cgno-input name=\"tags\" type=\"text\" placeholder=\"Comma-separated tags\" /\u003e\n\u003c/gno-form\u003e\n±±±\n\n\u003cgno-form exec=\"CreatePost\"\u003e\n  \u003cgno-input name=\"title\" type=\"text\" placeholder=\"Post title\" /\u003e\n  \u003cgno-textarea name=\"content\" rows=\"5\" placeholder=\"Post content\"\u003e\u003c/gno-textarea\u003e\n  \u003cgno-input name=\"tags\" type=\"text\" placeholder=\"Comma-separated tags\" /\u003e\n\u003c/gno-form\u003e\n\nThe command preview shows the full gnokey command with placeholders for each field value.\n\n12. Exec Form with All Field Types\nHere's an example demonstrating all supported field types in a single exec form:\n\n±±±markdown\n\u003cgno-form exec=\"CreateProfile\"\u003e\n  \u003cgno-input name=\"username\" placeholder=\"Your username\" description=\"Profile Information\" /\u003e\n  \u003cgno-input name=\"age\" type=\"number\" placeholder=\"Your age\" /\u003e\n  \u003cgno-textarea name=\"bio\" placeholder=\"Tell us about yourself\" rows=\"4\" /\u003e\n  \u003cgno-select name=\"country\" value=\"USA\" description=\"Select your country\" /\u003e\n  \u003cgno-select name=\"country\" value=\"France\" /\u003e\n  \u003cgno-select name=\"country\" value=\"Germany\" /\u003e\n  \u003cgno-input name=\"visibility\" type=\"radio\" value=\"public\" description=\"Profile visibility\" required=\"true\" /\u003e\n  \u003cgno-input name=\"visibility\" type=\"radio\" value=\"private\" /\u003e\n  \u003cgno-input name=\"notifications\" type=\"checkbox\" value=\"email\" description=\"Notifications\" checked=\"true\" /\u003e\n  \u003cgno-input name=\"notifications\" type=\"checkbox\" value=\"sms\" /\u003e\n\u003c/gno-form\u003e\n±±±\n\n\u003cgno-form exec=\"CreateProfile\"\u003e\n  \u003cgno-input name=\"username\" placeholder=\"Your username\" description=\"Profile Information\" value=\"alice\" /\u003e\n  \u003cgno-input name=\"age\" type=\"number\" placeholder=\"Your age\" /\u003e\n  \u003cgno-textarea name=\"bio\" placeholder=\"Tell us about yourself\" rows=\"4\" /\u003e\n  \u003cgno-select name=\"country\" value=\"USA\" description=\"Select your country\" /\u003e\n  \u003cgno-select name=\"country\" value=\"France\" /\u003e\n  \u003cgno-select name=\"country\" value=\"Germany\" /\u003e\n  \u003cgno-input name=\"visibility\" type=\"radio\" value=\"public\" description=\"Profile visibility\" required=\"true\" /\u003e\n  \u003cgno-input name=\"visibility\" type=\"radio\" value=\"private\" /\u003e\n  \u003cgno-input name=\"notifications\" type=\"checkbox\" value=\"email\" description=\"Notifications\" checked=\"true\" /\u003e\n  \u003cgno-input name=\"notifications\" type=\"checkbox\" value=\"sms\" /\u003e\n\u003c/gno-form\u003e\n\nAll field types update the command preview in real-time as you interact with them.\n\n#### Important Rules\n\n1. **Validation Rules**:\n   - Every ±\u003cgno-input\u003e± must have a ±name± attribute\n   - Every ±\u003cgno-textarea\u003e± must have a ±name± attribute\n   - Every ±\u003cgno-select\u003e± must have a ±name± and ±value± attribute\n   - Duplicate attribute names are not allowed (except for radio, checkbox, and select groups)\n   - Forms must be properly closed\n   - Nested forms will not render\n   - Radio and checkbox inputs must have a ±value± attribute\n   - The ±checked± attribute is only valid for radio and checkbox inputs\n   - The ±selected± attribute is only valid for ±\u003cgno-select\u003e± elements\n   - The ±readonly=\"true\"± attribute makes fields non-editable (works on all field types)\n   - The ±required=\"true\"± attribute marks fields as required (works on input, textarea, and select)\n   - Textarea ±rows± attribute must be between 2 and 10 (defaults to 4 if invalid or not specified)\n\n2. **Security Features**:\n   - Forms are processed on the realm where they are defined\n   - Each form submission is associated with its realm\n   - The realm name is displayed in the form header\n   - Input validation is handled by the realm's smart contract\n\n--- \n\n### Alerts\n\nAlerts are a way to highlight important information in your markdown.\n\nThere are five types of alerts:\n\n- INFO\n- NOTE\n- TIP\n- SUCCESS\n- WARNING\n- CAUTION\n\n**NOTE**: The default alert type is INFO (if a wrong alert type is used).\n\nThe alert boxes are expandable/collapsible and can have a title. \nBy default, the alert boxes are opened. The title is optional and if not provided, the alert type will be used as the title.\n\n±±±markdown\n\u003e [!NOTE]\n\u003e This is a note\n±±±\n\n\u003e [!NOTE]\n\u003e This is a note\n\n±±±markdown\n\u003e [!NOTE]-\n\u003e This is a closed note\n±±±\n\n\u003e [!NOTE]-\n\u003e This is a closed note\n\n±±±markdown\n\u003e [!NOTE] This is a note with title\n\u003e This is a note with title\n±±±\n\n\u003e [!NOTE] This is a note with title\n\u003e This is a note with title\n\n±±±markdown\n\u003e [!INFO]\n\u003e This is an info\n±±±\n\n\u003e [!INFO]\n\u003e This is an info\n\n±±±markdown\n\u003e [!TIP]\n\u003e This is a tip\n±±±\n\n\u003e [!TIP]\n\u003e This is a tip\n\n±±±\n\u003e [!SUCCESS]\n\u003e This is a success\n±±±\n\n\u003e [!SUCCESS]\n\u003e This is a success\n\n±±±\n\u003e [!WARNING]\n\u003e This is a warning\n±±±\n\n\u003e [!WARNING]\n\u003e This is a warning\n\n±±±\n\u003e [!CAUTION]\n\u003e This is a caution\n±±±\n\n\u003e [!CAUTION]\n\u003e This is a caution\n\n±±±markdown\n\u003e [!FOO]\n\u003e The default alert if a wrong alert type is used\n±±±\n\n\u003e [!FOO]\n\u003e This is the default alert\n\n±±±markdown\n\u003e [!NOTE]\n\u003e This is a note\n\u003e \u003e [!NOTE]\n\u003e \u003e This is a scoped note\n±±±\n\n\u003e [!NOTE]\n\u003e This is a note\n\u003e \u003e [!NOTE]\n\u003e \u003e This is a scoped note\n\n### Buttons\n\nA button is a link styled as a call to action. Write it as a single self-closing ±\u003cgno-button /\u003e± tag, on one line, with everything in attributes:\n\n- ±href± (required): the destination, resolved like a markdown link (internal, external, ±$help± transaction links). ±javascript:±, ±data:± and other unsafe URLs are rejected.\n- ±label± (required): the button text, shown as plain text (markdown inside it is not interpreted).\n- ±variant± (optional): ±outline±, and the alert kinds ±note±, ±tip±, ±caution±, ±warning±, ±success±, ±info±, space-separated. Unknown values are ignored.\n\n±±±markdown\n\u003cgno-button href=\"/r/gnoland/blog\" label=\"Read the blog\" /\u003e\n±±±\n\n\u003cgno-button href=\"/r/gnoland/blog\" label=\"Read the blog\" /\u003e\n\n±±±markdown\n\u003cgno-button href=\"/r/docs/home\" label=\"Default\" /\u003e\n\u003cgno-button href=\"/r/docs/home\" label=\"Outline\" variant=\"outline\" /\u003e\n\u003cgno-button href=\"/r/docs/home\" label=\"Caution\" variant=\"caution\" /\u003e\n\u003cgno-button href=\"/r/docs/home\" label=\"Warning\" variant=\"warning\" /\u003e\n\u003cgno-button href=\"/r/docs/home\" label=\"Info\" variant=\"info\" /\u003e\n\u003cgno-button href=\"/r/docs/home\" label=\"Note\" variant=\"note\" /\u003e\n\u003cgno-button href=\"/r/docs/home\" label=\"Tip\" variant=\"tip\" /\u003e\n\u003cgno-button href=\"/r/docs/home\" label=\"Success\" variant=\"success\" /\u003e\n\u003cgno-button href=\"/r/docs/home\" label=\"Caution outline\" variant=\"caution outline\" /\u003e\n±±±\n\n\u003cgno-button href=\"/r/docs/home\" label=\"Default\" /\u003e\n\u003cgno-button href=\"/r/docs/home\" label=\"Outline\" variant=\"outline\" /\u003e\n\u003cgno-button href=\"/r/docs/home\" label=\"Caution\" variant=\"caution\" /\u003e\n\u003cgno-button href=\"/r/docs/home\" label=\"Warning\" variant=\"warning\" /\u003e\n\u003cgno-button href=\"/r/docs/home\" label=\"Info\" variant=\"info\" /\u003e\n\u003cgno-button href=\"/r/docs/home\" label=\"Note\" variant=\"note\" /\u003e\n\u003cgno-button href=\"/r/docs/home\" label=\"Tip\" variant=\"tip\" /\u003e\n\u003cgno-button href=\"/r/docs/home\" label=\"Success\" variant=\"success\" /\u003e\n\u003cgno-button href=\"/r/docs/home\" label=\"Caution outline\" variant=\"caution outline\" /\u003e\n\nButtons are inline, so they also work inside text, lists, tables, alerts and columns:\n\n±±±markdown\nReady? \u003cgno-button href=\"https://gno.land\" label=\"Visit gno.land\" variant=\"outline\" /\u003e\n±±±\n\nReady? \u003cgno-button href=\"https://gno.land\" label=\"Visit gno.land\" variant=\"outline\" /\u003e\n\nInside a table cell, write a ±|± in an attribute as ±\u0026#124;± (for example ±label=\"A \u0026#124; B\"±): tables split cells on ±|± before the button is read.\n\nA tag that is not self-closing (±\u003cgno-button ...\u003e±), that misses ±href± or ±label±, or that is split over several lines is not a button: it is treated as raw HTML and removed. Buttons are not rendered inside ±\u003cgno-foreign\u003e± content.\n\n### User Mentions and Bech32 Addresses\n\nGno markdown automatically recognizes and renders user mentions and Bech32 addresses as clickable links.\n\n#### User Mentions\n\n// You can mention users using the \"@\" symbol followed by a username. The username can contain letters, numbers, and underscores.\n\n±±±markdown\nHello @alice, how are you doing?\n\nThis is a mention of @bob_user and @test-123.\n\nYou can also mention users like @user_name_123.\n±±±\n\n\nHello @alice, how are you doing?\n\nThis is a mention of @bob_user.\n\nYou can also mention users like @user_name_123.\n\n#### Bech32 Addresses\n\nGno markdown can automatically recognize and render valid Bech32 addresses as clickable links.\n\n±±±markdown\nThis is a GNO address: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n\nYou can also reference g1abc123def456ghi789jkl012mno345pqr678stu901vwx234yz5.\n±±±\n\nThis is a GNO address: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n\nYou can also reference g1abc123def456ghi789jkl012mno345pqr678stu901vwx234yz5.\n\n### Icons\n\nPut a small icon anywhere text goes (a heading, a paragraph, a list item, a table cell, a link label) with a\nself-closing ±\u003cgno-icon /\u003e± tag. It takes the size and color of the text around it.\n\n±±±markdown\n## \u003cgno-icon name=\"rocket\" /\u003e Launch\n\n- \u003cgno-icon name=\"check-circle\" label=\"Done\" /\u003e Audit\n- \u003cgno-icon name=\"clock\" label=\"Pending\" /\u003e Mainnet\n\n[\u003cgno-icon name=\"book\" /\u003e Read the docs](/r/docs)\n±±±\n\n- Renders as:\n\n---\n\n## \u003cgno-icon name=\"rocket\" /\u003e Launch\n\n- \u003cgno-icon name=\"check-circle\" label=\"Done\" /\u003e Audit\n- \u003cgno-icon name=\"clock\" label=\"Pending\" /\u003e Mainnet\n\n[\u003cgno-icon name=\"book\" /\u003e Read the docs](/r/docs)\n\n---\n\nAdd ±label=\"…\"± when the icon means something on its own (a status, a warning): screen readers announce it.\nLeave it out for a decorative icon. A link or a heading that holds only an icon needs the label: it is\ntheir only name, e.g. ±[\u003cgno-icon name=\"globe\" label=\"Website\" /\u003e](https://example.com)±. No other attribute\nis read.\n\nWrite the tag on one line, self-closing (±/\u003e±), with a lowercase name exactly as listed. A tag that is not\nself-closing, or whose name is unknown, renders nothing; the page source holds an HTML comment saying why.\nA page renders at most 1,000 icons.\n\nSome of the available names: ±star±, ±heart±, ±check-circle±, ±warning-triangle±, ±info±, ±wallet±, ±coin±,\n±key±, ±lock±, ±shield±, ±user±, ±users±, ±calendar±, ±clock±, ±mail±, ±globe±, ±rocket±, ±lightning±,\n±database±, ±server±, ±terminal±, ±branch±. All 491, with a picture of each, are in\n[ICONS.md](https://github.com/gnolang/gno/blob/master/gno.land/pkg/gnoweb/markdown/ICONS.md).\n\n### Smart Contract Integration\n\nXXX: TODO\n\n±±±markdown\ngno.land/r/boards\ngno.land/r/boards:foo/bar\ngno.land/r/docs/markdown$source\n±±±\n\ngno.land/r/boards\ngno.land/r/boards:foo/bar\ngno.land/r/docs/markdown$source\n\n### Mathematics\n\nSupports mathematical expressions using LaTeX syntax, which are rendered as MathML for proper mathematical display. An expression that cannot be converted is shown as its source, like inline code.\n\n#### Inline Math\n\nMathematical expressions can be written inline using single dollar signs:\n\n±±±markdown\nEinstein's famous equation is $E = mc^2$.\n±±±\n\nEinstein's famous equation is $E = mc^2$.\n\nGreek letters and symbols:\n\n±±±markdown\nGreek letters: $\\alpha, \\beta, \\gamma, \\Delta, \\Omega$\n\nMathematical operators: $+ - \\times \\div \\pm \\mp = \\neq \u003c \u003e \\leq \\geq$\n\nFunctions: $\\sin, \\cos, \\tan, \\log, \\ln, \\exp, \\max, \\min$\n±±±\n\nGreek letters: $\\alpha, \\beta, \\gamma, \\Delta, \\Omega$\n\nMathematical operators: $+ - \\times \\div \\pm \\mp = \\neq \u003c \u003e \\leq \\geq$\n\nFunctions: $\\sin, \\cos, \\tan, \\log, \\ln, \\exp, \\max, \\min$\n\nInline math can also be written between ±\\\\(± and ±\\\\)±:\n\n±±±markdown\nThe area of a circle is \\\\(\\pi r^2\\\\).\n±±±\n\nThe area of a circle is \\\\(\\pi r^2\\\\).\n\n#### Block Math\n\nFor larger mathematical expressions, use double dollar signs:\n\n±±±markdown\n$$\n\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}\n$$\n±±±\n\n$$\n\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}\n$$\n\nDisplay math can also be written between ±\\\\[± and ±\\\\]±, or in a code fence whose language is ±math±, as on GitHub:\n\n±±±±markdown\n\\\\[\ne^{i\\pi} + 1 = 0\n\\\\]\n\n±±±math\n\\sum_{k=1}^{n} k = \\frac{n(n+1)}{2}\n±±±\n±±±±\n\n\\\\[\ne^{i\\pi} + 1 = 0\n\\\\]\n\n±±±math\n\\sum_{k=1}^{n} k = \\frac{n(n+1)}{2}\n±±±\n\nA ±math± fence that cannot be converted stays a code block.\n\n#### Dollar Signs\n\nA single ±$± opens inline math only when the next character is not a space, and closes it only when the previous character is not a space and the next one is not a digit. Prices therefore stay text:\n\n±±±markdown\nIt costs $5, or $ 10 with shipping, and $5 and $10 together.\n±±±\n\nIt costs $5, or $ 10 with shipping, and $5 and $10 together.\n\nThe rule cannot tell every price from math: in ±$GNOT/$ATOM± the second ±$± follows a non-space, so the text between the two is read as math. Write ±\\$± for a dollar sign that must stay text:\n\n±±±markdown\nThe pair \\$GNOT/\\$ATOM, and a literal dollar in math: $\\$5$.\n±±±\n\nThe pair \\$GNOT/\\$ATOM, and a literal dollar in math: $\\$5$.\n\n#### Lines That Start a Block\n\nDisplay math ends at a line that would start another Markdown block, such as a list item (±- ±, ±+ ±, ±1. ±), a quote (±\u003e ±) or a heading (±#±), so that an unclosed ±$$± cannot swallow the rest of the page. A formula line that begins with a minus sign followed by a space is such a line; put an empty group ±{}± in front of it:\n\n±±±markdown\n$$\na\n{}- b\n$$\n±±±\n\n$$\na\n{}- b\n$$\n\n#### Colours\n\n±\\color± and ±\\textcolor± accept the theme colours ±red±, ±orange±, ±green±, ±blue±, ±purple± and ±gray± (or ±grey±), which stay readable in light and dark mode. Other colour names and hex or RGB values are ignored, and the expression keeps the text colour. ±\\color± colours everything after it in its group, so put it in braces to colour one term; ±\\textcolor± takes the term as its argument:\n\n±±±markdown\n${\\color{red} a^2} + {\\color{blue} b^2} = \\textcolor{green}{c^2}$\n±±±\n\n${\\color{red} a^2} + {\\color{blue} b^2} = \\textcolor{green}{c^2}$\n\n#### Advanced Examples\n\nMatrices:\n\n±±±markdown\n$$\n\\begin{pmatrix}\na \u0026 b \\\\\nc \u0026 d\n\\end{pmatrix}\n\\begin{pmatrix}\nx \\\\\ny\n\\end{pmatrix} =\n\\begin{pmatrix}\nax + by \\\\\ncx + dy\n\\end{pmatrix}\n$$\n±±±\n\n$$\n\\begin{pmatrix}\na \u0026 b \\\\\nc \u0026 d\n\\end{pmatrix}\n\\begin{pmatrix}\nx \\\\\ny\n\\end{pmatrix} =\n\\begin{pmatrix}\nax + by \\\\\ncx + dy\n\\end{pmatrix}\n$$\n\nComplex equations with Greek letters:\n\n±±±markdown\n$$\n\\nabla \\times \\vec{E} = -\\frac{\\partial \\vec{B}}{\\partial t}\n$$\n±±±\n\n$$\n\\nabla \\times \\vec{E} = -\\frac{\\partial \\vec{B}}{\\partial t}\n$$\n\nFractions and roots:\n\n±±±markdown\nFractions: $\\frac{a}{b}$, $\\frac{x^2 + y^2}{z^3}$\n\nSquare roots: $\\sqrt{x}$, $\\sqrt[3]{x}$, $\\sqrt[n]{x}$\n\nSuperscripts and subscripts: $x^2$, $x_1$, $x^{2+3}$, $x_{i+1}$\n±±±\n\nFractions: $\\frac{a}{b}$, $\\frac{x^2 + y^2}{z^3}$\n\nSquare roots: $\\sqrt{x}$, $\\sqrt[3]{x}$, $\\sqrt[n]{x}$\n\nSuperscripts and subscripts: $x^2$, $x_1$, $x^{2+3}$, $x_{i+1}$\n\n#### More complex examples:\n\nCharacteristic polynomial: $\\det(A - \\lambda I) = 0$\n\nEigenvalue equation: $A\\mathbf{v} = \\lambda\\mathbf{v}$\n\nSchrödinger equation (time-independent): $\\hat{H}|\\psi\\rangle = E|\\psi\\rangle$\n\nDirac equation: $(i\\gamma^\\mu\\partial_\\mu - m)\\psi = 0$\n\nKlein-Gordon equation: $(\\partial_\\mu\\partial^\\mu + m^2)\\phi = 0$\n\nMaxwell equations in tensor form: $\\partial_\\mu F^{\\mu\\nu} = \\mu_0 J^\\nu$\n\nEinstein field equations: $R_{\\mu\\nu} - \\frac{1}{2}Rg_{\\mu\\nu} + \\Lambda g_{\\mu\\nu} = \\frac{8\\pi G}{c^4}T_{\\mu\\nu}$\n\nSchwarzschild metric: $ds^2 = -\\left(1 - \\frac{2GM}{c^2r}\\right)dt^2 + \\left(1 - \\frac{2GM}{c^2r}\\right)^{-1}dr^2 + r^2(d\\theta^2 + \\sin^2\\theta d\\phi^2)$\n\nBoltzmann distribution: $P(E) = \\frac{1}{Z}e^{-\\beta E}$\n\nPartition function: $Z = \\sum_i e^{-\\beta E_i}$\n\nBose-Einstein distribution: $f(E) = \\frac{1}{e^{\\beta(E-\\mu)} - 1}$\n\nFirst law: $dU = \\delta Q - \\delta W$\n\nSecond law: $dS \\geq \\frac{\\delta Q}{T}$\n\n#### Supported Features\n\n- **Fractions**: ±\\frac{numerator}{denominator}±\n- **Superscripts and subscripts**: ±x^2±, ±x_1±, ±x^{2+3}±, ±x_{i+1}±\n- **Square roots**: ±\\sqrt{x}±, ±\\sqrt[n]{x}±\n- **Greek letters**: ±\\alpha±, ±\\beta±, ±\\gamma±, ±\\Delta±, ±\\Omega±, etc.\n- **Mathematical operators**: ±\\times±, ±\\div±, ±\\pm±, ±\\mp±, ±\\leq±, ±\\geq±, ±\\neq±, ±\\approx±, ±\\equiv±\n- **Arrows**: ±\\rightarrow±, ±\\leftarrow±, ±\\leftrightarrow±, ±\\Rightarrow±, ±\\Leftarrow±, ±\\Leftrightarrow±\n- **Set theory**: ±\\in±, ±\\notin±, ±\\subset±, ±\\supset±, ±\\cup±, ±\\cap±, ±\\emptyset±\n- **Logical operators**: ±\\land±, ±\\lor±, ±\\lnot±, ±\\forall±, ±\\exists±\n- **Integrals and sums**: ±\\int±, ±\\sum±, ±\\prod±, ±\\lim±\n- **Functions**: ±\\sin±, ±\\cos±, ±\\tan±, ±\\log±, ±\\ln±, ±\\exp±, ±\\max±, ±\\min±\n- **Matrices**: ±\\begin{matrix}...\\end{matrix}±, ±\\begin{pmatrix}...\\end{pmatrix}±\n- **Colours**: ±{\\color{blue} ...}±, ±\\textcolor{red}{...}±, theme colours only\n- **Operator names**: ±\\operatorname{argmax}±, ±\\operatorname*{argmax}_x± (limits below), ±\\mathop{...}±\n- **Spacing**: ±\\,±, ±\\:±, ±\\\u003e±, ±\\;±, ±\\quad±, ±\\qquad±, ±\\kern1em±, ±\\mkern18mu±, ±\\hspace{1em}±, ±\\phantom{x}±\n- **Tags**: ±\\tag{1}± is shown after the formula as written; ±\\label± is accepted and ignored\n- **Not supported**: ±\\ref± and ±\\eqref± (there is no equation numbering), and macro expansion (±\\newcommand± and ±\\def± are accepted but their definitions are not expanded)\n- **Complex equations**: Schrödinger, Maxwell, Fourier transforms, etc.\n\n### And more...\n\nXXX: TODO\n\n## Future Enhancements\n\nThe markdown support in Gno is being actively developed. Future enhancements may include:\n\n- Extended support for custom components\n- Interactive elements specific to blockchain functions\n- Rich rendering of on-chain data\n- Better integration with Gno's package system\n\n[Read more](https://github.com/gnolang/gno/issues/3255)\n\n## Conclusion\n\nMarkdown on Gno provides a familiar syntax for developers who have experience with GitHub Flavored Markdown, while adding blockchain-specific extensions that make it more powerful in the context of the Gno platform.\n\nAs the Gno ecosystem grows, expect the markdown capabilities to expand accordingly, providing even richer formatting and interactive elements for documentation and user interfaces.\n\n## See Also\n\nFor programmatic markdown generation using Go code, check out the [±p/moul/md± demo](/r/docs/moul_md) which demonstrates how to use the ±p/moul/md± package to generate markdown content in your Gno realms.\n`\n\toutput = strings.ReplaceAll(output, \"±\", \"`\")\n\treturn output\n}\n"},{"name":"markdown_test.gno","body":"package markdown\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestRender(t *testing.T) {\n\toutput := Render(\"\")\n\tif !strings.Contains(output, \"Gno Flavored Markdown\") {\n\t\tt.Errorf(\"invalid output\")\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"b166dJQCvCbJV3YQLeHSkLlcEpycCJgkhy7HO9TXkAAkZsi1NuByD/lP2dRiHhrXdYuLMfnptM1nKUnWX1xDlg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"minisocial","path":"gno.land/r/docs/minisocial","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/minisocial\"\ngno = \"0.9\"\n"},{"name":"minisocial.gno","body":"package minisocial\n\nfunc Render(_ string) string {\n\treturn `# MiniSocial\nMiniSocial is a minimalistic social media platform made for example purposes. \n\nThere are two versions of this app:\n- [V1](/r/docs/minisocial/v1) - handles simple post creation and stores posts in a slice\n- [V2](/r/docs/minisocial/v2) - handles post creation, updating, and deletion, \nand manages storage more efficiently with an AVL tree. V2 also utilizes different p/ packages to handle pagination, \neasier Markdown formatting, etc.`\n\n}\n\n// Original work \u0026 inspiration here:\n// https://gno.land/r/leon/fosdem25/microposts\n// https://gno.land/r/moul/microposts\n// Find the full tutorial on the official gno.land docs at docs.gno.land\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"pTRSxOj0Q8hdgpgNlG+s+DWzqTbKYKVLhfntqKS3fMcTX55S7/28FGV4Bq6gS6tiQ0x6LioL02kh6pQDV1Ll3w=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"minisocial","path":"gno.land/r/docs/minisocial/v1","files":[{"name":"admin.gno","body":"package minisocial\n\nimport \"gno.land/p/nt/ownable/v0\"\n\nvar Ownable = ownable.NewWithAddress(\"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\") // @leohhhn\n\n// ResetPosts allows admin deletion of the posts\nfunc ResetPosts(cur realm) {\n\tOwnable.AssertOwnedBy(cur.Previous().Address())\n\tposts = nil\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/minisocial/v1\"\ngno = \"0.9\"\n"},{"name":"posts.gno","body":"package minisocial\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"gno.land/p/moul/helplink/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nvar posts []*Post // inefficient for large amounts of posts; see v2\n\n// CreatePost creates a new post\nfunc CreatePost(cur realm, text string) error {\n\t// If the body of the post is empty, return an error\n\tif text == \"\" {\n\t\treturn errors.New(\"empty post text\")\n\t}\n\n\t// Append the new post to the list\n\tposts = append(posts, \u0026Post{\n\t\ttext:      text,                     // Set the input text\n\t\tauthor:    cur.Previous().Address(), // The author of the address is the previous realm, the realm that called this one\n\t\tcreatedAt: time.Now(),               // Capture the time of the transaction, in this case the block timestamp\n\t})\n\n\treturn nil\n}\n\nfunc Render(_ string) string {\n\toutput := \"# MiniSocial\\n\\n\" // \\n is needed just like in standard Markdown\n\t// Create a clickable link to create a post\n\toutput += helplink.Func(\"Write a post!\", \"CreatePost\", \"text\", \"\")\n\toutput += \"\\n\\n\"\n\n\t// Handle the edge case\n\tif len(posts) == 0 {\n\t\toutput += \"No posts.\\n\"\n\t\treturn output\n\t}\n\n\t// Let's append the text of each post to the output\n\tfor i, post := range posts {\n\t\t// Let's append some post metadata\n\t\toutput += ufmt.Sprintf(\"#### Post #%d\\n\\n\", i)\n\t\t// Add the stringified post\n\t\toutput += post.String()\n\t\t// Add a line break for cleaner UI\n\t\toutput += \"---\\n\\n\"\n\t}\n\n\treturn output\n}\n"},{"name":"posts_test.gno","body":"package minisocial\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\" // Provides testing utilities\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nfunc TestCreatePostSingle(cur realm, t *testing.T) {\n\t// Get a test address for alice\n\taliceAddr := testutils.TestAddress(\"alice\")\n\t// TestSetRealm sets the realm caller, in this case Alice\n\ttesting.SetRealm(testing.NewUserRealm(aliceAddr))\n\n\ttext1 := \"Hello World!\"\n\terr := CreatePost(cross(cur), text1)\n\tuassert.True(t, err == nil, \"expected no error\")\n\n\t// Get the rendered page\n\tgot := Render(\"\")\n\n\t// Content should have the text and alice's address in it\n\tcondition := strings.Contains(got, text1) \u0026\u0026 strings.Contains(got, aliceAddr.String())\n\tuassert.True(t, condition, \"expected render to contain text \u0026 alice's address\")\n}\n\nfunc TestCreatePostMultiple(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(testutils.TestAddress(\"alice\")))\n\n\t// Initialize a slice to hold the test posts and their authors\n\tposts := []struct {\n\t\ttext   string\n\t\tauthor string\n\t}{\n\t\t{\"Hello World!\", \"alice\"},\n\t\t{\"This is some new text!\", \"bob\"},\n\t\t{\"Another post by alice\", \"alice\"},\n\t\t{\"A post by charlie!\", \"charlie\"},\n\t}\n\n\tfor _, p := range posts {\n\t\t// Set the appropriate caller realm based on the author\n\t\tauthorAddr := testutils.TestAddress(p.author)\n\t\ttesting.SetRealm(testing.NewUserRealm(authorAddr))\n\n\t\t// Create the post\n\t\terr := CreatePost(cross(cur), p.text)\n\t\tuassert.True(t, err == nil, \"expected no error for post \"+p.text)\n\t}\n\n\t// Get the rendered page\n\tgot := Render(\"\")\n\n\t// Check that all posts and their authors are present in the rendered output\n\tfor _, p := range posts {\n\t\texpectedText := p.text\n\t\texpectedAuthor := testutils.TestAddress(p.author).String() // Get the address for the author\n\t\tcondition := strings.Contains(got, expectedText) \u0026\u0026 strings.Contains(got, expectedAuthor)\n\t\tuassert.True(t, condition, \"expected render to contain text '\"+expectedText+\"' and address '\"+expectedAuthor+\"'\")\n\t}\n}\n"},{"name":"types.gno","body":"package minisocial\n\nimport (\n\t// The standard Gno package\n\t\"time\" // For handling time operations\n\n\t\"gno.land/p/nt/ufmt/v0\" // For string formatting, like `fmt`\n)\n\n// Post defines the main data we keep about each post\ntype Post struct {\n\ttext      string    // Main text body\n\tauthor    address   // Address of the post author, provided by the execution context\n\tcreatedAt time.Time // When the post was created\n}\n\n// String stringifies a Post\nfunc (p Post) String() string {\n\tout := p.text\n\tout += \"\\n\\n\"\n\tout += ufmt.Sprintf(\"_by %s_, \", p.author)\n\t// We can use `ufmt` to format strings, and the built-in time library formatting function\n\tout += ufmt.Sprintf(\"_on %s_\", p.createdAt.Format(\"02 Jan 2006, 15:04\"))\n\n\tout += \"\\n\\n\"\n\treturn out\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"fvbjZT1fuMoBQHnUefbV4aF0eYdP1Gp/thddZ62FQlBvYEXB722xEuWc3afVWMO2l3bcJnyW7HMTCiFwEwGc8w=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"minisocial","path":"gno.land/r/docs/minisocial/v2","files":[{"name":"admin.gno","body":"package minisocial\n\nimport (\n\t\"gno.land/p/nt/avl/pager/v0\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/ownable/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n)\n\nvar Ownable = ownable.NewWithAddress(\"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\") // @leohhhn\n\n// ResetPosts allows admin deletion of the posts\nfunc ResetPosts(cur realm) {\n\tOwnable.AssertOwnedBy(cur.Previous().Address())\n\tposts = avl.NewTree()\n\tpostID = seqid.ID(0)\n\tpag = pager.NewPager(posts, 5, true)\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/minisocial/v2\"\ngno = \"0.9\"\n"},{"name":"posts.gno","body":"package minisocial\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/nt/avl/pager/v0\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\n\t\"gno.land/r/sys/users\"\n)\n\nvar (\n\tpostID seqid.ID                         // counter for post IDs\n\tposts  = avl.NewTree()                  // seqid.ID.String() \u003e *Post\n\tpag    = pager.NewPager(posts, 5, true) // To help with pagination in rendering\n\n\t// Errors\n\tErrEmptyPost           = errors.New(\"empty post text\")\n\tErrPostNotFound        = errors.New(\"post not found\")\n\tErrUpdateWindowExpired = errors.New(\"update window expired\")\n\tErrUnauthorized        = errors.New(\"you're not authorized to update this post\")\n)\n\n// CreatePost creates a new post\nfunc CreatePost(cur realm, text string) error {\n\tif text == \"\" {\n\t\treturn ErrEmptyPost\n\t}\n\n\t// Get the next ID\n\t// seqid.IDs are sequentially stored in the AVL tree\n\t// This provides chronological order when iterating\n\tid := postID.Next()\n\n\t// Set the key:value pair into the AVL tree:\n\t// avl.Tree.Set takes a string for a key, and anything as a value.\n\t// Stringify the key, and set the pointer to a new Post struct\n\tposts.Set(id.String(), \u0026Post{\n\t\tid:        id,                       // Set the ID, used later for editing or deletion\n\t\ttext:      text,                     // Set the input text\n\t\tauthor:    cur.Previous().Address(), // The author of the address is the previous realm, the realm that called this one\n\t\tcreatedAt: time.Now(),               // Capture the time of the transaction, in this case the block timestamp\n\t\tupdatedAt: time.Now(),\n\t})\n\n\treturn nil\n}\n\n// UpdatePost allows the author to update a post\n// The post can only be updated up to 10 minutes after posting\nfunc UpdatePost(cur realm, id string, text string) error {\n\t// Try to get the post\n\traw := posts.Get(id)\n\tif raw == nil {\n\t\treturn ErrPostNotFound\n\t}\n\n\t// Cast post from AVL tree\n\tpost := raw.(*Post)\n\tif cur.Previous().Address() != post.author {\n\t\treturn ErrUnauthorized\n\t}\n\n\t// Can only update 10 mins after it was posted\n\tif post.updatedAt.After(post.createdAt.Add(time.Minute * 10)) {\n\t\treturn ErrUpdateWindowExpired\n\t}\n\n\tpost.text = text\n\tpost.updatedAt = time.Now()\n\n\treturn nil\n}\n\n// DeletePost deletes a post with a specific id\n// Only the creator of a post can delete the post\nfunc DeletePost(cur realm, id string) error {\n\t// Try to get the post\n\traw := posts.Get(id)\n\tif raw == nil {\n\t\treturn ErrPostNotFound\n\t}\n\n\t// Cast post from AVL tree\n\tpost := raw.(*Post)\n\tif cur.Previous().Address() != post.author {\n\t\treturn ErrUnauthorized\n\t}\n\n\t// Use avl.Tree.Remove\n\t_, removed := posts.Remove(id)\n\tif !removed {\n\t\t// This shouldn't happen after all checks above\n\t\t// If it does, discard any possible state changes\n\t\tpanic(\"failed to remove post\")\n\t}\n\n\treturn nil\n}\n\n// Render renders the main page of threads\nfunc Render(path string) string {\n\tout := md.H1(\"MiniSocial\")\n\tif posts.Size() == 0 {\n\t\tout += \"No posts yet!\\n\\n\"\n\t\treturn out\n\t}\n\n\t// Get the page from the path\n\tpage := pag.MustGetPageByPath(path)\n\n\t// Iterate over items in the page\n\tfor _, item := range page.Items {\n\t\tpost := item.Value.(*Post)\n\n\t\t// Try resolving the address for a username\n\t\ttext := post.author.String()\n\t\tuser := users.ResolveAddress(post.author)\n\t\tif user != nil {\n\t\t\ttext = user.RenderLink(\"\")\n\t\t}\n\n\t\tout += md.H4(ufmt.Sprintf(\"Post #%d - %s\\n\\n\", int(post.id), text))\n\n\t\tout += post.String()\n\t\tout += md.HorizontalRule()\n\t}\n\n\tout += page.Picker(path)\n\tout += \"\\n\\n\"\n\tout += \"Page \" + strconv.Itoa(page.PageNumber) + \" of \" + strconv.Itoa(page.TotalPages) + \"\\n\\n\"\n\n\treturn out\n}\n"},{"name":"posts_test.gno","body":"package minisocial\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\" // Provides testing utilities\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nfunc TestCreatePostSingle(cur realm, t *testing.T) {\n\t// Get a test address for alice\n\taliceAddr := testutils.TestAddress(\"alice\")\n\t// TestSetRealm sets the realm caller, in this case Alice\n\ttesting.SetRealm(testing.NewUserRealm(aliceAddr))\n\n\ttext1 := \"Hello World!\"\n\terr := CreatePost(cross(cur), text1)\n\tuassert.True(t, err == nil, \"expected no error\")\n\n\t// Get the rendered page\n\tgot := Render(\"\")\n\n\t// Content should have the text and alice's address in it\n\tcondition := strings.Contains(got, text1) \u0026\u0026 strings.Contains(got, aliceAddr.String())\n\tuassert.True(t, condition, \"expected render to contain text \u0026 alice's address\")\n}\n\nfunc TestCreatePostMultiple(cur realm, t *testing.T) {\n\t// Initialize a slice to hold the test posts and their authors\n\tposts := []struct {\n\t\ttext   string\n\t\tauthor string\n\t}{\n\t\t{\"Hello World!\", \"alice\"},\n\t\t{\"This is some new text!\", \"bob\"},\n\t\t{\"Another post by alice\", \"alice\"},\n\t\t{\"A post by charlie!\", \"charlie\"},\n\t}\n\n\tfor _, p := range posts {\n\t\t// Set the appropriate caller realm based on the author\n\t\tauthorAddr := testutils.TestAddress(p.author)\n\t\ttesting.SetRealm(testing.NewUserRealm(authorAddr))\n\n\t\t// Create the post\n\t\terr := CreatePost(cross(cur), p.text)\n\t\tuassert.True(t, err == nil, \"expected no error for post \"+p.text)\n\t}\n\n\t// Get the rendered page\n\tgot := Render(\"\")\n\n\t// Check that all posts and their authors are present in the rendered output\n\tfor _, p := range posts {\n\t\texpectedText := p.text\n\t\texpectedAuthor := testutils.TestAddress(p.author).String() // Get the address for the author\n\t\tcondition := strings.Contains(got, expectedText) \u0026\u0026 strings.Contains(got, expectedAuthor)\n\t\tuassert.True(t, condition, \"expected render to contain text '\"+expectedText+\"' and address '\"+expectedAuthor+\"'\")\n\t}\n}\n\nfunc TestReset(cur realm, t *testing.T) {\n\taliceAddr := testutils.TestAddress(\"alice\")\n\ttesting.SetRealm(testing.NewUserRealm(aliceAddr))\n\n\ttext1 := \"Hello World!\"\n\t_ = CreatePost(cross(cur), text1)\n\n\tgot := Render(\"\")\n\tuassert.True(t, strings.Contains(got, text1), \"expected render to contain text1\")\n\n\t// Set admin\n\ttesting.SetRealm(testing.NewUserRealm(Ownable.Owner()))\n\tResetPosts(cross(cur))\n\n\tgot = Render(\"\")\n\tuassert.False(t, strings.Contains(got, text1), \"expected render to not contain text1\")\n\n\ttext2 := \"Some other Text!!\"\n\t_ = CreatePost(cross(cur), text2)\n\n\tgot = Render(\"\")\n\tuassert.False(t, strings.Contains(got, text1), \"expected render to not contain text1\")\n\n\tuassert.True(t, strings.Contains(got, text2), \"expected render to contain text2\")\n}\n\n// TODO: Add tests for Update \u0026 Delete\n"},{"name":"types.gno","body":"package minisocial\n\nimport (\n\t// The standard Gno package\n\t\"time\" // For handling time operations\n\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\n\t\"gno.land/r/sys/users\"\n)\n\n// Post defines the main data we keep about each post\ntype Post struct {\n\tid        seqid.ID\n\ttext      string\n\tauthor    address\n\tcreatedAt time.Time\n\tupdatedAt time.Time\n}\n\nfunc (p Post) String() string {\n\tout := p.text + \"\\n\\n\"\n\n\tauthor := p.author.String()\n\t// We can import and use the r/sys/users package to resolve addresses\n\tuser := users.ResolveAddress(p.author)\n\tif user != nil {\n\t\t// RenderLink provides a link that is clickable\n\t\t// The link goes to the user's profile page\n\t\tauthor = user.RenderLink(\"\")\n\t}\n\n\tout += ufmt.Sprintf(\"_by %s on %s_\\n\\n\", author, p.createdAt.Format(\"02 Jan 2006, 15:04\"))\n\treturn out\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"YzGc32l/FaKZYZivAwLvYgO7DIOD7r55vvop1oGMOzE+q/LA/kK2GuoSBEi8Z2dZC0+EqeCtbW4dGJrH2Pgm4A=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"mistakes","path":"gno.land/r/docs/mistakes","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/mistakes\"\ngno = \"0.9\"\n"},{"name":"mistakes.gno","body":"// Package mistakes shows common mistakes in Gno development and how to avoid them.\npackage mistakes\n\nfunc Render(_ string) string {\n\treturn `# Common Mistakes in Gno\n\n## 1. Integer Overflow\n\nGo (and Gno) integers can overflow silently:\n\n` + \"```go\" + `\nvar balance int64 = 9223372036854775807 // max int64\nbalance += 1 // Wraps to -9223372036854775808!\n` + \"```\" + `\n\nUse the [` + \"`math/overflow`\" + `](https://docs.gno.land/resources/gno-stdlibs) standard library instead:\n\n` + \"```go\" + `\nimport \"math/overflow\"\n\nresult, ok := overflow.Add64(a, b)\nif !ok {\n    panic(\"overflow\")\n}\n` + \"```\" + `\n\n---\n\n## 2. Errors Don't Revert State\n\nReturning an error doesn't revert state changes:\n\n` + \"```go\" + `\n// WRONG\nvar balances map[address]int64\n\nfunc Transfer(cur realm, to address, amount int64) error {\n    if !cur.IsCurrent() {\n        panic(\"invalid realm\")\n    }\n    from := cur.Previous().Address()\n    balances[from] -= amount\n    if balances[from] \u003c 0 {\n        return errors.New(\"insufficient balance\")  // State NOT reverted!\n    }\n    balances[to] += amount\n    return nil\n}\n` + \"```\" + `\n\nCheck conditions first, or use panic to revert:\n\n` + \"```go\" + `\n// CORRECT\nvar balances map[address]int64\n\nfunc Transfer(cur realm, to address, amount int64) {\n    if !cur.IsCurrent() {\n        panic(\"invalid realm\")\n    }\n    from := cur.Previous().Address()\n    if balances[from] \u003c amount {\n        panic(\"insufficient balance\")  // Reverts all changes\n    }\n    balances[from] -= amount\n    balances[to] += amount\n}\n` + \"```\" + `\n\n---\n\n## 3. OriginCaller vs cur.Previous()\n\nUsing [` + \"`OriginCaller`\" + `](/r/docs/crossing) for authorization enables middleman attacks:\n\n` + \"```go\" + `\n// VULNERABLE: Malicious realm can call this on behalf of any user\nvar balances map[address]int64\n\nfunc Withdraw(_ realm, amount int64) {\n    caller := unsafe.OriginCaller()  // Always returns the EOA who signed tx\n    balances[caller] -= amount\n}\n` + \"```\" + `\n\nUse [` + \"`cur.Previous().Address()`\" + `](/r/docs/crossing) for permission checks,\nguarded by ` + \"`cur.IsCurrent()`\" + ` so a non-crossing call can't pass a non-current\nrealm handle to forge the caller:\n\n` + \"```go\" + `\n// SAFE\nvar balances map[address]int64\n\nfunc Withdraw(cur realm, amount int64) {\n    if !cur.IsCurrent() {\n        panic(\"invalid realm\")\n    }\n    caller := cur.Previous().Address()  // Returns immediate caller\n    balances[caller] -= amount\n}\n` + \"```\" + `\n\n---\n\n## 4. Not Validating Addresses\n\n` + \"```go\" + `\n// WRONG\nvar admin address\n\nfunc SetAdmin(newAdmin address) {\n    admin = newAdmin  // Could be an empty or malformed address!\n}\n\n// CORRECT\nvar admin address\n\nfunc SetAdmin(_ realm, newAdmin address) {\n    if !newAdmin.IsValid() {\n        panic(\"invalid address\")\n    }\n    admin = newAdmin\n}\n` + \"```\" + `\n\n---\n\n## 5. Returning References to Internal State\n\nReturning a pointer, slice, map, or interface value gives the caller a live\nreference to your realm's data. The read-only taint stops an external realm from\nwriting to a returned field directly, but it does not stop a mutating method on\nthe value, and same-realm code is not tainted at all. Either path skips the\nchecks your setters enforce.\n\n` + \"```go\" + `\n// RISKY\ntype Config struct {\n    admin address\n}\n\n// SetAdmin has no checks; any caller holding a *Config can call it.\nfunc (c *Config) SetAdmin(a address) { c.admin = a }\n\nvar config Config\n\n// Returns a live pointer, so a caller can call SetAdmin directly\nfunc GetConfig() *Config {\n    return \u0026config\n}\n` + \"```\" + `\n\nReturn a copy, or expose only read methods, when the caller should not change\nstored state:\n\n` + \"```go\" + `\n// SAFE\nvar admins []address\n\nfunc Admins() []address {\n    return append([]address(nil), admins...)\n}\n` + \"```\" + `\n`\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"X6+CWJgt6ffP3WQEjAuhs2Gvjy/mAu9hbz5Wd4QsNNAAVlhnXCE5vjPQDyyzZismxQHApDmL+uTuh3Y+FdmaWg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"moul_md","path":"gno.land/r/docs/moul_md","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/moul_md\"\ngno = \"0.9\"\n"},{"name":"moul_md.gno","body":"package moul_md\n\nimport \"gno.land/p/moul/md/v0\"\n\n// This package demonstrates the usage of the gno.land/p/moul/md/v0 package.\n\nfunc Render(path string) string {\n\tsections := []string{\n\t\trenderIntro(),\n\t\trenderTextFormatting(),\n\t\trenderHeaders(),\n\t\trenderLists(),\n\t\trenderLinksAndImages(),\n\t\trenderCode(),\n\t\trenderBlockquotes(),\n\t\trenderAdvancedFeatures(),\n\t\trenderUtilityFunctions(),\n\t\trenderPracticalExample(),\n\t\trenderBestPractices(),\n\t\trenderSeeAlso(),\n\t}\n\n\toutput := \"\"\n\tfor _, section := range sections {\n\t\toutput += section\n\t}\n\treturn output\n}\n\nfunc renderIntro() string {\n\treturn md.H1(\"Package Demo: `p/moul/md` \") +\n\t\tmd.Paragraph(\"This document demonstrates the features of the \"+md.Link(\"gno.land/p/moul/md/v0\", \"https://gno.land/p/moul/md/v0\")+\" package, showing both the source code and the rendered result for each feature.\") +\n\t\t\"\\n\" +\n\t\tmd.Blockquote(md.Bold(\"Note:\")+\" The md package provides helper functions to generate markdown programmatically, making it easier to create dynamic documentation and content.\") +\n\t\t\"\\n\"\n}\n\nfunc renderTextFormatting() string {\n\treturn md.H2(\"Text Formatting\") + \"\\n\" +\n\t\t// Bold\n\t\trenderExample(\n\t\t\t\"Bold Text\",\n\t\t\t`md.Bold(\"Important text\")`,\n\t\t\tmd.Bold(\"Important text\"),\n\t\t) +\n\t\t// Italic\n\t\trenderExample(\n\t\t\t\"Italic Text\",\n\t\t\t`md.Italic(\"Emphasized text\")`,\n\t\t\tmd.Italic(\"Emphasized text\"),\n\t\t) +\n\t\t// Strikethrough\n\t\trenderExample(\n\t\t\t\"Strikethrough Text\",\n\t\t\t`md.Strikethrough(\"Deprecated feature\")`,\n\t\t\tmd.Strikethrough(\"Deprecated feature\"),\n\t\t) +\n\t\t// Combined\n\t\trenderExample(\n\t\t\t\"Combined Formatting\",\n\t\t\t`md.Bold(md.Italic(\"Very important\"))`,\n\t\t\tmd.Bold(md.Italic(\"Very important\")),\n\t\t)\n}\n\nfunc renderHeaders() string {\n\tcode := `md.H1(\"Main Title\")\nmd.H2(\"Section\")\nmd.H3(\"Subsection\")\nmd.H4(\"Sub-subsection\")\nmd.H5(\"Minor heading\")\nmd.H6(\"Smallest heading\")`\n\n\tresult := md.H1(\"Main Title\") +\n\t\tmd.H2(\"Section\") +\n\t\tmd.H3(\"Subsection\") +\n\t\tmd.H4(\"Sub-subsection\") +\n\t\tmd.H5(\"Minor heading\") +\n\t\tmd.H6(\"Smallest heading\")\n\n\treturn md.H2(\"Headers\") +\n\t\tmd.Paragraph(\"The package supports all six levels of markdown headers:\") +\n\t\t\"\\n\" +\n\t\tmd.LanguageCodeBlock(\"go\", code) +\n\t\t\"\\nResult:\\n\" +\n\t\tresult +\n\t\t\"\\n\"\n}\n\nfunc renderLists() string {\n\toutput := md.H2(\"Lists\") + \"\\n\"\n\n\t// Bullet Lists\n\tbulletCode := `items := []string{\n    \"First item\",\n    \"Second item\",\n    \"Third item with\\nmultiple lines\",\n}\nmd.BulletList(items)`\n\n\toutput += md.H3(\"Bullet Lists\") +\n\t\tmd.LanguageCodeBlock(\"go\", bulletCode) +\n\t\t\"\\nResult:\\n\" +\n\t\tmd.BulletList([]string{\n\t\t\t\"First item\",\n\t\t\t\"Second item\",\n\t\t\t\"Third item with\\nmultiple lines\",\n\t\t}) + \"\\n\"\n\n\t// Ordered Lists\n\torderedCode := `steps := []string{\n    \"First step\",\n    \"Second step\",\n    \"Third step with\\nadditional details\",\n}\nmd.OrderedList(steps)`\n\n\toutput += md.H3(\"Ordered Lists\") +\n\t\tmd.LanguageCodeBlock(\"go\", orderedCode) +\n\t\t\"\\nResult:\\n\" +\n\t\tmd.OrderedList([]string{\n\t\t\t\"First step\",\n\t\t\t\"Second step\",\n\t\t\t\"Third step with\\nadditional details\",\n\t\t}) + \"\\n\"\n\n\t// Todo Lists\n\ttodoCode := `tasks := []string{\n    \"Completed task\",\n    \"Another completed task\",\n    \"Pending task\",\n    \"Another pending task\",\n}\ncompleted := []bool{true, true, false, false}\nmd.TodoList(tasks, completed)`\n\n\toutput += md.H3(\"Todo Lists\") +\n\t\tmd.LanguageCodeBlock(\"go\", todoCode) +\n\t\t\"\\nResult:\\n\" +\n\t\tmd.TodoList(\n\t\t\t[]string{\n\t\t\t\t\"Completed task\",\n\t\t\t\t\"Another completed task\",\n\t\t\t\t\"Pending task\",\n\t\t\t\t\"Another pending task\",\n\t\t\t},\n\t\t\t[]bool{true, true, false, false},\n\t\t) + \"\\n\"\n\n\t// Nested Lists\n\tnestedCode := `md.BulletItem(\"Parent item\") +\nmd.Nested(\n    md.BulletItem(\"Nested item 1\") +\n    md.BulletItem(\"Nested item 2\") +\n    md.Nested(\n        md.BulletItem(\"Deeply nested\"),\n        \"    \",\n    ),\n    \"  \",\n)`\n\n\tnestedResult := md.BulletItem(\"Parent item\") +\n\t\tmd.Nested(\n\t\t\tmd.BulletItem(\"Nested item 1\")+\n\t\t\t\tmd.BulletItem(\"Nested item 2\")+\n\t\t\t\tmd.Nested(\n\t\t\t\t\tmd.BulletItem(\"Deeply nested\"),\n\t\t\t\t\t\"    \",\n\t\t\t\t),\n\t\t\t\"  \",\n\t\t)\n\n\toutput += md.H3(\"Nested Lists\") +\n\t\tmd.LanguageCodeBlock(\"go\", nestedCode) +\n\t\t\"\\nResult:\\n\" +\n\t\tnestedResult + \"\\n\"\n\n\treturn output\n}\n\nfunc renderLinksAndImages() string {\n\treturn md.H2(\"Links and Images\") + \"\\n\" +\n\t\t// Regular Links\n\t\trenderExample(\n\t\t\t\"Regular Links\",\n\t\t\t`md.Link(\"Gno Homepage\", \"https://gno.land\")`,\n\t\t\tmd.Link(\"Gno Homepage\", \"https://gno.land\"),\n\t\t) +\n\t\t// User Links\n\t\trenderExample(\n\t\t\t\"User Links\",\n\t\t\t`md.UserLink(\"moul\")`,\n\t\t\tmd.UserLink(\"moul\"),\n\t\t) +\n\t\trenderExample(\n\t\t\t\"\",\n\t\t\t`md.UserLink(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")`,\n\t\t\tmd.UserLink(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"),\n\t\t) +\n\t\t// Images\n\t\tmd.H3(\"Images\") +\n\t\tmd.LanguageCodeBlock(\"go\", `md.Image(\"Gno Logo\", \"/public/imgs/gnoland.svg\")`) +\n\t\t\"\\nResult:\\n\" +\n\t\tmd.Image(\"Gno Logo\", \"/public/imgs/gnoland.svg\") + \"\\n\" +\n\t\t// Clickable Images\n\t\tmd.H3(\"Clickable Images\") +\n\t\tmd.LanguageCodeBlock(\"go\", `md.InlineImageWithLink(\"Click me!\", \"/public/imgs/gnoland.svg\", \"https://gno.land\")`) +\n\t\t\"\\nResult:\\n\" +\n\t\tmd.InlineImageWithLink(\"Click me!\", \"/public/imgs/gnoland.svg\", \"https://gno.land\") + \"\\n\"\n}\n\nfunc renderCode() string {\n\treturn md.H2(\"Code\") + \"\\n\" +\n\t\t// Inline Code\n\t\trenderExample(\n\t\t\t\"Inline Code\",\n\t\t\t`\"Use \" + md.InlineCode(\"gno test\") + \" to run tests\"`,\n\t\t\t\"Use \"+md.InlineCode(\"gno test\")+\" to run tests\",\n\t\t) +\n\t\t// Simple Code Block\n\t\tmd.H3(\"Simple Code Block\") +\n\t\tmd.LanguageCodeBlock(\"go\", `md.CodeBlock(\"func main() {\\n    println(\\\"Hello, Gno!\\\")\\n}\")`) +\n\t\t\"\\nResult:\\n\" +\n\t\tmd.CodeBlock(\"func main() {\\n    println(\\\"Hello, Gno!\\\")\\n}\") + \"\\n\" +\n\t\t// Language Code Block\n\t\tmd.H3(\"Language-specific Code Block\") +\n\t\tmd.LanguageCodeBlock(\"go\", `md.LanguageCodeBlock(\"go\", \"package main\\n\\nfunc main() {\\n    println(\\\"Hello!\\\")\\n}\")`) +\n\t\t\"\\nResult:\\n\" +\n\t\tmd.LanguageCodeBlock(\"go\", \"package main\\n\\nfunc main() {\\n    println(\\\"Hello!\\\")\\n}\") + \"\\n\"\n}\n\nfunc renderBlockquotes() string {\n\treturn md.H2(\"Blockquotes\") +\n\t\tmd.LanguageCodeBlock(\"go\", `md.Blockquote(\"This is an important note.\")`) +\n\t\t\"\\nResult:\\n\" +\n\t\tmd.Blockquote(\"This is an important note.\") + \"\\n\" +\n\t\tmd.LanguageCodeBlock(\"go\", `md.Blockquote(\"Multi-line quotes\\nare also supported\\nwith proper formatting.\")`) +\n\t\t\"\\nResult:\\n\" +\n\t\tmd.Blockquote(\"Multi-line quotes\\nare also supported\\nwith proper formatting.\") + \"\\n\"\n}\n\nfunc renderAdvancedFeatures() string {\n\toutput := md.H2(\"Advanced Features\") + \"\\n\"\n\n\t// Collapsible Sections\n\toutput += md.H3(\"Collapsible Sections\") +\n\t\tmd.LanguageCodeBlock(\"go\", `md.CollapsibleSection(\"Click to expand\", \"Hidden content here!\")`) +\n\t\t\"\\nResult:\\n\" +\n\t\tmd.CollapsibleSection(\"Click to expand\", \"Hidden content here!\") + \"\\n\"\n\n\t// Two Columns\n\tcolumnsCode := `md.Columns([]string{\n    \"Left column content\",\n    \"Right column content\",\n}, false)`\n\n\toutput += md.H3(\"Two Columns\") +\n\t\tmd.LanguageCodeBlock(\"go\", columnsCode) +\n\t\t\"\\nResult:\\n\" +\n\t\tmd.Columns([]string{\n\t\t\t\"Left column content\",\n\t\t\t\"Right column content\",\n\t\t}, false) + \"\\n\"\n\n\t// Multiple Columns\n\tmultiColumnsCode := `md.ColumnsN([]string{\n    \"Item 1\", \"Item 2\", \"Item 3\",\n    \"Item 4\", \"Item 5\", \"Item 6\",\n}, 3, true)`\n\n\toutput += md.H3(\"Multiple Columns (3 per row)\") +\n\t\tmd.LanguageCodeBlock(\"go\", multiColumnsCode) +\n\t\t\"\\nResult:\\n\" +\n\t\tmd.ColumnsN([]string{\n\t\t\t\"Item 1\", \"Item 2\", \"Item 3\",\n\t\t\t\"Item 4\", \"Item 5\", \"Item 6\",\n\t\t}, 3, true) + \"\\n\"\n\n\treturn output\n}\n\nfunc renderUtilityFunctions() string {\n\treturn md.H2(\"Utility Functions\") + \"\\n\" +\n\t\t// Horizontal Rule\n\t\trenderExampleWithRawResult(\n\t\t\t\"Horizontal Rule\",\n\t\t\t`md.HorizontalRule()`,\n\t\t\tmd.HorizontalRule(),\n\t\t) +\n\t\t// Footnotes\n\t\tmd.H3(\"Footnotes\") +\n\t\tmd.LanguageCodeBlock(\"go\", `\"This has a footnote[^1].\\n\\n\" + md.FootnoteDefinition(\"1\", \"Footnote content here.\")`) +\n\t\t\"\\nResult:\\n\" +\n\t\t\"This has a footnote[^1].\\n\\n\" +\n\t\tmd.FootnoteDefinition(\"1\", \"Footnote content here.\") + \"\\n\" +\n\t\t// Paragraphs\n\t\trenderExampleWithRawResult(\n\t\t\t\"Paragraphs\",\n\t\t\t`md.Paragraph(\"This ensures proper spacing.\")`,\n\t\t\tmd.Paragraph(\"This ensures proper spacing.\"),\n\t\t)\n}\n\nfunc renderPracticalExample() string {\n\tcode := `// Example function that generates a rich user profile with balanced columns\nfunc RenderProfile(username, name, bio string, avatar string, tasks []string, completed []bool) string {\n    // First row: Avatar/Basic Info | Bio\n    avatarSection := md.Image(name + \" avatar\", avatar) + \"\\n\\n\" +\n        md.H3(\"Basic Info\") +\n        md.BulletList([]string{\n            md.Bold(\"Name:\") + \" \" + name,\n            md.Bold(\"Username:\") + \" \" + md.UserLink(username),\n            md.Bold(\"Status:\") + \" 🟢 Active\",\n            md.Bold(\"Joined:\") + \" January 2024\",\n        })\n    \n    bioSection := md.H3(\"Bio\") +\n        md.Blockquote(bio)\n    \n    // Second row: Tasks | Links\n    tasksSection := md.H3(\"Current Tasks\") +\n        md.TodoList(tasks, completed)\n    \n    linksSection := md.H3(\"Links\") +\n        md.BulletList([]string{\n            md.Link(\"GitHub\", \"https://github.com/\" + username),\n            md.Link(\"Portfolio\", \"https://example.com/\" + username),\n            md.Link(\"LinkedIn\", \"https://linkedin.com/in/\" + username),\n        })\n    \n    // Combine with main title and two sets of columns\n    return md.H1(\"User Profile: \" + name) +\n        md.HorizontalRule() +\n        md.Columns([]string{avatarSection, bioSection}, true) +\n        \"\\n\" +\n        md.Columns([]string{tasksSection, linksSection}, true)\n}\n\n// Usage:\nprofile := RenderProfile(\n    \"johndoe\",\n    \"John Doe\", \n    \"Passionate Gno developer building the future of smart contracts. Love working with blockchain technology and contributing to open source.\",\n    \"/public/imgs/gnoland.svg\",\n    []string{\"Complete Gno tutorial\", \"Build first realm\", \"Deploy to testnet\", \"Write documentation\"},\n    []bool{true, true, false, false},\n)`\n\n\t// Generate actual result - First row\n\tavatarSection := md.Image(\"John Doe avatar\", \"/public/imgs/gnoland.svg\") + \"\\n\\n\" +\n\t\tmd.H3(\"Basic Info\") +\n\t\tmd.BulletList([]string{\n\t\t\tmd.Bold(\"Name:\") + \" John Doe\",\n\t\t\tmd.Bold(\"Username:\") + \" \" + md.UserLink(\"johndoe\"),\n\t\t\tmd.Bold(\"Status:\") + \" 🟢 Active\",\n\t\t\tmd.Bold(\"Joined:\") + \" January 2024\",\n\t\t})\n\n\tbioSection := md.H3(\"Bio\") +\n\t\tmd.Blockquote(\"Passionate Gno developer building the future of smart contracts. Love working with blockchain technology and contributing to open source.\")\n\n\t// Second row\n\ttasksSection := md.H3(\"Current Tasks\") +\n\t\tmd.TodoList(\n\t\t\t[]string{\"Complete Gno tutorial\", \"Build first realm\", \"Deploy to testnet\", \"Write documentation\"},\n\t\t\t[]bool{true, true, false, false},\n\t\t)\n\n\tlinksSection := md.H3(\"Links\") +\n\t\tmd.BulletList([]string{\n\t\t\tmd.Link(\"GitHub\", \"https://github.com/johndoe\"),\n\t\t\tmd.Link(\"Portfolio\", \"https://example.com/johndoe\"),\n\t\t\tmd.Link(\"LinkedIn\", \"https://linkedin.com/in/johndoe\"),\n\t\t})\n\n\tresult := md.H1(\"User Profile: John Doe\") +\n\t\tmd.HorizontalRule() +\n\t\tmd.Columns([]string{avatarSection, bioSection}, true) +\n\t\t\"\\n\" +\n\t\tmd.Columns([]string{tasksSection, linksSection}, true)\n\n\treturn md.H2(\"Practical Example\") +\n\t\tmd.Paragraph(\"Here's a complete example showing how to build a rich user profile page using columns, images, and various md features:\") +\n\t\t\"\\n\" +\n\t\tmd.LanguageCodeBlock(\"go\", code) +\n\t\t\"\\nResult when called with the above parameters:\\n\" +\n\t\tresult + \"\\n\"\n}\n\nfunc renderBestPractices() string {\n\tpractices := []string{\n\t\t\"Prioritize code readability over performance in Render() - use clear variable names, logical grouping, and readable concatenation for humans\",\n\t\t\"Use the md package for all programmatic markdown generation in Render() methods\",\n\t\t\"Combine functions for complex formatting: \" + md.InlineCode(\"md.Bold(md.Italic(\\\"text\\\"))\"),\n\t\t\"Use \" + md.InlineCode(\"md.Paragraph()\") + \" to ensure proper spacing between elements\",\n\t\t\"Leverage \" + md.InlineCode(\"md.ColumnsN()\") + \" for responsive layouts\",\n\t}\n\n\treturn md.H2(\"Best Practices\") +\n\t\tmd.OrderedList(practices) +\n\t\t\"\\n\"\n}\n\nfunc renderSeeAlso() string {\n\tlinks := []string{\n\t\tmd.Link(\"gno.land/p/moul/md/v0\", \"https://gno.land/p/moul/md/v0\") + \" - The md package source code\",\n\t\tmd.Link(\"Markdown on Gno\", \"https://gno.land/r/docs/markdown\") + \" - Comprehensive guide on Gno Flavored Markdown syntax\",\n\t}\n\n\treturn md.H2(\"See Also\") +\n\t\tmd.BulletList(links)\n}\n\n// Helper function to render an example with code and result\nfunc renderExample(title, code, result string) string {\n\toutput := \"\"\n\tif title != \"\" {\n\t\toutput += md.H3(title)\n\t}\n\toutput += md.LanguageCodeBlock(\"go\", code) + \"\\n\"\n\toutput += md.Paragraph(\"Result: \"+result) + \"\\n\"\n\treturn output\n}\n\n// Helper function for examples where result needs raw output\nfunc renderExampleWithRawResult(title, code, result string) string {\n\toutput := md.H3(title)\n\toutput += md.LanguageCodeBlock(\"go\", code) + \"\\n\"\n\toutput += \"Result:\\n\" + result + \"\\n\"\n\treturn output\n}\n"},{"name":"moul_md_filetest.gno","body":"// Package main\n// This is a filetest. It's somewhat similar to Go's 'Example' files/functions. It's there to showcase functionality and print the output of a specific scenario related to the package it's testing. In this case, it's related to the `Render` function.\npackage main\n\nimport \"gno.land/r/docs/moul_md\"\n\nfunc main() {\n\t// Test the main Render function\n\tprintln(moul_md.Render(\"\"))\n}\n\n// Output:\n// # Package Demo: `p/moul/md`\n// This document demonstrates the features of the [gno\\.land/p/moul/md/v0](https://gno.land/p/moul/md/v0) package, showing both the source code and the rendered result for each feature.\n//\n//\n//\n// \u003e **Note:** The md package provides helper functions to generate markdown programmatically, making it easier to create dynamic documentation and content.\n//\n//\n// ## Text Formatting\n//\n// ### Bold Text\n// ```go\n// md.Bold(\"Important text\")\n// ```\n//\n// Result: **Important text**\n//\n//\n// ### Italic Text\n// ```go\n// md.Italic(\"Emphasized text\")\n// ```\n//\n// Result: *Emphasized text*\n//\n//\n// ### Strikethrough Text\n// ```go\n// md.Strikethrough(\"Deprecated feature\")\n// ```\n//\n// Result: ~~Deprecated feature~~\n//\n//\n// ### Combined Formatting\n// ```go\n// md.Bold(md.Italic(\"Very important\"))\n// ```\n//\n// Result: ***Very important***\n//\n//\n// ## Headers\n// The package supports all six levels of markdown headers:\n//\n//\n// ```go\n// md.H1(\"Main Title\")\n// md.H2(\"Section\")\n// md.H3(\"Subsection\")\n// md.H4(\"Sub-subsection\")\n// md.H5(\"Minor heading\")\n// md.H6(\"Smallest heading\")\n// ```\n//\n// Result:\n// # Main Title\n// ## Section\n// ### Subsection\n// #### Sub-subsection\n// ##### Minor heading\n// ###### Smallest heading\n//\n// ## Lists\n//\n// ### Bullet Lists\n// ```go\n// items := []string{\n//     \"First item\",\n//     \"Second item\",\n//     \"Third item with\\nmultiple lines\",\n// }\n// md.BulletList(items)\n// ```\n//\n// Result:\n// - First item\n// - Second item\n// - Third item with\n//   multiple lines\n//\n// ### Ordered Lists\n// ```go\n// steps := []string{\n//     \"First step\",\n//     \"Second step\",\n//     \"Third step with\\nadditional details\",\n// }\n// md.OrderedList(steps)\n// ```\n//\n// Result:\n// 1. First step\n// 2. Second step\n// 3. Third step with\n//    additional details\n//\n// ### Todo Lists\n// ```go\n// tasks := []string{\n//     \"Completed task\",\n//     \"Another completed task\",\n//     \"Pending task\",\n//     \"Another pending task\",\n// }\n// completed := []bool{true, true, false, false}\n// md.TodoList(tasks, completed)\n// ```\n//\n// Result:\n// - [x] Completed task\n// - [x] Another completed task\n// - [ ] Pending task\n// - [ ] Another pending task\n//\n// ### Nested Lists\n// ```go\n// md.BulletItem(\"Parent item\") +\n// md.Nested(\n//     md.BulletItem(\"Nested item 1\") +\n//     md.BulletItem(\"Nested item 2\") +\n//     md.Nested(\n//         md.BulletItem(\"Deeply nested\"),\n//         \"    \",\n//     ),\n//     \"  \",\n// )\n// ```\n//\n// Result:\n// - Parent item\n//   - Nested item 1\n//   - Nested item 2\n//       - Deeply nested\n//\n// ## Links and Images\n//\n// ### Regular Links\n// ```go\n// md.Link(\"Gno Homepage\", \"https://gno.land\")\n// ```\n//\n// Result: [Gno Homepage](https://gno.land)\n//\n//\n// ### User Links\n// ```go\n// md.UserLink(\"moul\")\n// ```\n//\n// Result: [@moul](/u/moul)\n//\n//\n// ```go\n// md.UserLink(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n// ```\n//\n// Result: [g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5](/u/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5)\n//\n//\n// ### Images\n// ```go\n// md.Image(\"Gno Logo\", \"/public/imgs/gnoland.svg\")\n// ```\n//\n// Result:\n// ![Gno Logo](/public/imgs/gnoland.svg)\n// ### Clickable Images\n// ```go\n// md.InlineImageWithLink(\"Click me!\", \"/public/imgs/gnoland.svg\", \"https://gno.land\")\n// ```\n//\n// Result:\n// [![Click me\\!](/public/imgs/gnoland.svg)](https://gno.land)\n// ## Code\n//\n// ### Inline Code\n// ```go\n// \"Use \" + md.InlineCode(\"gno test\") + \" to run tests\"\n// ```\n//\n// Result: Use `gno test` to run tests\n//\n//\n// ### Simple Code Block\n// ```go\n// md.CodeBlock(\"func main() {\\n    println(\\\"Hello, Gno!\\\")\\n}\")\n// ```\n//\n// Result:\n// ```\n// func main() {\n//     println(\"Hello, Gno!\")\n// }\n// ```\n//\n// ### Language-specific Code Block\n// ```go\n// md.LanguageCodeBlock(\"go\", \"package main\\n\\nfunc main() {\\n    println(\\\"Hello!\\\")\\n}\")\n// ```\n//\n// Result:\n// ```go\n// package main\n//\n// func main() {\n//     println(\"Hello!\")\n// }\n// ```\n//\n// ## Blockquotes\n// ```go\n// md.Blockquote(\"This is an important note.\")\n// ```\n//\n// Result:\n//\n// \u003e This is an important note.\n//\n//\n// ```go\n// md.Blockquote(\"Multi-line quotes\\nare also supported\\nwith proper formatting.\")\n// ```\n//\n// Result:\n//\n// \u003e Multi-line quotes\n// \u003e are also supported\n// \u003e with proper formatting.\n//\n//\n// ## Advanced Features\n//\n// ### Collapsible Sections\n// ```go\n// md.CollapsibleSection(\"Click to expand\", \"Hidden content here!\")\n// ```\n//\n// Result:\n// \u003cdetails\u003e\u003csummary\u003eClick to expand\u003c/summary\u003e\n//\n// Hidden content here!\n// \u003c/details\u003e\n//\n// ### Two Columns\n// ```go\n// md.Columns([]string{\n//     \"Left column content\",\n//     \"Right column content\",\n// }, false)\n// ```\n//\n// Result:\n// \u003cgno-columns\u003e\n// Left column content\n// \u003cgno-columns-sep\u003e\n// Right column content\n// \u003c/gno-columns\u003e\n//\n// ### Multiple Columns (3 per row)\n// ```go\n// md.ColumnsN([]string{\n//     \"Item 1\", \"Item 2\", \"Item 3\",\n//     \"Item 4\", \"Item 5\", \"Item 6\",\n// }, 3, true)\n// ```\n//\n// Result:\n// \u003cgno-columns\u003e\n// Item 1\n// \u003cgno-columns-sep\u003e\n// Item 2\n// \u003cgno-columns-sep\u003e\n// Item 3\n// \u003c/gno-columns\u003e\n// \u003cgno-columns\u003e\n// Item 4\n// \u003cgno-columns-sep\u003e\n// Item 5\n// \u003cgno-columns-sep\u003e\n// Item 6\n// \u003c/gno-columns\u003e\n//\n// ## Utility Functions\n//\n// ### Horizontal Rule\n// ```go\n// md.HorizontalRule()\n// ```\n//\n// Result:\n// ---\n//\n// ### Footnotes\n// ```go\n// \"This has a footnote[^1].\\n\\n\" + md.FootnoteDefinition(\"1\", \"Footnote content here.\")\n// ```\n//\n// Result:\n// This has a footnote[^1].\n//\n// [^1]:\n//     Footnote content here.\n//\n// ### Paragraphs\n// ```go\n// md.Paragraph(\"This ensures proper spacing.\")\n// ```\n//\n// Result:\n// This ensures proper spacing.\n//\n//\n// ## Practical Example\n// Here's a complete example showing how to build a rich user profile page using columns, images, and various md features:\n//\n//\n// ```go\n// // Example function that generates a rich user profile with balanced columns\n// func RenderProfile(username, name, bio string, avatar string, tasks []string, completed []bool) string {\n//     // First row: Avatar/Basic Info | Bio\n//     avatarSection := md.Image(name + \" avatar\", avatar) + \"\\n\\n\" +\n//         md.H3(\"Basic Info\") +\n//         md.BulletList([]string{\n//             md.Bold(\"Name:\") + \" \" + name,\n//             md.Bold(\"Username:\") + \" \" + md.UserLink(username),\n//             md.Bold(\"Status:\") + \" 🟢 Active\",\n//             md.Bold(\"Joined:\") + \" January 2024\",\n//         })\n//\n//     bioSection := md.H3(\"Bio\") +\n//         md.Blockquote(bio)\n//\n//     // Second row: Tasks | Links\n//     tasksSection := md.H3(\"Current Tasks\") +\n//         md.TodoList(tasks, completed)\n//\n//     linksSection := md.H3(\"Links\") +\n//         md.BulletList([]string{\n//             md.Link(\"GitHub\", \"https://github.com/\" + username),\n//             md.Link(\"Portfolio\", \"https://example.com/\" + username),\n//             md.Link(\"LinkedIn\", \"https://linkedin.com/in/\" + username),\n//         })\n//\n//     // Combine with main title and two sets of columns\n//     return md.H1(\"User Profile: \" + name) +\n//         md.HorizontalRule() +\n//         md.Columns([]string{avatarSection, bioSection}, true) +\n//         \"\\n\" +\n//         md.Columns([]string{tasksSection, linksSection}, true)\n// }\n//\n// // Usage:\n// profile := RenderProfile(\n//     \"johndoe\",\n//     \"John Doe\",\n//     \"Passionate Gno developer building the future of smart contracts. Love working with blockchain technology and contributing to open source.\",\n//     \"/public/imgs/gnoland.svg\",\n//     []string{\"Complete Gno tutorial\", \"Build first realm\", \"Deploy to testnet\", \"Write documentation\"},\n//     []bool{true, true, false, false},\n// )\n// ```\n//\n// Result when called with the above parameters:\n// # User Profile: John Doe\n// ---\n// \u003cgno-columns\u003e\n// ![John Doe avatar](/public/imgs/gnoland.svg)\n//\n// ### Basic Info\n// - **Name:** John Doe\n// - **Username:** [@johndoe](/u/johndoe)\n// - **Status:** 🟢 Active\n// - **Joined:** January 2024\n//\n// \u003cgno-columns-sep\u003e\n// ### Bio\n//\n// \u003e Passionate Gno developer building the future of smart contracts. Love working with blockchain technology and contributing to open source.\n//\n//\n// \u003cgno-columns-sep\u003e\n//\n// \u003cgno-columns-sep\u003e\n//\n// \u003c/gno-columns\u003e\n//\n// \u003cgno-columns\u003e\n// ### Current Tasks\n// - [x] Complete Gno tutorial\n// - [x] Build first realm\n// - [ ] Deploy to testnet\n// - [ ] Write documentation\n//\n// \u003cgno-columns-sep\u003e\n// ### Links\n// - [GitHub](https://github.com/johndoe)\n// - [Portfolio](https://example.com/johndoe)\n// - [LinkedIn](https://linkedin.com/in/johndoe)\n//\n// \u003cgno-columns-sep\u003e\n//\n// \u003cgno-columns-sep\u003e\n//\n// \u003c/gno-columns\u003e\n//\n// ## Best Practices\n// 1. Prioritize code readability over performance in Render() - use clear variable names, logical grouping, and readable concatenation for humans\n// 2. Use the md package for all programmatic markdown generation in Render() methods\n// 3. Combine functions for complex formatting: `md.Bold(md.Italic(\"text\"))`\n// 4. Use `md.Paragraph()` to ensure proper spacing between elements\n// 5. Leverage `md.ColumnsN()` for responsive layouts\n//\n// ## See Also\n// - [gno\\.land/p/moul/md/v0](https://gno.land/p/moul/md/v0) - The md package source code\n// - [Markdown on Gno](https://gno.land/r/docs/markdown) - Comprehensive guide on Gno Flavored Markdown syntax\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"hyzdVXsanDlKAO6l9lu/r2W8eQWvpmwzMSFBcEgFyvMg2WvXapciRZ4ZKVWAx4/Or4EbdM64U1D1viGmi+8hOQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"optional_render","path":"gno.land/r/docs/optional_render","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/optional_render\"\ngno = \"0.9\"\n"},{"name":"optional_render.gno","body":"package optional_render\n\nfunc Info() string {\n\treturn `Having a Render() function in your realm is optional!\nIf you do decide to have a Render() function, it must have the following signature:\nfunc Render(path string) string { ... }`\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"CSyVjrKPr3vs2kDzT2dLWPyNrogynlmngdHYygzDBdA+FWZ2rpgAgCTcuXTeE/WtLyj4ZgeKEKrx3vg345AaPw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"private","path":"gno.land/r/docs/private","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/private\"\ngno = \"0.9\"\nprivate = true\n"},{"name":"private.gno","body":"package private\n\nimport \"strings\"\n\n// Documentation for private realms in Gno.\nfunc Render(path string) string {\n\toutput := `# Private Realms\n\n## Introduction\n\nThis document describes the ±private± flag in ±gnomod.toml±.  \nThis flag is **optional** and can only be used by **realms**.\n\n\u003e [!NOTE]\n\u003e The ±private± flag does not make a realm secret or hidden.  \n\u003e All code remains open-source and visible on-chain.\n\n## Declaring a Private Realm\n\nTo mark a realm as private, add the following ±private± value in your ±gnomod.toml± file:\n\n±±±toml\nmodule = \"gno.land/r/demo/privaterealm\"\ngno = \"0.9\"\nprivate = true\n±±±\n\n## Rules of a Private Realm\n\nA realm marked as private follows strict rules:\n\n- It cannot be imported by other packages.\n- It can be re-uploaded and the new version fully overwrites the old one.\n- Memory, pointers, or types defined in this package cannot be stored elsewhere.\n\nThese restrictions aim to achieve one main goal:\n\n\u003e [!TIP]\n\u003e **Private realms are swappable.**  \n\u003e You can redeploy or update them without impacting other realms.  \n\n## Use Cases\n\nPrivate realms are useful when:\n\n- The realm represents a **home realm** for a specific user (±r/username/home±).\n- The realm is **designed to evolve** and may need to be replaced later.\n- You want to ensure that no external state depends on its internal types or values.\n`\n\toutput = strings.ReplaceAll(output, \"±\", \"`\")\n\treturn output\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"04RuY1nwmSDjX/iP5Xh6N8QoowStoIyNeI/15pe5U1RLisfdzZRsibq1cZjNlZujTqiEHl6L00PUSXl95bNdCQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"registry","path":"gno.land/r/docs/registry","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/registry\"\ngno = \"0.9\"\n"},{"name":"registry.gno","body":"// Package registry is a service registry.\n// Callers register, update, and remove named entries stored in an avl tree.\n// Each entry is owned by the address that created it.\npackage registry\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/markdown/sanitize/v0\"\n)\n\n// Service represents a registered service\ntype Service struct {\n\tName        string\n\tDescription string\n\tEndpoint    string\n\tOwner       address\n\tActive      bool\n}\n\nvar services avl.Tree // key (owner:name) -\u003e *Service\n\nconst (\n\tmaxNameLength        = 50\n\tmaxDescriptionLength = 200\n\tmaxEndpointLength    = 500\n)\n\n// Register adds a new service to the registry\nfunc Register(cur realm, name, description, endpoint string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(\"invalid realm\")\n\t}\n\tname = strings.TrimSpace(name)\n\tif name == \"\" {\n\t\tpanic(\"name cannot be empty\")\n\t}\n\tif len(name) \u003e maxNameLength {\n\t\tpanic(\"name too long\")\n\t}\n\tif len(description) \u003e maxDescriptionLength {\n\t\tpanic(\"description too long\")\n\t}\n\tif len(endpoint) \u003e maxEndpointLength {\n\t\tpanic(\"endpoint too long\")\n\t}\n\n\tcaller := cur.Previous().Address()\n\tkey := caller.String() + \":\" + name\n\n\tif services.Has(key) {\n\t\tpanic(\"service with this name from this address already exists\")\n\t}\n\n\tservice := \u0026Service{\n\t\tName:        name,\n\t\tDescription: description,\n\t\tEndpoint:    endpoint,\n\t\tOwner:       caller,\n\t\tActive:      true,\n\t}\n\n\tservices.Set(key, service)\n}\n\n// Update updates an existing service (owner only)\nfunc Update(cur realm, name, description, endpoint string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(\"invalid realm\")\n\t}\n\tif len(description) \u003e maxDescriptionLength {\n\t\tpanic(\"description too long\")\n\t}\n\tif len(endpoint) \u003e maxEndpointLength {\n\t\tpanic(\"endpoint too long\")\n\t}\n\n\tcaller := cur.Previous().Address()\n\tkey := caller.String() + \":\" + name\n\n\tval := services.Get(key)\n\tif val == nil {\n\t\tpanic(\"service not found\")\n\t}\n\n\tservice := val.(*Service)\n\tservice.Description = description\n\tservice.Endpoint = endpoint\n\tservices.Set(key, service)\n}\n\n// Deactivate marks a service as inactive (owner only)\nfunc Deactivate(cur realm, name string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(\"invalid realm\")\n\t}\n\tcaller := cur.Previous().Address()\n\tkey := caller.String() + \":\" + name\n\n\tval := services.Get(key)\n\tif val == nil {\n\t\tpanic(\"service not found\")\n\t}\n\n\tservice := val.(*Service)\n\tservice.Active = false\n\tservices.Set(key, service)\n}\n\n// Delete removes a service permanently (owner only)\nfunc Delete(cur realm, name string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(\"invalid realm\")\n\t}\n\tcaller := cur.Previous().Address()\n\tkey := caller.String() + \":\" + name\n\n\tif !services.Has(key) {\n\t\tpanic(\"service not found\")\n\t}\n\n\tservices.Remove(key)\n}\n\n// Lookup returns a copy of a service by owner address and name. Returning a\n// value (not the stored *Service) keeps callers from mutating registry state\n// through the returned pointer. See /r/docs/mistakes.\nfunc Lookup(owner address, name string) (Service, bool) {\n\tkey := owner.String() + \":\" + name\n\tval := services.Get(key)\n\tif val == nil {\n\t\treturn Service{}, false\n\t}\n\treturn *val.(*Service), true\n}\n\n// GetAllServices returns copies of all services, like Lookup.\nfunc GetAllServices() []Service {\n\tvar result []Service\n\tservices.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\tresult = append(result, *value.(*Service))\n\t\treturn false\n\t})\n\treturn result\n}\n\nfunc Render(path string) string {\n\tif path != \"\" {\n\t\t// Path format: \"address:name\". Split on the first colon only, since a\n\t\t// bech32 address never contains one but a name might.\n\t\tparts := strings.SplitN(path, \":\", 2)\n\t\tif len(parts) != 2 {\n\t\t\treturn \"# Invalid Path\\n\\nPath should be in format `address:name`.\"\n\t\t}\n\n\t\towner := address(parts[0])\n\t\tname := parts[1]\n\t\tservice, ok := Lookup(owner, name)\n\t\tif !ok {\n\t\t\treturn \"# Service Not Found\\n\\nNo service with address `\" + sanitize.InlineText(parts[0]) + \"` and name `\" + sanitize.InlineText(name) + \"` is registered.\"\n\t\t}\n\t\treturn renderService(service)\n\t}\n\n\treturn renderHome()\n}\n\nfunc renderHome() string {\n\tout := `# Registry Pattern\n\nAllows entities to register themselves in a shared\nindex. Other contracts can then look up registered entities.\nCommon use cases include mapping data, token discovery and realm showcasing.\n\n## Code Pattern\n\n` + \"```go\" + `\npackage registry\n\nimport (\n    \"gno.land/p/nt/avl/v0\"\n)\n\ntype Entry struct {\n    Name  string\n    Owner address\n    Data  string // Your custom payload\n}\n\nvar (\n    entries        avl.Tree // key (owner:name) -\u003e *Entry\n)\n\nfunc Register(cur realm, name, data string) {\n    if !cur.IsCurrent() {\n        panic(\"invalid realm\")\n    }\n    caller := cur.Previous().Address()\n    key := caller.String() + \":\" + name\n\n    if entries.Has(key) {\n        panic(\"you already have an entry with this name\")\n    }\n\n    entries.Set(key, \u0026Entry{\n        Name:  name,\n        Owner: caller,\n        Data:  data,\n    })\n}\n\n// Return a copy, not the stored *Entry.\nfunc Lookup(owner address, name string) (Entry, bool) {\n    key := owner.String() + \":\" + name\n    val := entries.Get(key)\n    if val == nil {\n        return Entry{}, false\n    }\n    return *val.(*Entry), true\n}\n` + \"```\" + `\n\n\n## This Example\n\nThis service registry allows anyone to register a service with a unique name.\nOnly the owner can update or deactivate their service, but anyone can look up\nservices by name.\n\n## Live Demo\n\n[Register Service](/r/docs/registry$help\u0026func=Register)\n\n### Registered Services\n\n`\n\tcount := services.Size()\n\n\tif count == 0 {\n\t\tout += \"_No services registered yet._\\n\"\n\t} else {\n\t\tservices.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\t\tservice := value.(*Service)\n\t\t\tstatus := \"✅\"\n\t\t\tif !service.Active {\n\t\t\t\tstatus = \"❌\"\n\t\t\t}\n\t\t\tout += \"- \" + status + \" [\" + sanitize.InlineText(service.Name) + \"](\" + sanitize.URL(\"/r/docs/registry:\"+key) + \")\\n\"\n\t\t\treturn false\n\t\t})\n\t}\n\n\tout += `\n---\n\n\n## Examples\n\n- [r/sys/users](/r/sys/users) - Official username registry. Users register\ntheir username and address, and anyone can look them up.\n\n- [r/nt/grc20reg/v0](/r/nt/grc20reg/v0) - GRC20 token registry.\nToken realms register themselves so other contracts can discover available tokens.\n\n- [r/leon/hor](/r/leon/hor) - Hall of Realms. Realms register themselves by\ncalling ` + \"`hor.Register()`\" + ` from their init function to be showcased in the exhibition.\n\n[View source code](/r/docs/registry$source)\n`\n\treturn out\n}\n\nfunc renderService(service Service) string {\n\tstatus := \"Active ✅\"\n\tif !service.Active {\n\t\tstatus = \"Inactive ❌\"\n\t}\n\n\treturn `# Service: ` + sanitize.InlineText(service.Name) + `\n\n## Details\n\n| Property | Value |\n|----------|-------|\n| Status | ` + status + ` |\n| Description | ` + sanitize.TableCell(service.Description) + ` |\n| Endpoint | ` + sanitize.TableCell(service.Endpoint) + ` |\n| Owner | ` + service.Owner.String() + ` |\n\n---\n\n[Back to Registry](/r/docs/registry)\n`\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"/1bzz+w24Rv4NRNg6jT6r9Uo/RDJSBoad3/gqNdFrJYnGC6XqP1iZXLHynBiv3QPz88/F4kmB9jcJcM4uOoBnw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"resolveusers","path":"gno.land/r/docs/resolveusers","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/resolveusers\"\ngno = \"0.9\"\n"},{"name":"resolve.gno","body":"package resolveusers\n\nimport (\n\t\"gno.land/r/sys/users\" // Import the user registry\n)\n\nfunc Render(path string) string {\n\tout := \"# Username checker\\n\\n\"\n\n\t// Default render\n\tif path == \"\" {\n\t\tout += `This is short example on how developers can use the gno.land user registry to resolve\nusernames and addresses in their realms. Check out the example below.\n\n---\n\n`\n\t\tout += \"Add `:{name OR address}` to the search bar to check for a name or address.\\n\\n\"\n\t\tout += \"Here are some examples:\\n\\n\"\n\t\tout += \"- [@test1](/r/docs/resolveusers:test1)\\n\"\n\t\tout += \"- [g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5](/r/docs/resolveusers:g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5)\\n\"\n\n\t\treturn out\n\t}\n\n\t// Try to resolve the search input in the registry\n\tdata, _ := users.ResolveAny(path)\n\tif data != nil {\n\t\tout += \"## Found the user you're looking for: \"\n\t\t// RenderLink will return a clickable gnoweb link leading to the user's page\n\t\tout += data.RenderLink(\"\")\n\t\treturn out\n\t}\n\n\t// Note: the above can be done with known inputs as well, ie ResolveName \u0026 ResolveAddress.\n\n\tout += \"## Didn't find that user :/\"\n\n\treturn out\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"H07zDmiyU0igjp2Moj2ddm7En+zfHI/aSbNY/r0kLeYI1oQqTH5tE5+94EQjo8BuCR9Kylb7FpCSMl6YYCc21g=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"routing","path":"gno.land/r/docs/routing","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/routing\"\ngno = \"0.9\"\n"},{"name":"routing.gno","body":"package routing\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/nt/mux/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/sys/users\"\n)\n\nfunc Render(path string) string {\n\t// Initialize the router\n\trouter := mux.NewRouter()\n\n\t// Then, pass specific path patterns and their function handlers\n\t// The handler functions need to have a specific signature:\n\t// func(*mux.ResponseWriter, *mux.Request)\n\n\t// Below are some examples for specific path patterns and their handlers\n\n\t// When no render path is passed, ie the root render\n\trouter.HandleFunc(\"\", homeHandler)\n\n\t// When a specific render path is passed\n\trouter.HandleFunc(\"staticpage\", staticpageHandler)\n\n\t// When a render path with a variable is passed, ie `addr`\n\trouter.HandleFunc(\"user/{name}\", profileHandler)\n\n\t// When a wildcard path is passed\n\trouter.HandleFunc(\"wildcard/*/\", wildcardHandler)\n\n\t// Finally, Pass the render path to the router\n\treturn router.Render(path)\n}\n\nfunc homeHandler(res *mux.ResponseWriter, _ *mux.Request) {\n\tout := \"# Routing\\n\\n\"\n\n\tout += `This short example showcases how the [p/demo/mux](/p/demo/mux) package works.\nThis pure package aims to offer similar functionality to [**http.ServeMux**](https://pkg.go.dev/net/http#ServeMux) in Go, but for Gno's **Render()** requests.\n\nThis home page is handled by the homeHandler function. Check out the examples below for more ways\nto use this package:\n- [A static page](/r/docs/routing:staticpage)\n- [A path handling path variables, such as user pages](/r/docs/routing:user/test1)\n- [A path handling wildcard](/r/docs/routing:wildcard/wildcardhelp/whatever)\n`\n\n\t// Write to the result at the end\n\tres.Write(out)\n}\n\nfunc staticpageHandler(res *mux.ResponseWriter, req *mux.Request) {\n\tout := \"# Welcome to the static page!\\n\\n\"\n\tout += \"There isn't much on this page, but it's cool because shows you that using query parameters is also possible!\\n\\n\"\n\tout += \"Try adding [`?gno_is=value`](/r/docs/routing:staticpage?gno_is=cool) to the URL.\\n\\n\"\n\n\t// We can use the Query API to get the values of query parameters\n\tval := req.Query.Get(\"gno_is\")\n\tif val != \"\" {\n\t\tout += ufmt.Sprintf(\"You inputted a value for the parameter `gno_is`: **%s**\\n\\n\", val)\n\n\t}\n\n\tout += \"### Multiple values for the same parameter\\n\\n\"\n\tout += \"The same parameter can have [more than one value](/r/docs/routing:staticpage?gno_is=cool\u0026gno_is=transparent).\\n\\n\"\n\tout += \"We can access it via the `*mux.Request.Query` map.\\n\\n\"\n\n\tres.Write(out)\n}\n\nfunc profileHandler(res *mux.ResponseWriter, req *mux.Request) {\n\tout := \"# User display page\\n\\n\"\n\n\t// Integrate with r/sys/users to get user data\n\tname := req.GetVar(\"name\")\n\tuserData, _ := users.ResolveName(name)\n\tif userData == nil {\n\t\tout += \"This name does not exist in the [User Registry](/r/sys/users)!\"\n\t\tres.Write(out)\n\t\treturn\n\t}\n\n\tout += \"Found an address matching the name:\\n\\n\"\n\tout += userData.RenderLink(\"\")\n\tres.Write(out)\n}\n\nfunc wildcardHandler(res *mux.ResponseWriter, req *mux.Request) {\n\tout := \"# Wildcard handler\\n\\n\"\n\n\t// Use GetVar(\"*\") to access the wildcard part of the route\n\twild := req.GetVar(\"*\")\n\tout += ufmt.Sprintf(\"This page matched a wildcard route.\\n\\n\")\n\tout += ufmt.Sprintf(\"**Wildcard path:** `%s`\\n\\n\", wild)\n\n\tout += \"You can use this to match and handle arbitrarily nested paths.\\n\\n\"\n\tout += \"Try visiting:\\n\"\n\tout += \"- [/r/docs/routing:wildcard/foo/bar](/r/docs/routing:wildcard/foo/bar)\\n\"\n\tout += \"- [/r/docs/routing:wildcard/a/b/c](/r/docs/routing:wildcard/a/b/c)\\n\"\n\n\t// Optionally split the wildcard into segments\n\tsegments := strings.Split(wild, \"/\")\n\tout += \"\\n### Wildcard segments:\\n\"\n\tfor i, seg := range segments {\n\t\tout += ufmt.Sprintf(\"- Segment %d: `%s`\\n\", i+1, seg)\n\t}\n\n\tres.Write(out)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"00CoFYxvlPTxrzgWSeM1xvgMqAx50PZoX0Jf9a9TEt1AvAZLaWB0JehoES72LhUN7ikgS9mIyXxBmDI7oKVnvQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"safeobjects","path":"gno.land/r/docs/safeobjects","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/safeobjects\"\ngno = \"0.9\"\n"},{"name":"safeobjects.gno","body":"package safeobjects\n\nimport (\n\t\"chain/runtime/unsafe\"\n)\n\n// ExposedSafeObject methods exposed to the public. However, the methods all\n// contain a caller check - making them, and the object itself \"safe\".\n// The methods are only callable by the current admin.\n// A prime example of a safe object can be found in \"p/demo/ownable\".\nvar ExposedSafeObject = SafeObject{\n\tprivilegedData: \"This message can only be set by the admin.\",\n\tadmin:          address(\"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\"),\n}\n\n// SafeObject is an object that contains some privileged data\n// This data might be privileged because only certain users\n// have the edit rights, or for other reasons.\ntype SafeObject struct {\n\tprivilegedData string\n\tadmin          address\n}\n\n// Set is a function only the admin can call\nfunc (so SafeObject) Set(value string) {\n\tif unsafe.CurrentRealm().Address() != so.admin {\n\t\tpanic(\"caller is not authorized\")\n\t}\n\n\tso.privilegedData = value\n}\n\n// UpdateAdmin is a function only the admin can call\nfunc (so SafeObject) UpdateAdmin(newAdmin address) {\n\tif unsafe.CurrentRealm().Address() != so.admin {\n\t\tpanic(\"caller is not authorized\")\n\t}\n\n\tif !newAdmin.IsValid() {\n\t\tpanic(\"new admin address is invalid\")\n\t}\n\n\tso.admin = newAdmin\n}\n\n// Get is callable by anyone\nfunc (so SafeObject) Get() string {\n\treturn so.privilegedData\n}\n\nfunc Render(_ string) string {\n\treturn `\n# Safe Objects\n\n**Safe Objects** are objects that can be exposed at the realm top level\nwhile still keeping the write access to their memory limited.\n\nSafe Objects allow only authorized users (like admins) can modify their\ninternal state, even if they are exposed at the realm top level.\n\nA prime example of a commonly used safe object is the Ownable object, found under in [**p/demo/ownable**](/p/demo/ownable).\n\nTo call methods on exposed safe objects, users need to use [MsgRun](/r/docs/complexargs).\n\n---\n\n` + \"`r/docs/safeobjects.ExposedSafeObject`\" + ` values:\n- Message: ` + ExposedSafeObject.privilegedData + `\n- Admin: ` + ExposedSafeObject.admin.String()\n\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"5WsvRH2TiBvJ/SMgF4U+pCbKd+8M4+Yjuib7WliB3FVzRB/RiB/nbIn39qCr5JglIk5dbzotHqBo2g9LG0oa4w=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"soliditypatterns","path":"gno.land/r/docs/soliditypatterns","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/soliditypatterns\"\ngno = \"0.9\"\n"},{"name":"soliditypatterns.gno","body":"package soliditypatterns\n\nfunc Render(_ string) string {\n\tcontent := `# Common Solidity Patterns\n\nThis is a list of common Solidity patterns, converted and explained to match Gno best practices.\n\n## Examples\n\n- [Basic Counter](/r/docs/soliditypatterns/counter) - Increment and decrement a counter stored in a contract.\n- [Only Owner can change](/r/docs/soliditypatterns/ownable) - Simple example of ownership over a contract.\n- [State Lock](/r/docs/soliditypatterns/statelock) - Modifiers.\n- [Sending Coins (Banker)](/r/docs/soliditypatterns/banker) - The Gno equivalent of Solidity's payable and ETH transfers.\n- [Reentrancy](/r/docs/soliditypatterns/reentrancy) - Why reentrancy attacks are NOT possible in Gno.\n\n\u003e [!NOTE]\n\u003e  If you have experience with Solidity, or a common pattern is missing from this list, please add more examples to the [Monorepo](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/docs/soliditypatterns)!\n\n`\n\treturn content\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"xD94XkaUZoxmNprCsuzAnDMpiDvuIerEZ44rNR25uKYUYQ8SfIk5cykmd9sX58sxTj55JSG7KLMSFF8TcNrblQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"banker","path":"gno.land/r/docs/soliditypatterns/banker","files":[{"name":"banker.gno","body":"// Package banker recreates Solidity's payable/withdraw pattern in Gno.\n// It accepts deposits and lets callers withdraw their balance through the chain banker.\npackage banker\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"chain/runtime/unsafe\"\n\t\"strconv\"\n\n\t\"gno.land/p/nt/avl/v0\"\n)\n\n// This example demonstrates the Gno equivalent of Solidity's\n// payable functions and ETH transfers.\n\nvar deposits avl.Tree // address -\u003e int64\n\n// Deposit accepts coins and tracks the sender's balance\n// In Solidity, this would be a payable function\nfunc Deposit(cur realm) {\n\tif !cur.IsCurrent() {\n\t\tpanic(\"invalid realm\")\n\t}\n\n\t// Require a direct user call so coins are credited to the real sender.\n\tif !cur.Previous().IsUserCall() {\n\t\tpanic(\"invalid non-user call\")\n\t}\n\n\tcaller := cur.Previous().Address()\n\tsent := unsafe.OriginSend()\n\n\t// Check that coins were sent\n\tamount := sent.AmountOf(\"ugnot\")\n\tif amount \u003c= 0 {\n\t\tpanic(\"must send some ugnot\")\n\t}\n\n\t// Get current balance\n\tcurrentBalance := int64(0)\n\tif val := deposits.Get(caller.String()); val != nil {\n\t\tcurrentBalance = val.(int64)\n\t}\n\n\t// Update balance\n\tdeposits.Set(caller.String(), currentBalance+amount)\n}\n\n// Withdraw sends coins back to the caller\n// This is equivalent to Solidity's transfer() or call{value: x}()\nfunc Withdraw(cur realm, amount int64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(\"invalid realm\")\n\t}\n\n\t// Require a direct user call so only the balance owner can withdraw.\n\tif !cur.Previous().IsUserCall() {\n\t\tpanic(\"invalid non-user call\")\n\t}\n\n\t// A negative amount would inflate the balance and send negative coins.\n\tif amount \u003c= 0 {\n\t\tpanic(\"amount must be positive\")\n\t}\n\n\tcaller := cur.Previous().Address()\n\n\t// Get current balance\n\tcurrentBalance := int64(0)\n\tif val := deposits.Get(caller.String()); val != nil {\n\t\tcurrentBalance = val.(int64)\n\t}\n\n\t// Check balance\n\tif currentBalance \u003c amount {\n\t\tpanic(\"insufficient balance\")\n\t}\n\n\tnewBalance := currentBalance - amount\n\tif newBalance == 0 {\n\t\tdeposits.Remove(caller.String())\n\t} else {\n\t\tdeposits.Set(caller.String(), newBalance)\n\t}\n\n\t// Create a banker to send coins\n\tbnk := banker.NewBanker(banker.BankerTypeRealmSend, cur)\n\n\t// Get the realm's address\n\trealmAddr := cur.Address()\n\n\t// Send coins from realm to caller\n\tbnk.SendCoins(realmAddr, caller, chain.Coins{chain.NewCoin(\"ugnot\", amount)})\n}\n\n// GetBalance returns the deposited balance for an address\nfunc GetBalance(addr address) int64 {\n\tbalance := deposits.Get(addr.String())\n\tif balance == nil {\n\t\treturn 0\n\t}\n\treturn balance.(int64)\n}\n\n// GetRealmBalance returns the total coins held by this realm\nfunc GetRealmBalance() int64 {\n\tbnk := banker.NewReadonlyBanker()\n\trealmAddr := unsafe.CurrentRealm().Address()\n\tcoins := bnk.GetCoins(realmAddr)\n\treturn coins.AmountOf(\"ugnot\")\n}\n\nfunc RenderDemo() string {\n\tout := \"## Live Demo\\n\\n\"\n\tout += \"Realm Balance: **\" + strconv.FormatInt(GetRealmBalance(), 10) + \" ugnot**\\n\\n\"\n\tout += \"[Deposit](/r/docs/soliditypatterns/banker$help\u0026func=Deposit) | \"\n\tout += \"[Withdraw](/r/docs/soliditypatterns/banker$help\u0026func=Withdraw)\\n\"\n\treturn out\n}\n\nfunc Render(_ string) string {\n\treturn `# Sending Coins (Banker)\n\nIn Solidity, you send ETH using ` + \"`transfer()`\" + `, ` + \"`send()`\" + `, or ` + \"`call{value: x}()`\" + `.\nIn Gno, you use the **Banker** to send coins.\n\n` + RenderDemo() + `\n## Solidity vs Gno Comparison\n\n### Solidity: Receiving ETH\n\n` + \"```solidity\" + `\n// Payable function receives ETH\nfunction deposit() public payable {\n    balances[msg.sender] += msg.value;\n}\n` + \"```\" + `\n\n### Gno: Receiving Coins\n\n` + \"```go\" + `\nimport \"chain/runtime/unsafe\"\n\nvar deposits map[address]int64\n\nfunc Deposit(cur realm) {\n    if !cur.IsCurrent() {\n        panic(\"invalid realm\")\n    }\n    caller := cur.Previous().Address()\n    sent := unsafe.OriginSend()\n\n    // Unlike Solidity's single msg.value, Gno supports multiple coin types\n    // A user can send ugnot, custom tokens, or both in one transaction\n    amount := sent.AmountOf(\"ugnot\")\n    deposits[caller] += amount\n}\n` + \"```\" + `\n\n\u003e **Note:** This example uses a map for simplicity. In production, prefer an AVL tree\n\u003e for better performance. See [source code](/r/docs/soliditypatterns/banker$source).\n\n---\n\n### Solidity: Sending ETH\n\n` + \"```solidity\" + `\n// Using transfer (reverts on failure)\npayable(recipient).transfer(amount);\n\n// Using call\n(bool success, ) = recipient.call{value: amount}(\"\");\nrequire(success, \"Transfer failed\");\n` + \"```\" + `\n\n### Gno: Sending Coins\n\n` + \"```go\" + `\nimport (\n    \"chain\"\n    \"chain/banker\"\n    \"chain/runtime/unsafe\"\n)\n\nvar deposits map[address]int64\n\nfunc Withdraw(cur realm, amount int64) {\n    if !cur.IsCurrent() {\n        panic(\"invalid realm\")\n    }\n    caller := cur.Previous().Address()\n\n    // Update state before sending (checks-effects-interactions)\n    deposits[caller] -= amount\n\n    // Create banker with send capability\n    bnk := banker.NewBanker(banker.BankerTypeRealmSend, cur)\n\n    // Send coins\n    realmAddr := cur.Address()\n    bnk.SendCoins(realmAddr, caller, chain.Coins{chain.NewCoin(\"ugnot\", amount)})\n}\n` + \"```\" + `\n\n## Banker Types\n\n| Type | Purpose |\n|------|---------|\n| ` + \"`BankerTypeReadonly`\" + ` | Read balances only |\n| ` + \"`BankerTypeRealmSend`\" + ` | Send coins from realm |\n| ` + \"`BankerTypeRealmIssue`\" + ` | Issue new realm-specific tokens |\n\n## Key Differences from Solidity\n\nGno requires an explicit Banker with appropriate permissions.\nReentrancy attacks are not possible (see [Reentrancy](/r/docs/soliditypatterns/reentrancy)).\nMultiple coin denominations are supported, not just one native token.\n\n[View source code](/r/docs/soliditypatterns/banker$source)\n\n---\n\n[Back to Solidity Patterns](/r/docs/soliditypatterns) | [Banker Documentation](https://docs.gno.land/resources/gno-stdlibs/#banker)\n`\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/soliditypatterns/banker\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"QHxwJZ13PMjD0VGia77letu6ww2wu5GdiA5Vlb/xd1VtZjZPQgSrfDKQLfUIy3PbB/khCDUIDlmv99xRsT22TA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"counter","path":"gno.land/r/docs/soliditypatterns/counter","files":[{"name":"demo.gno","body":"package counter\n\nimport \"strconv\"\n\nvar count int64 = 0\n\nfunc Increment(cur realm) {\n\tcount++\n}\n\nfunc Decrement(cur realm) {\n\tcount--\n}\n\nfunc GetCount(cur realm) int64 {\n\treturn count\n}\n\nfunc RenderDemo() string {\n\tcontent := \"## Live Demo\\n\"\n\tcontent += \"Counter Value : **\" + strconv.FormatInt(count, 10) + \"**\\n\\n\"\n\tcontent += \"[Increment](counter$help\u0026func=Increment)\\n\\n\"\n\tcontent += \"[Decrement](counter$help\u0026func=Decrement)\\n\\n\"\n\treturn content\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/soliditypatterns/counter\"\ngno = \"0.9\"\n"},{"name":"render.gno","body":"package counter\n\nimport \"strings\"\n\nfunc Render(path string) string {\n\tcontent := `\n# Counter\n\nThis Solidity contract defines a counter that can be incremented or decremented by anyone.\n\n---\n\n### Solidity Version\n\n^^^solidity\ncontract Counter {\n    int public count = 0;\n\n    function increment() public {\n        count++;\n    }\n\n    function decrement() public {\n        count--;\n    }\n}\n^^^\n\n#### Explanation\n\nThis Solidity contract does:\n\n* Declare a ^count^ variable readable by anyone initialized at 0\n* Define two functions, ^increment^ and ^decrement^, that increase and decrease the counter by 1.\n* These functions are marked ^public^, meaning anyone can call them\n\n---\n\n### Gno Version\n\n^^^go\npackage counter\n\nvar count int64 = 0\n\nfunc Increment(cur realm) {\n    count++\n}\n\nfunc Decrement(cur realm) {\n    count--\n}\n^^^\n\n* We define a global variable ^count^ of type ^int64^. Since it is declared at the package level, its value is **persistent**, just like in Solidity. As it is capitalized, it can be accessed from outside the package (read-only).\n* We provide two functions: ^Increment^ and ^Decrement^, which modify the counter value using Go-style syntax (^count++^ and ^count--^). As they are capitalized, they can be called from outside the package.\n \n---\n`\n\treturn strings.ReplaceAll(content+RenderDemo(), \"^\", \"`\")\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"nhAFPQZ02aRxhC9LaspHb+See3OCA+PtAlMiTEh4mpcyUmEAh7S3s6Oa/TdYcM2/DyeXtLKrjj6Mp9s9tBH1xQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"ownable","path":"gno.land/r/docs/soliditypatterns/ownable","files":[{"name":"demo.gno","body":"package ownable\n\nimport (\n\t\"chain/runtime/unsafe\"\n)\n\nvar owner address = \"\"\n\nfunc InitOwner(cur realm) {\n\tif len(owner) != 0 {\n\t\tpanic(\"owner already defined\")\n\t}\n\towner = cur.Previous().Address()\n}\n\nfunc assertOnlyOwner() {\n\tif unsafe.PreviousRealm().Address() != owner {\n\t\tpanic(\"caller isn't the owner\")\n\t}\n}\n\nfunc ChangeOwner(cur realm, newOwner address) {\n\tassertOnlyOwner()\n\towner = newOwner\n}\n\nfunc RenderDemo() string {\n\tcontent := \"## Live Demo\\n\"\n\tcontent += \"Owner address: **\" + owner.String() + \"**\\n\\n\"\n\tcontent += \"[InitOwner](ownable$help\u0026func=InitOwner)\\n\\n\"\n\tcontent += \"[ChangeOwner](ownable$help\u0026func=ChangeOwner)\\n\\n\"\n\treturn content\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/soliditypatterns/ownable\"\ngno = \"0.9\"\n"},{"name":"render.gno","body":"package ownable\n\nimport \"strings\"\n\nfunc Render(path string) string {\n\tcontent := `\n# Ownable\n\nThis Solidity contract defines a counter that can be incremented or decremented by anyone.\n\n---\n\n### Solidity Version\n\n^^^solidity\ncontract OnlyOwnerCanChange {\n    address private owner;\n\n    constructor() {\n        owner = msg.sender;\n    }\n\n    modifier onlyOwner {\n        require(msg.sender == owner, \"Not owner\");\n        _;\n    }\n\n    function changeOwner(address memory newOwner) public onlyOwner {\n        owner = newOwner\n    }\n}\n^^^\n\n* It declares a state variable ^owner^ of type ^address^, which stores the address of the account that deployed the contract.\n* The ^constructor^ sets the ^owner^ variable to the address that deployed the contract using ^msg.sender^.\n* It defines a ^modifier onlyOwner^ that restricts function execution to the owner only, using ^require^ to check that the caller is the current ^owner^. The special symbol ^_;^ is where the modified function's code runs.\n* It defines a ^changeOwner^ function that allows the current owner to assign a new owner address. The function uses the ^onlyOwner^ modifier to enforce access control.\n\n---\n## Gno Version\n\n^^^go\npackage onlyownercanchange\n\nimport \"chain/runtime/unsafe\"\n\nvar owner address = \"\"\n\nfunc InitOwner(cur realm) {\n\tif len(owner) != 0 {\n\t\tpanic(\"owner already defined\")\n\t}\n\towner = unsafe.PreviousRealm().Address()\n}\n\nfunc assertOnlyOwner() {\n\tif unsafe.PreviousRealm().Address() != owner {\n\t\tpanic(\"caller isn't the owner\")\n\t}\n}\n\nfunc ChangeOwner(cur realm, newOwner address) {\n\tassertOnlyOwner()\n\towner = newOwner\n}\n^^^\n\n* We declare a persistent global variable ^owner^ of type ^address^ (an alias for ^string^ in Gno), initialized to the empty string ^\"\"^. This holds the address of the contract owner.\n* The function ^InitOwner^ is used to initialize the ^owner^. It can only be called once: if ^owner^ is already set (non-empty), it throws an error using ^panic^. It sets the ^owner^ to the address of the previous realm (the caller) via ^unsafe.PreviousRealm().Address()^, which is similar to ^msg.sender^ in Solidity.\n* The helper function ^assertOnlyOwner^ checks that the caller is the current ^owner^ by comparing ^unsafe.PreviousRealm().Address()^ with ^owner^. If not, it panics. This is equivalent to a Solidity ^modifier onlyOwner^.\n* The function ^ChangeOwner^ allows the current owner to transfer ownership to a new address. It first calls ^assertOnlyOwner()^ to ensure that only the current owner can do this.\n* All functions are capitalized (exported), meaning they can be called externally. However, ^assertOnlyOwner^ is unexported (lowercase), meaning it's only usable within the package.\n\n\n\u003e [!NOTE]\n\u003e For a library that handles this for you, check out [^/p/demo/ownable^](/p/demo/ownable).\n\n---\n`\n\treturn strings.ReplaceAll(content+RenderDemo(), \"^\", \"`\")\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"kKkKmUQPFkrsbFG2sJWWR0ltmp8dcW3fKPOBBzfPf6cNEjfJiCVUIl9AuNUJn0KWq9qKsBB2wD6X0uSQHXtaRQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"reentrancy","path":"gno.land/r/docs/soliditypatterns/reentrancy","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/soliditypatterns/reentrancy\"\ngno = \"0.9\"\n"},{"name":"reentrancy.gno","body":"// Package reentrancy explains why Gno is not exposed to Solidity-style reentrancy.\n// It shows how to keep transfers safe.\npackage reentrancy\n\nfunc Render(_ string) string {\n\treturn `# Reentrancy in Gno\n\nIn Solidity, sending ETH automatically triggers the recipient's ` + \"`receive()`\" + ` or ` + \"`fallback()`\" + `\nfunction. An attacker can exploit this to call back into the original contract before state\nupdates complete. The infamous DAO hack used this to drain $60M.\n\n## The Solidity Problem\n\n` + \"```solidity\" + `\ncontract VulnerableVault {\n    mapping(address =\u003e uint) balances;\n\n    function withdraw() public {\n        uint amount = balances[msg.sender];\n        msg.sender.call{value: amount}(\"\");  // 1. Sends ETH, triggering attacker's receive()\n        balances[msg.sender] = 0;            // 3. State update happens too late!\n    }\n}\n\ncontract Attacker {\n    VulnerableVault vault;\n\n    receive() external payable {\n        if (address(vault).balance \u003e= 1 ether) {\n            vault.withdraw();  // 2. Re-enters withdraw() while balance is still non-zero\n        }\n    }\n}\n` + \"```\" + `\n\n## Why Gno is Safe\n\nCoin transfers in Gno do **not** execute code on the recipient. Unlike Solidity's\n` + \"`receive()`\" + ` and ` + \"`fallback()`\" + ` hooks, Gno realm code only runs through explicit\n` + \"`cross`\" + ` function calls—eliminating the reentrancy attack vector entirely.\n\n` + \"```go\" + `\nfunc Withdraw(cur realm, amount int64) {\n    if !cur.IsCurrent() {\n        panic(\"invalid realm\")\n    }\n    // Require a direct user call so only the balance owner can withdraw.\n    if !cur.Previous().IsUserCall() {\n        panic(\"invalid non-user call\")\n    }\n\n    caller := cur.Previous().Address()\n\n    bnk := banker.NewBanker(banker.BankerTypeRealmSend, cur)\n    bnk.SendCoins(cur.Address(), caller, chain.Coins{chain.NewCoin(\"ugnot\", amount)})  // No code executes on recipient\n    deposits[caller] -= amount  // Safe - no reentrancy possible\n}\n` + \"```\" + `\n\n## Best Practice: Checks-Effects-Interactions\n\nEven though Gno prevents reentrancy attacks, following the **Checks-Effects-Interactions**\npattern keeps your code clear and maintainable:\n\n` + \"```go\" + `\nfunc Withdraw(cur realm, amount int64) {\n    // 1. Checks - validate all preconditions\n    if !cur.IsCurrent() {\n        panic(\"invalid realm\")\n    }\n    if !cur.Previous().IsUserCall() {\n        panic(\"invalid non-user call\")\n    }\n\n    caller := cur.Previous().Address()\n    balance := deposits[caller]\n    if balance \u003c amount {\n        panic(\"insufficient balance\")\n    }\n\n    // 2. Effects - update state before external calls\n    deposits[caller] -= amount\n\n    // 3. Interactions - perform external operations last\n    bnk := banker.NewBanker(banker.BankerTypeRealmSend, cur)\n    bnk.SendCoins(cur.Address(), caller, chain.Coins{chain.NewCoin(\"ugnot\", amount)})\n}\n` + \"```\" + `\n\nThis pattern remains a best practice for code clarity and future-proofing, even when\nreentrancy isn't a concern.\n\n---\n\n\n## See Also\n\n- [Banker](/r/docs/soliditypatterns/banker) - Coin transfer patterns\n`\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"XAIZTWNQoulA0bCa6pe7eF0O3CK4f0904n8yluAEQJ1lQbs1oS/GQGT/e70ICzK5W6g966ZYgzyuzQrizDbe7Q=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"statelock","path":"gno.land/r/docs/soliditypatterns/statelock","files":[{"name":"demo.gno","body":"package statelock\n\ntype State string\n\nconst (\n\tStateActive   State = \"Active\"\n\tStateLocked   State = \"Locked\"\n\tStateDisabled State = \"Disabled\"\n)\n\nvar state State = StateActive\n\nfunc assertInState(expected State) {\n\tif state != expected {\n\t\tpanic(\"invalid state: expected \" + expected + \", got \" + state)\n\t}\n}\n\nfunc Lock(cur realm) {\n\tassertInState(StateActive)\n\tstate = StateLocked\n}\n\nfunc Unlock(cur realm) {\n\tassertInState(StateLocked)\n\tstate = StateActive\n}\n\nfunc RenderDemo() string {\n\tcontent := \"## Live Demo\\n\"\n\tcontent += \"Contract State : **\" + string(state) + \"**\\n\\n\"\n\tcontent += \"[Lock](statelock$help\u0026func=Lock)\\n\\n\"\n\tcontent += \"[Unlock](statelock$help\u0026func=Unlock)\\n\\n\"\n\treturn content\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/soliditypatterns/statelock\"\ngno = \"0.9\"\n"},{"name":"render.gno","body":"package statelock\n\nimport \"strings\"\n\nfunc Render(path string) string {\n\tcontent := `\n# State Lock\n\nThis example showcases how a Solidity modifier might be implemented in Gno.\n\n---\n\n## In Solidity\n\n^^^solidity\ncontract StateLock {\n    enum State { Active, Locked, Disabled }\n\n    State public state = State.Active;\n\n    modifier inState(State _state) {\n        require(state == _state);\n        _;\n    }\n\n    function lock() public inState(State.Active) {\n        state = State.Locked;\n    }\n\n    function unlock() public inState(State.Locked) {\n        state = State.Active;\n    }\n}\n^^^\n\n* An ^enum State^ is declared with three possible values: ^Active^, ^Locked^, and ^Disabled^.\n* A ^public^ state variable ^state^ is declared to hold the current state. It can be read externally thanks to the ^public^ keyword.\n* A ^modifier inState^ is defined to restrict function execution based on the current ^state^. It checks that ^state == _state^ using ^require^, and then allows the function body to continue.\n* The ^lock^ function can only be called if the contract is in the ^Created^ state. When called, it changes the state to ^Locked^.\n\n\n---\n\n## Gno Version\n\n^^^go\ntype State string\n\nconst (\n\tStateActive   State = \"Active\"\n\tStateLocked   State = \"Locked\"\n\tStateDisabled State = \"Disabled\"\n)\n\nvar state State = StateActive\n\nfunc assertInState(expected State) {\n\tif state != expected {\n\t\tpanic(\"invalid state: expected \" + expected + \", got \" + state)\n\t}\n}\n\nfunc Lock(cur realm) {\n\tassertInState(StateActive)\n\tstate = StateLocked\n}\n\nfunc Unlock(cur realm) {\n\tassertInState(StateLocked)\n\tstate = StateActive\n}\n^^^\n\n* We define a custom type ^State^ as a ^string^ to simulate enum-like behavior.\n* We declare a ^const^ block listing all valid states: ^StateActive^, ^StateLocked^, and ^StateDisabled^. These are as the Solidity ^Enum^.\n* A global variable ^state^ of type ^State^ is initialized to ^StateActive^.\n* The helper function ^assertInState^ ensures that the contract is in the expected state. If not, it throws an error using ^panic^, showing both the expected and actual state values. This is equivalent to a Solidity ^modifier^.\n* The ^Lock^ function changes the state from ^Active^ to ^Locked^. It first ensures the state is currently ^Active^ using ^assertInState^.\n* The ^Unlock^ function reverses the state from ^Locked^ to ^Active^, again checking the current state before proceeding.\n\n---\n\n`\n\treturn strings.ReplaceAll(content+RenderDemo(), \"^\", \"`\")\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"lztfHUYEdGf4t98vqUGbGhjtLwuau74YThay9DKp3yJE1XUg5uv39Hm2PnOkhVJKdh/ShM2Lu6rk2YAQaZ2XAQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"source","path":"gno.land/r/docs/source","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/source\"\ngno = \"0.9\"\n"},{"name":"source.gno","body":"package source\n\n// Welcome to the source code of this realm!\n\nfunc Render(_ string) string {\n\treturn `# Viewing source code  \ngno.land makes it easy to view the source code of any pure\npackage or realm, by using ABCI queries.\n\ngno.land's web frontend, ` + \"`gnoweb`, \" + ` makes this easy by\nproviding a intuitive UI that fetches the source of the\nrealm, that you can inspect anywhere by simply clicking\non the [source] button.\n\nCheck it out in the top right corner!\n`\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"nGRYU14XuH06nYPF/yiwEY2iGujMSp20ZSgVYDEEsaMn0yPL2xvP/aPHX/00UBPF2lX7M+Hwq7E9i5lE+9is4Q=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"tdd","path":"gno.land/r/docs/tdd","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/tdd\"\ngno = \"0.9\"\n"},{"name":"tdd.gno","body":"// Package tdd demonstrates test-driven development with a small calculator.\npackage tdd\n\n// Calculator provides simple arithmetic operations\n\nvar lastResult int64\n\nfunc Add(_ realm, a, b int64) int64 {\n\tlastResult = a + b\n\treturn lastResult\n}\n\nfunc Subtract(_ realm, a, b int64) int64 {\n\tlastResult = a - b\n\treturn lastResult\n}\n\nfunc Multiply(_ realm, a, b int64) int64 {\n\tlastResult = a * b\n\treturn lastResult\n}\n\nfunc Divide(_ realm, a, b int64) int64 {\n\tif b == 0 {\n\t\tpanic(\"division by zero\")\n\t}\n\tlastResult = a / b\n\treturn lastResult\n}\n\nfunc GetLastResult() int64 {\n\treturn lastResult\n}\n\nfunc Render(_ string) string {\n\treturn `# Test-Driven Development (TDD) in Gno\n\nTDD is writing tests BEFORE writing code.\n\n## The TDD Cycle\n\n1. **Red** - Write a failing test\n2. **Green** - Write minimal code to pass\n3. **Refactor** - Improve while keeping tests green\n\n## Quick Example\n\nFirst, write the test. A function that changes realm state takes a cur realm\nparameter, so the test takes one too and calls it across the realm boundary\nwith cross(cur):\n\n` + \"```go\" + `\nimport \"testing\"\n\n// calculator_test.gno\nfunc TestAdd(cur realm, t *testing.T) {\n    result := Add(cross(cur), 2, 3)\n    if result != 5 {\n        t.Errorf(\"Add(2, 3) = %d; want 5\", result)\n    }\n}\n` + \"```\" + `\n\nThen write the code:\n\n` + \"```go\" + `\n// calculator.gno\nfunc Add(_ realm, a, b int64) int64 {\n    return a + b\n}\n` + \"```\" + `\n\n## Table-Driven Tests\n\nTest multiple cases efficiently:\n\n` + \"```go\" + `\nfunc TestAdd(cur realm, t *testing.T) {\n    tests := []struct {\n        a, b, want int64\n    }{\n        {2, 3, 5},\n        {-2, -3, -5},\n        {0, 5, 5},\n    }\n\n    for _, tt := range tests {\n        got := Add(cross(cur), tt.a, tt.b)\n        if got != tt.want {\n            t.Errorf(\"Add(%d, %d) = %d; want %d\",\n                tt.a, tt.b, got, tt.want)\n        }\n    }\n}\n` + \"```\" + `\n\n## Testing Panics\n\nA panic that crosses a realm boundary aborts the program, so it can't be caught\nwith recover. Use the revive builtin to assert that a cross-call panics:\n\n` + \"```go\" + `\nfunc TestDivideByZero(cur realm, t *testing.T) {\n    rec := revive(func() {\n        Divide(cross(cur), 10, 0)\n    })\n    if rec == nil {\n        t.Error(\"expected panic\")\n    }\n}\n` + \"```\" + `\n\n## Testing with Addresses\n\n` + \"```go\" + `\nimport \"gno.land/p/nt/testutils/v0\"\n\nfunc TestWithCaller(cur realm, t *testing.T) {\n    addr := testutils.TestAddress(\"user1\")\n    testing.SetOriginCaller(addr)\n\n    result := SomeFunctionThatChecksCaller(cross(cur))\n    // Assert...\n}\n` + \"```\" + `\n\n## Testing with Coins\n\n` + \"```go\" + `\nfunc TestWithCoins(cur realm, t *testing.T) {\n    testing.SetOriginSend(chain.Coins{chain.NewCoin(\"ugnot\", 1000)})\n\n    result := SomePayableFunction(cross(cur))\n    // Assert...\n}\n` + \"```\" + `\n\n## Running Tests\n\n` + \"```bash\" + `\ngno test .              # Run all tests\ngno test -v .           # Verbose output\ngno test -run TestAdd . # Run specific test\n` + \"```\" + `\n\n---\n\n### See also:\n- [gno test documentation](https://docs.gno.land/reference/gno-cli/gno-test)\n- [Crossing](/r/docs/crossing) - How to test realm boundaries\n- [Common Mistakes](/r/docs/mistakes) - Pitfalls to avoid\n`\n}\n"},{"name":"tdd_test.gno","body":"package tdd\n\nimport \"testing\"\n\nfunc TestAdd(cur realm, t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\ta, b     int64\n\t\texpected int64\n\t}{\n\t\t{\"positive numbers\", 2, 3, 5},\n\t\t{\"negative numbers\", -2, -3, -5},\n\t\t{\"mixed signs\", -2, 3, 1},\n\t\t{\"zero\", 0, 5, 5},\n\t}\n\n\tfor _, tt := range tests {\n\t\tresult := Add(cross(cur), tt.a, tt.b)\n\t\tif result != tt.expected {\n\t\t\tt.Errorf(\"%s: Add(%d, %d) = %d; want %d\",\n\t\t\t\ttt.name, tt.a, tt.b, result, tt.expected)\n\t\t}\n\t}\n}\n\nfunc TestSubtract(cur realm, t *testing.T) {\n\tresult := Subtract(cross(cur), 5, 3)\n\tif result != 2 {\n\t\tt.Errorf(\"Subtract(5, 3) = %d; want 2\", result)\n\t}\n}\n\nfunc TestMultiply(cur realm, t *testing.T) {\n\tresult := Multiply(cross(cur), 4, 5)\n\tif result != 20 {\n\t\tt.Errorf(\"Multiply(4, 5) = %d; want 20\", result)\n\t}\n}\n\nfunc TestDivide(cur realm, t *testing.T) {\n\tresult := Divide(cross(cur), 10, 2)\n\tif result != 5 {\n\t\tt.Errorf(\"Divide(10, 2) = %d; want 5\", result)\n\t}\n}\n\nfunc TestGetLastResult(cur realm, t *testing.T) {\n\tAdd(cross(cur), 7, 8)\n\tif GetLastResult() != 15 {\n\t\tt.Errorf(\"GetLastResult() = %d; want 15\", GetLastResult())\n\t}\n\n\tMultiply(cross(cur), 3, 4)\n\tif GetLastResult() != 12 {\n\t\tt.Errorf(\"GetLastResult() = %d; want 12\", GetLastResult())\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"jYavTrhBAOvT4rCe2b++4VCyrWSWjmdxHBMdjutOrWZA+EOAis/SPe1qpNelV8Tbl9iGG2nBKs/grsg6QJLHiQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"txlink","path":"gno.land/r/docs/txlink","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/txlink\"\ngno = \"0.9\"\n"},{"name":"txlink.gno","body":"package txlink\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/moul/txlink/v0\"\n)\n\ntype Message struct {\n\tAuthor string\n\tText   string\n\tTime   time.Time\n}\n\nvar messages []Message\n\n// Post saves a message with the caller as the author\nfunc Post(cur realm, text string) {\n\tmsg := Message{\n\t\tAuthor: cur.Previous().Address().String(),\n\t\tText:   text,\n\t\tTime:   time.Now(),\n\t}\n\tmessages = append(messages, msg)\n}\n\nfunc Render(_ string) string {\n\tout := \"# Transaction Links\\n\\n\"\n\tout += `Transaction links (\"txlinks\" for short) are a standardized way of creating\nURLs that Gno-enabled wallets can interpret and generate transactions from.\n\nThey allow developers to turn basic hyperlinks into realm calls\nwith optional arguments and funds attached.\n\nArguments with empty values are treated as _wallet-settable_, allowing the\nuser to customize them before submitting the transaction.\n\nYou can also link to functions in _other realms_, using the pkgpath of the realm in question.\n\nCheck out the links below.\n\n`\n\n\tout += \"## Post a Message\\n\"\n\n\t// Basic call\n\tout += \"- [Say Hello](\" + txlink.Call(\"Post\", \"text\", \"Hello from txlink!\") + \") - A simple, static argument call\\n\"\n\n\t// A link builder including some funds specified in the string format (chain.Coin.String())\n\tcustom := txlink.NewLink(\"Post\").\n\t\tAddArgs(\"text\", \"Support this board with 1ugnot\").\n\t\tSetSend(\"1ugnot\").\n\t\tURL()\n\tout += \"- [Send Supportive Message](\" + custom + \") - A more complex call with coins included\\n\"\n\n\t// Up to the user to set the argument\n\tout += \"- [Write Your Own Message](\" + txlink.NewLink(\"Post\").\n\t\tAddArgs(\"text\", \"\"). // Should be editable in the user's wallet\n\t\tURL() + \") - A user-defined argument call\\n\"\n\n\t// Cross-realm example\n\tcrossRealm := txlink.Realm(\"gno.land/r/docs/minisocial/v2\").\n\t\tNewLink(\"CreatePost\").\n\t\tAddArgs(\"text\", \"Hello from `/r/docs/txlink`!\").\n\t\tURL()\n\tout += \"- [Say Hello on MiniSocial V2!](\" + crossRealm + \") - A call to another realm\\n\"\n\n\tout += \"\\n## Recent Messages\\n\"\n\tif len(messages) == 0 {\n\t\tout += \"_No messages yet._\\n\\n\"\n\t\treturn out\n\t}\n\n\tfor i := len(messages) - 1; i \u003e= 0 \u0026\u0026 i \u003e= len(messages)-5; i-- {\n\t\tmsg := messages[i]\n\t\tout += \"- **\" + msg.Author + \"**: \" + msg.Text + \" (\" + msg.Time.Format(\"15:04:05\") + \")\\n\"\n\t}\n\n\treturn out\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"h7C3EECvYZ4gBSEBwzT+DTDPeoFaZDVtjbOS9azRUO16dsSkJM0AFwuajZRmpPeNLGErkGLkWeEufyTAeHcaPA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"userprofile","path":"gno.land/r/docs/userprofile","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/docs/userprofile\"\ngno = \"0.9\"\n"},{"name":"userprofile.gno","body":"// Package userprofile shows how to integrate /r/sys/users with /r/demo/profile.\n// It reads a user's profile.\npackage userprofile\n\nimport (\n\t\"gno.land/p/nt/markdown/sanitize/v0\"\n\t\"gno.land/r/demo/profile\"\n\t\"gno.land/r/sys/users\"\n)\n\n// This example demonstrates using both r/sys/users and r/demo/profile\n// to create a rich user experience.\n\nfunc Render(path string) string {\n\tif path == \"\" {\n\t\treturn renderHome()\n\t}\n\n\treturn renderUser(path)\n}\n\nfunc renderHome() string {\n\treturn `# User \u0026 Profile Integration\n\nThis example demonstrates how to use both [` + \"`/r/sys/users`\" + `](/r/sys/users) and [` + \"`/r/demo/profile`\" + `](/r/demo/profile)\nto create rich user experiences in your realm.\n\n## /r/sys/users\n\nThe [user registry](/r/sys/users) provides:\n- Username registration\n- Address ↔ Username resolution\n\n` + \"```go\" + `\nimport \"gno.land/r/sys/users\"\n\n// Resolve a username to user data\ndata, _ := users.ResolveName(\"alice\")\naddr := data.Addr()\n\n// Resolve an address to user data\ndata = users.ResolveAddress(someAddress)\nname := data.Name()\n\n// Resolve either (smart detection)\ndata, _ = users.ResolveAny(\"alice\")\ndata, _ = users.ResolveAny(\"g1...\")\n` + \"```\" + `\n\n## /r/demo/profile\n\nThe [profile registry](/r/demo/profile) provides:\n- Display name\n- Avatar\n- Bio\n- Custom fields\n\n[Set your profile →](/r/demo/profile$help\u0026func=SetStringField)\n\n` + \"```go\" + `\nimport \"gno.land/r/demo/profile\"\n\n// Get profile fields\ndisplayName := profile.GetStringField(addr, profile.DisplayName, \"Anonymous\")\navatar := profile.GetStringField(addr, profile.Avatar, \"\")\nbio := profile.GetStringField(addr, profile.Bio, \"\")\n\n// Set profile fields (for current user)\nprofile.SetStringField(cross(cur), profile.DisplayName, \"Alice\")\nprofile.SetStringField(cross(cur), profile.Bio, \"Gno developer\")\n` + \"```\" + `\n\n## Combined Usage Pattern\n\n` + \"```go\" + `\nfunc RenderUser(nameOrAddr string) string {\n    // First, resolve the user\n    userData, _ := users.ResolveAny(nameOrAddr)\n    if userData == nil {\n        return \"User not found\"\n    }\n\n    addr := userData.Addr()\n    name := userData.Name()\n\n    // Then, get their profile\n    displayName := profile.GetStringField(addr, profile.DisplayName, name)\n    avatar := profile.GetStringField(addr, profile.Avatar, \"\")\n    bio := profile.GetStringField(addr, profile.Bio, \"No bio\")\n\n    // Render combined info\n    out := \"# \" + displayName + \"\\n\"\n    if avatar != \"\" {\n        out += \"![avatar](\" + avatar + \")\\n\"\n    }\n    out += bio + \"\\n\"\n    out += \"\\nUsername: @\" + name + \"\\n\"\n    out += \"Address: \" + addr.String() + \"\\n\"\n\n    return out\n}\n` + \"```\" + `\n\n## Live Demo\n\nTry looking up at test user:\n- [@test1](/r/docs/userprofile:test1)\n\n---\n\n### See also:\n- [/r/sys/users](/r/sys/users) - User registry\n- [/r/demo/profile](/r/demo/profile) - Profile data\n`\n}\n\nfunc renderUser(nameOrAddr string) string {\n\tout := \"# User Lookup\\n\\n\"\n\n\t// Try to resolve the user\n\tuserData, _ := users.ResolveAny(nameOrAddr)\n\tif userData == nil {\n\t\tout += \"User not found: `\" + sanitize.InlineText(nameOrAddr) + \"`\\n\\n\"\n\t\tout += \"[Back to Home](/r/docs/userprofile)\\n\"\n\t\treturn out\n\t}\n\n\taddr := userData.Addr()\n\tname := userData.Name()\n\n\tout += \"## Found User\\n\\n\"\n\n\t// User registry data\n\tout += \"### Registry Data (r/sys/users)\\n\\n\"\n\tout += \"| Field | Value |\\n\"\n\tout += \"|-------|-------|\\n\"\n\tout += \"| Username | @\" + sanitize.TableCell(name) + \" |\\n\"\n\tout += \"| Address | `\" + addr.String() + \"` |\\n\"\n\tout += \"| Profile Link | \" + userData.RenderLink(\"\") + \" |\\n\"\n\tout += \"\\n\"\n\n\t// Profile data\n\tout += \"### Profile Data (r/demo/profile)\\n\\n\"\n\n\tdisplayName := profileCell(profile.GetStringField(addr, profile.DisplayName, \"\"))\n\tavatar := profileCell(profile.GetStringField(addr, profile.Avatar, \"\"))\n\tbio := profileCell(profile.GetStringField(addr, profile.Bio, \"\"))\n\n\tout += \"| Field | Value |\\n\"\n\tout += \"|-------|-------|\\n\"\n\tout += \"| Display Name | \" + displayName + \" |\\n\"\n\tout += \"| Avatar | \" + avatar + \" |\\n\"\n\tout += \"| Bio | \" + bio + \" |\\n\"\n\tout += \"\\n\"\n\n\tout += \"---\\n\\n\"\n\tout += \"[Back to Home](/r/docs/userprofile)\\n\"\n\n\treturn out\n}\n\n// profileCell sanitizes a profile field for a markdown table cell, keeping\n// the \"_not set_\" placeholder when the field is empty.\nfunc profileCell(v string) string {\n\tif v == \"\" {\n\t\treturn \"_not set_\"\n\t}\n\treturn sanitize.TableCell(v)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"ct65MvHAPQrQW7MSecg3rid3HLOyfgcMrMV7MxXD65JV0eSTM9F8n5YmChVS41dogchrzfIt6pecT18kRCkxEg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"gnomaze","path":"gno.land/r/gfanton/gnomaze","files":[{"name":"asset.gno","body":"package gnomaze\n\ntype svg string\n\nvar svgassets = map[string]svg{\n\t\"banana\": `\u003cg id=\"SVGRepo_iconCarrier\"\u003e\u003cpath d=\"M746.666667 234.666667c21.333333 0 149.333333-21.333333 149.333333 170.666666S618.666667 938.666667 234.666667 938.666667c-42.666667 0-85.333333-21.333333-85.333334-21.333334l-21.333333-64s21.333333-85.333333 149.333333-149.333333 218.944-117.632 273.344-172.010667C593.344 489.344 661.333333 405.333333 661.333333 341.333333c0-41.770667 42.666667-85.333333 42.666667-85.333333\" fill=\"#FFE082\"\u003e\u003c/path\u003e\u003cpath d=\"M170.666667 874.666667s149.333333-42.666667 277.333333-106.666667 405.333333-256 320-533.333333c0 0 128-21.333333 128 170.666666S618.666667 938.666667 234.666667 938.666667c-42.666667 0-85.333333-21.333333-85.333334-21.333334l21.333334-42.666666z\" fill=\"#FFCA28\"\u003e\u003c/path\u003e\u003cpath d=\"M876.010667 297.344C836.010667 211.562667 746.666667 256 746.666667 85.333333c-64 0-106.666667 42.666667-106.666667 42.666667s64 64 64 128c42.666667 0 77.056-3.349333 80.874667 101.333333 28.842667-122.218667 91.136-59.989333 91.136-59.989333z\" fill=\"#C0CA33\"\u003e\u003c/path\u003e\u003cpath d=\"M661.333333 341.333333s-1.344-38.677333 42.666667-85.333333 85.994667-8.661333 85.994667-8.661333l-5.12 109.994666s-18.666667-69.333333-48-71.104C675.008 282.496 661.333333 341.333333 661.333333 341.333333z\" fill=\"#C0CA33\"\u003e\u003c/path\u003e\u003cpath d=\"M128 874.666667l21.333333 42.666666h21.333334v-42.666666l-42.666667-21.333334z\" fill=\"#5D4037\"\u003e\u003c/path\u003e\u003cpath d=\"M746.666667 85.333333c-64 0-106.666667 42.666667-106.666667 42.666667h64l42.666667-42.666667z\" fill=\"#827717\"\u003e\u003c/path\u003e\u003c/g\u003e`,\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/gfanton/gnomaze\"\ngno = \"0.9\"\n"},{"name":"maze.gno","body":"// This package demonstrate the capability of gno to build dynamic svg image\n// based on different query parameters.\n// Raycasting implementation as been heavily inspired by this project: https://github.com/AZHenley/raycasting\n\npackage gnomaze\n\nimport (\n\t\"chain/runtime\"\n\t\"encoding/base64\"\n\t\"hash/adler32\"\n\t\"math\"\n\t\"math/rand\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/moul/txlink/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/leon/hor\"\n)\n\nconst baseLevel = 7\n\n// Constants for cell dimensions\nconst (\n\tcellSize = 1.0\n\thalfCell = cellSize / 2\n)\n\ntype CellKind int\n\nconst (\n\tCellKindEmpty = iota\n\tCellKindWall\n)\n\nvar (\n\tlevel            int = 1\n\tsalt             int64\n\tmaze             [][]int\n\tendPos, startPos Position\n)\n\nfunc init(cur realm) {\n\t// Generate the map\n\tseed := uint64(runtime.ChainHeight())\n\trng := rand.New(rand.NewPCG(seed, uint64(time.Now().Unix())))\n\tgenerateLevel(rng, level)\n\tsalt = rng.Int64()\n\n\t// Register to hor\n\thor.Register(cross(cur), \"GnoMaze, A 3D Maze Game\", \"\")\n}\n\n// Position represents the X, Y coordinates in the maze\ntype Position struct{ X, Y int }\n\n// Player represents a player with position and viewing angle\ntype Player struct {\n\tX, Y, Angle, FOV float64\n}\n\n// PlayerState holds the player's grid position and direction\ntype PlayerState struct {\n\tCellX     int // Grid X position\n\tCellY     int // Grid Y position\n\tDirection int // 0-7 (0 = east, 1 = SE, 2 = S, etc.)\n}\n\n// Angle calculates the direction angle in radians\nfunc (p *PlayerState) Angle() float64 {\n\treturn float64(p.Direction) * math.Pi / 4\n}\n\n// Position returns the player's exact position in the grid\nfunc (p *PlayerState) Position() (float64, float64) {\n\treturn float64(p.CellX) + halfCell, float64(p.CellY) + halfCell\n}\n\n// SumCode returns a hash string based on the player's position\nfunc (p *PlayerState) SumCode() string {\n\ta := adler32.New()\n\n\tvar width int\n\tif len(maze) \u003e 0 {\n\t\twidth = len(maze[0])\n\t}\n\n\tufmt.Fprintf(a, \"%d-%d-%d\", p.CellY*width+p.CellX, level, salt)\n\treturn strconv.FormatUint(uint64(a.Sum32()), 10)\n}\n\n// Move updates the player's position based on movement deltas\nfunc (p *PlayerState) Move(dx, dy int) {\n\tnewX := p.CellX + dx\n\tnewY := p.CellY + dy\n\n\tif newY \u003e= 0 \u0026\u0026 newY \u003c len(maze) \u0026\u0026 newX \u003e= 0 \u0026\u0026 newX \u003c len(maze[0]) {\n\t\tif maze[newY][newX] == 0 {\n\t\t\tp.CellX = newX\n\t\t\tp.CellY = newY\n\t\t}\n\t}\n}\n\n// Rotate changes the player's direction\nfunc (p *PlayerState) Rotate(clockwise bool) {\n\tif clockwise {\n\t\tp.Direction = (p.Direction + 1) % 8\n\t} else {\n\t\tp.Direction = (p.Direction + 7) % 8\n\t}\n}\n\n// GenerateNextLevel validates the answer and generates a new level\nfunc GenerateNextLevel(cur realm, answer string) {\n\tseed := uint64(runtime.ChainHeight())\n\trng := rand.New(rand.NewPCG(seed, uint64(time.Now().Unix())))\n\n\tendState := PlayerState{CellX: endPos.X, CellY: endPos.Y}\n\thash := endState.SumCode()\n\tif hash != answer {\n\t\tpanic(\"invalid answer\")\n\t}\n\n\t// Generate new map\n\tlevel++\n\tsalt = rng.Int64()\n\tgenerateLevel(rng, level)\n}\n\n// generateLevel creates a new maze for the given level\nfunc generateLevel(rng *rand.Rand, level int) {\n\tif level \u003c 0 {\n\t\tpanic(\"invalid level\")\n\t}\n\n\tsize := level + baseLevel\n\tmaze, startPos, endPos = generateMap(rng, size, size)\n}\n\n// generateMap creates a random maze using a depth-first search algorithm.\nfunc generateMap(rng *rand.Rand, width, height int) ([][]int, Position, Position) {\n\t// Initialize the maze grid filled with walls.\n\tm := make([][]int, height)\n\tfor y := range m {\n\t\tm[y] = make([]int, width)\n\t\tfor x := range m[y] {\n\t\t\tm[y][x] = CellKindWall\n\t\t}\n\t}\n\n\t// Define start position and initialize stack for DFS\n\tstart := Position{1, 1}\n\tstack := []Position{start}\n\tm[start.Y][start.X] = CellKindEmpty\n\n\t// Initialize distance matrix and track farthest\n\tdist := make([][]int, height)\n\tfor y := range dist {\n\t\tdist[y] = make([]int, width)\n\t\tfor x := range dist[y] {\n\t\t\tdist[y][x] = -1\n\t\t}\n\t}\n\tdist[start.Y][start.X] = CellKindEmpty\n\tmaxDist := 0\n\tcandidates := []Position{start}\n\n\t// Possible directions for movement: right, left, down, up\n\tdirections := []Position{{1, 0}, {-1, 0}, {0, 1}, {0, -1}}\n\n\t// Generate maze paths using DFS\n\tfor len(stack) \u003e 0 {\n\t\tcurrent := stack[len(stack)-1]\n\t\tstack = stack[:len(stack)-1]\n\n\t\tvar dirCandidates []struct {\n\t\t\tnext, wall Position\n\t\t}\n\n\t\t// Evaluate possible candidates for maze paths\n\t\tfor _, d := range directions {\n\t\t\tnx, ny := current.X+d.X*2, current.Y+d.Y*2\n\t\t\twx, wy := current.X+d.X, current.Y+d.Y\n\n\t\t\t// Check if the candidate position is within bounds and still a wall\n\t\t\tif nx \u003e 0 \u0026\u0026 nx \u003c width-1 \u0026\u0026 ny \u003e 0 \u0026\u0026 ny \u003c height-1 \u0026\u0026 m[ny][nx] == 1 {\n\t\t\t\tdirCandidates = append(dirCandidates, struct{ next, wall Position }{\n\t\t\t\t\tPosition{nx, ny}, Position{wx, wy},\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\t// If candidates are available, choose one and update the maze\n\t\tif len(dirCandidates) \u003e 0 {\n\t\t\tchosen := dirCandidates[rng.IntN(len(dirCandidates))]\n\t\t\tm[chosen.wall.Y][chosen.wall.X] = CellKindEmpty\n\t\t\tm[chosen.next.Y][chosen.next.X] = CellKindEmpty\n\n\t\t\t// Update distance for the next cell\n\t\t\tcurrentDist := dist[current.Y][current.X]\n\t\t\tnextDist := currentDist + 2\n\t\t\tdist[chosen.next.Y][chosen.next.X] = nextDist\n\n\t\t\t// Update maxDist and candidates\n\t\t\tif nextDist \u003e maxDist {\n\t\t\t\tmaxDist = nextDist\n\t\t\t\tcandidates = []Position{chosen.next}\n\t\t\t} else if nextDist == maxDist {\n\t\t\t\tcandidates = append(candidates, chosen.next)\n\t\t\t}\n\n\t\t\tstack = append(stack, current, chosen.next)\n\t\t}\n\t}\n\n\t// Select a random farthest position as the end\n\tvar end Position\n\tif len(candidates) \u003e 0 {\n\t\tend = candidates[rng.IntN(len(candidates))]\n\t} else {\n\t\tend = Position{width - 2, height - 2} // Fallback to bottom-right\n\t}\n\n\treturn m, start, end\n}\n\n// castRay simulates a ray casting in the maze to find walls\nfunc castRay(playerX, playerY, rayAngle float64, m [][]int) (distance float64, wallHeight float64, endCellHit bool, endDistance float64) {\n\tx, y := playerX, playerY\n\tdx, dy := math.Cos(rayAngle), math.Sin(rayAngle)\n\tsteps := 0\n\tendCellHit = false\n\tendDistance = 0.0\n\n\tfor {\n\t\tix, iy := int(math.Floor(x)), int(math.Floor(y))\n\t\tif ix == endPos.X \u0026\u0026 iy == endPos.Y {\n\t\t\tendCellHit = true\n\t\t\tendDistance = math.Sqrt(math.Pow(x-playerX, 2) + math.Pow(y-playerY, 2))\n\t\t}\n\n\t\tif iy \u003c 0 || iy \u003e= len(m) || ix \u003c 0 || ix \u003e= len(m[0]) || m[iy][ix] != 0 {\n\t\t\tbreak\n\t\t}\n\n\t\tx += dx * 0.1\n\t\ty += dy * 0.1\n\t\tsteps++\n\t\tif steps \u003e 400 {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tdistance = math.Sqrt(math.Pow(x-playerX, 2) + math.Pow(y-playerY, 2))\n\twallHeight = 300.0 / distance\n\treturn\n}\n\n// GenerateSVG creates an SVG representation of the maze scene\nfunc GenerateSVG(cur realm, p *PlayerState) string {\n\tconst (\n\t\tsvgWidth, svgHeight = 800, 600\n\t\toffsetX, offsetY    = 0.0, 500.0\n\t\tgroundLevel         = 300\n\t\trays                = 124\n\t\tfov                 = math.Pi / 4\n\t\tminiMapSize         = 100.0\n\t\tvisibleCells        = 7\n\t\tdirLen              = 2.0\n\t)\n\n\tm := maze\n\tplayerX, playerY := p.Position()\n\tangle := p.Angle()\n\n\tsliceWidth := float64(svgWidth) / float64(rays)\n\tangleStep := fov / float64(rays)\n\n\tvar svg strings.Builder\n\tsvg.WriteString(`\u003csvg width=\"800\" height=\"600\" xmlns=\"http://www.w3.org/2000/svg\"\u003e`)\n\tsvg.WriteString(`\u003crect x=\"0\" y=\"0\" width=\"800\" height=\"300\" fill=\"rgb(20,40,20)\"/\u003e`)\n\tsvg.WriteString(`\u003crect x=\"0\" y=\"300\" width=\"800\" height=\"300\" fill=\"rgb(40,60,40)\"/\u003e`)\n\n\tvar drawBanana func()\n\tfor i := 0; i \u003c rays; i++ {\n\t\trayAngle := angle - fov/2 + float64(i)*angleStep\n\t\tdistance, wallHeight, endHit, endDist := castRay(playerX, playerY, rayAngle, m)\n\t\tdarkness := 1.0 + distance/4.0\n\t\tcolorVal1 := int(180.0 / darkness)\n\t\tcolorVal2 := int(32.0 / darkness)\n\t\tyPos := groundLevel - wallHeight/2\n\n\t\tufmt.Fprintf(\u0026svg,\n\t\t\t`\u003crect x=\"%f\" y=\"%f\" width=\"%f\" height=\"%f\" fill=\"rgb(%d,69,%d)\"/\u003e`,\n\t\t\tfloat64(i)*sliceWidth, yPos, sliceWidth, wallHeight, colorVal1, colorVal2)\n\n\t\tif drawBanana != nil {\n\t\t\tcontinue // Banana already drawn\n\t\t}\n\n\t\t// Only draw banana if the middle ray hit the end\n\t\t// XXX: improve this by checking for a hit in the middle of the end cell\n\t\tif i == rays/2 \u0026\u0026 endHit \u0026\u0026 endDist \u003c distance {\n\t\t\ticonHeight := 10.0 / endDist\n\t\t\tscale := iconHeight / 100\n\t\t\tx := float64(i)*sliceWidth + sliceWidth/2\n\t\t\ty := groundLevel + 20 + (iconHeight*scale)/2\n\n\t\t\tdrawBanana = func() {\n\t\t\t\tufmt.Fprintf(\u0026svg,\n\t\t\t\t\t`\u003cg transform=\"translate(%f %f) scale(%f)\"\u003e%s\u003c/g\u003e`,\n\t\t\t\t\tx, y, scale, string(svgassets[\"banana\"]),\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n\n\tif drawBanana != nil {\n\t\tdrawBanana()\n\t}\n\n\tplayerCellX, playerCellY := int(math.Floor(playerX)), int(math.Floor(playerY))\n\n\txStart := max(0, playerCellX-visibleCells/2)\n\txEnd := min(len(m[0]), playerCellX+visibleCells/2+1)\n\n\tyStart := max(0, playerCellY-visibleCells/2)\n\tyEnd := min(len(m), playerCellY+visibleCells/2+1)\n\n\tscaleX := miniMapSize / float64(xEnd-xStart)\n\tscaleY := miniMapSize / float64(yEnd-yStart)\n\n\tfor y := yStart; y \u003c yEnd; y++ {\n\t\tfor x := xStart; x \u003c xEnd; x++ {\n\t\t\tcolor := \"black\"\n\t\t\tif m[y][x] == 1 {\n\t\t\t\tcolor = \"rgb(149,0,32)\"\n\t\t\t}\n\t\t\tufmt.Fprintf(\u0026svg,\n\t\t\t\t`\u003crect x=\"%f\" y=\"%f\" width=\"%f\" height=\"%f\" fill=\"%s\"/\u003e`,\n\t\t\t\tfloat64(x-xStart)*scaleX+offsetX, float64(y-yStart)*scaleY+offsetY, scaleX, scaleY, color)\n\t\t}\n\t}\n\n\tpx := (playerX-float64(xStart))*scaleX + offsetX\n\tpy := (playerY-float64(yStart))*scaleY + offsetY\n\tufmt.Fprintf(\u0026svg, `\u003ccircle cx=\"%f\" cy=\"%f\" r=\"%f\" fill=\"rgb(200,200,200)\"/\u003e`, px, py, scaleX/2)\n\n\tdx := math.Cos(angle) * dirLen\n\tdy := math.Sin(angle) * dirLen\n\tufmt.Fprintf(\u0026svg,\n\t\t`\u003cline x1=\"%f\" y1=\"%f\" x2=\"%f\" y2=\"%f\" stroke=\"rgb(200,200,200)\" stroke-width=\"1\"/\u003e`,\n\t\tpx, py, (playerX+dx-float64(xStart))*scaleX+offsetX, (playerY+dy-float64(yStart))*scaleY+offsetY)\n\n\tsvg.WriteString(`\u003c/svg\u003e`)\n\treturn svg.String()\n}\n\n// renderGrid3D creates a 3D view of the grid\nfunc renderGrid3D(cur realm, p *PlayerState) string {\n\tsvg := GenerateSVG(cur, p)\n\tbase64SVG := base64.StdEncoding.EncodeToString([]byte(svg))\n\treturn ufmt.Sprintf(\"![SVG Image](data:image/svg+xml;base64,%s)\", base64SVG)\n}\n\n// generateDirLink generates a link to change player direction\nfunc generateDirLink(path string, p *PlayerState, action string) string {\n\tnewState := *p // Make copy\n\n\tswitch action {\n\tcase \"forward\":\n\t\tdx, dy := directionDeltas(newState.Direction)\n\t\tnewState.Move(dx, dy)\n\tcase \"left\":\n\t\tnewState.Rotate(false)\n\tcase \"right\":\n\t\tnewState.Rotate(true)\n\t}\n\n\tvals := make(url.Values)\n\tvals.Set(\"x\", strconv.Itoa(newState.CellX))\n\tvals.Set(\"y\", strconv.Itoa(newState.CellY))\n\tvals.Set(\"dir\", strconv.Itoa(newState.Direction))\n\n\tvals.Set(\"sum\", newState.SumCode())\n\treturn path + \"?\" + vals.Encode()\n}\n\n// isPlayerTouchingWall checks if the player's position is inside a wall\nfunc isPlayerTouchingWall(x, y float64) bool {\n\tix, iy := int(math.Floor(x)), int(math.Floor(y))\n\tif iy \u003c 0 || iy \u003e= len(maze) || ix \u003c 0 || ix \u003e= len(maze[0]) {\n\t\treturn true\n\t}\n\treturn maze[iy][ix] == CellKindEmpty\n}\n\n// directionDeltas provides deltas for movement based on direction\nfunc directionDeltas(d int) (x, y int) {\n\ts := []struct{ x, y int }{\n\t\t{1, 0},   // 0 == E\n\t\t{1, 1},   // SE\n\t\t{0, 1},   // S\n\t\t{-1, 1},  // SW\n\t\t{-1, 0},  // W\n\t\t{-1, -1}, // NW\n\t\t{0, -1},  // N\n\t\t{1, -1},  // NE\n\t}[d]\n\treturn s.x, s.y\n}\n\n// atoiDefault converts string to integer with a default fallback\nfunc atoiDefault(s string, def int) int {\n\tif s == \"\" {\n\t\treturn def\n\t}\n\ti, _ := strconv.Atoi(s)\n\treturn i\n}\n\n// Render renders the game interface\nfunc Render(cur realm, path string) string {\n\tu, _ := url.Parse(path)\n\tquery := u.Query()\n\n\tp := PlayerState{\n\t\tCellX:     atoiDefault(query.Get(\"x\"), startPos.X),\n\t\tCellY:     atoiDefault(query.Get(\"y\"), startPos.Y),\n\t\tDirection: atoiDefault(query.Get(\"dir\"), 0), // Start facing east\n\t}\n\n\tcpath := strings.TrimPrefix(cur.PkgPath(), runtime.ChainDomain())\n\tpsum := p.SumCode()\n\treset := \"[reset](\" + cpath + \")\"\n\n\tif startPos.X != p.CellX || startPos.Y != p.CellY {\n\t\tif sum := query.Get(\"sum\"); psum != sum {\n\t\t\treturn \"invalid sum : \" + reset\n\t\t}\n\t}\n\n\tif endPos.X == p.CellX \u0026\u0026 endPos.Y == p.CellY {\n\t\treturn strings.Join([]string{\n\t\t\tufmt.Sprintf(\"### Congrats you win level %d !!\", level),\n\t\t\tufmt.Sprintf(\"Code for next level is: %s\", psum),\n\t\t\tufmt.Sprintf(\"[Generate Next Level: %d](%s)\", level+1, txlink.Call(\"GenerateNextLevel\", \"answer\", psum)),\n\t\t}, \"\\n\\n\")\n\t}\n\n\t// Generate commands\n\tcommands := strings.Join([]string{\n\t\t\"\u003cgno-columns\u003e\",\n\t\t\"\u003cgno-columns-sep\u003e\",\n\t\tufmt.Sprintf(\"[▲](%s)\", generateDirLink(cpath, \u0026p, \"forward\")),\n\t\t\"\u003cgno-columns-sep\u003e\",\n\t\t\"\u003c/gno-columns\u003e\",\n\t\t\"\u003cgno-columns\u003e\",\n\t\tufmt.Sprintf(\"[◄](%s)\", generateDirLink(cpath, \u0026p, \"left\")),\n\t\t\"\u003cgno-columns-sep\u003e\",\n\t\t\"\u003cgno-columns-sep\u003e\",\n\t\tufmt.Sprintf(\"[►](%s)\", generateDirLink(cpath, \u0026p, \"right\")),\n\t\t\"\u003c/gno-columns\u003e\",\n\t}, \"\\n\\n\")\n\n\t// Generate view\n\tview := strings.Join([]string{\n\t\t\"\u003cgno-columns\u003e\",\n\t\trenderGrid3D(cross(cur), \u0026p),\n\t\t\"\u003c/gno-columns\u003e\",\n\t}, \"\\n\\n\")\n\n\treturn strings.Join([]string{\n\t\t\"## Find the banana: Level \" + strconv.Itoa(level),\n\t\t\"---\", view, \"---\", commands, \"---\",\n\t\treset,\n\t\tufmt.Sprintf(\"Position: (%d, %d) Direction: %fπ\", p.CellX, p.CellY, float64(p.Direction)/math.Pi),\n\t}, \"\\n\\n\")\n}\n\n// max returns the maximum of two integers\nfunc max(a, b int) int {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n\n// min returns the minimum of two integers\nfunc min(a, b int) int {\n\tif a \u003c b {\n\t\treturn a\n\t}\n\treturn b\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"y8McaReAoVmAlXSvb71W7RqSi7iUVGhlcnlUUyfWf6s4AdqzOdOtfr0HPj8JyudiMjnQjHEwwsR2v5uU86GdyQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7","package":{"name":"faucet","path":"gno.land/r/gnoland/faucet","files":[{"name":"admin.gno","body":"package faucet\n\nimport (\n\t\"chain\"\n\t\"errors\"\n)\n\nfunc AdminSetInPause(cur realm, inPause bool) string {\n\tif err := assertIsAdmin(0, cur); err != nil {\n\t\treturn err.Error()\n\t}\n\tgInPause = inPause\n\treturn \"\"\n}\n\nfunc AdminSetMessage(cur realm, message string) string {\n\tif err := assertIsAdmin(0, cur); err != nil {\n\t\treturn err.Error()\n\t}\n\tgMessage = message\n\treturn \"\"\n}\n\nfunc AdminSetTransferLimit(cur realm, amount int64) string {\n\tif err := assertIsAdmin(0, cur); err != nil {\n\t\treturn err.Error()\n\t}\n\tgLimit = chain.NewCoin(\"ugnot\", amount)\n\treturn \"\"\n}\n\nfunc AdminSetAdminAddr(cur realm, addr address) string {\n\tif err := assertIsAdmin(0, cur); err != nil {\n\t\treturn err.Error()\n\t}\n\tgAdminAddr = addr\n\treturn \"\"\n}\n\nfunc AdminAddController(cur realm, addr address) string {\n\tif err := assertIsAdmin(0, cur); err != nil {\n\t\treturn err.Error()\n\t}\n\n\tsize := gControllers.Size()\n\n\tif size \u003e= gControllersMaxSize {\n\t\treturn \"can not add more controllers than allowed\"\n\t}\n\n\tif gControllers.Has(addr.String()) {\n\t\treturn addr.String() + \" exists, no need to add.\"\n\t}\n\n\tgControllers.Set(addr.String(), addr)\n\n\treturn \"\"\n}\n\nfunc AdminRemoveController(cur realm, addr address) string {\n\tif err := assertIsAdmin(0, cur); err != nil {\n\t\treturn err.Error()\n\t}\n\n\tif !gControllers.Has(addr.String()) {\n\t\treturn addr.String() + \" is not on the controller list\"\n\t}\n\n\t_, ok := gControllers.Remove(addr.String())\n\n\t// it not should happen.\n\t// we will check anyway to prevent issues in the underline implementation.\n\n\tif !ok {\n\t\treturn addr.String() + \" is not on the controller list\"\n\t}\n\n\treturn \"\"\n}\n\nfunc assertIsAdmin(_ int, rlm realm) error {\n\tif !rlm.IsCurrent() {\n\t\treturn errors.New(\"unauthorized: rlm is not the caller's live cur\")\n\t}\n\tcaller := rlm.Previous().Address()\n\tif caller != gAdminAddr {\n\t\treturn errors.New(\"restricted for admin\")\n\t}\n\treturn nil\n}\n"},{"name":"faucet.gno","body":"package faucet\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"errors\"\n\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nvar (\n\t// configurable by admin.\n\tgAdminAddr          address = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\") // govdao t1 multisig\n\tgControllers                = bptree.NewBPTree32()\n\tgControllersMaxSize         = 10 // limit it to 10\n\tgInPause                    = false\n\tgMessage                    = \"# Community Faucet.\\n\\n\"\n\n\t// internal vars, for stats.\n\tgTotalTransferred chain.Coins\n\tgTotalTransfers   = uint(0)\n\n\t// per request limit, 350 gnot\n\tgLimit chain.Coin = chain.NewCoin(\"ugnot\", 350_000_000)\n)\n\nfunc Transfer(cur realm, to address, send int64) string {\n\tif err := assertIsController(0, cur); err != nil {\n\t\treturn err.Error()\n\t}\n\n\tif gInPause {\n\t\treturn errors.New(\"faucet in pause\").Error()\n\t}\n\n\t// limit the per request\n\tif send \u003e gLimit.Amount {\n\t\treturn errors.New(\"Per request limit \" + gLimit.String() + \" exceed\").Error()\n\t}\n\tsendCoins := chain.Coins{chain.NewCoin(\"ugnot\", send)}\n\n\tgTotalTransferred = gTotalTransferred.Add(sendCoins)\n\tgTotalTransfers++\n\n\tbanker_ := banker.NewBanker(banker.BankerTypeRealmSend, cur)\n\tpkgaddr := cur.Address()\n\tbanker_.SendCoins(pkgaddr, to, sendCoins)\n\treturn \"\"\n}\n\nfunc GetPerTransferLimit() int64 {\n\treturn gLimit.Amount\n}\n\nfunc bankerAddr(cur realm) address {\n\treturn cur.Address()\n}\n\nfunc Render(cur realm, _ string) string {\n\tbanker_ := banker.NewReadonlyBanker()\n\tbalance := banker_.GetCoins(bankerAddr(cross(cur)))\n\n\toutput := gMessage\n\tif gInPause {\n\t\toutput += \"Status: inactive.\\n\"\n\t} else {\n\t\toutput += \"Status: active.\\n\"\n\t}\n\toutput += ufmt.Sprintf(\"Balance: %s.\\n\", balance.String())\n\toutput += ufmt.Sprintf(\"Total transfers: %s (in %d times).\\n\\n\", gTotalTransferred.String(), gTotalTransfers)\n\n\toutput += \"Package address: \" + cur.Address().String() + \"\\n\\n\"\n\toutput += ufmt.Sprintf(\"Admin: %s\\n\\n \", gAdminAddr.String())\n\toutput += ufmt.Sprintf(\"Controllers:\\n\\n \")\n\n\tfor i := 0; i \u003c gControllers.Size(); i++ {\n\t\t_, v := gControllers.GetByIndex(i)\n\t\toutput += ufmt.Sprintf(\"%s  \", v.(address))\n\t}\n\n\toutput += \"\\n\\n\"\n\toutput += ufmt.Sprintf(\"Per request limit: %s\\n\\n\", gLimit.String())\n\n\treturn output\n}\n\nfunc assertIsController(_ int, rlm realm) error {\n\tif !rlm.IsCurrent() {\n\t\treturn errors.New(\"unauthorized: rlm is not the caller's live cur\")\n\t}\n\tcaller := rlm.Previous().Address()\n\tok := gControllers.Has(caller.String())\n\tif !ok {\n\t\treturn errors.New(caller.String() + \" is not on the controller list\")\n\t}\n\treturn nil\n}\n"},{"name":"faucet_test.gno","body":"package faucet\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n)\n\nfunc TestPackage(cur realm, t *testing.T) {\n\tvar (\n\t\tadminaddr        = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\")\n\t\tfaucetaddr       = chain.PackageAddress(\"gno.land/r/gnoland/faucet\")\n\t\tcontrolleraddr1  = testutils.TestAddress(\"controller1\")\n\t\tcontrolleraddr2  = testutils.TestAddress(\"controller2\")\n\t\tcontrolleraddr3  = testutils.TestAddress(\"controller3\")\n\t\tcontrolleraddr4  = testutils.TestAddress(\"controller4\")\n\t\tcontrolleraddr5  = testutils.TestAddress(\"controller5\")\n\t\tcontrolleraddr6  = testutils.TestAddress(\"controller6\")\n\t\tcontrolleraddr7  = testutils.TestAddress(\"controller7\")\n\t\tcontrolleraddr8  = testutils.TestAddress(\"controller8\")\n\t\tcontrolleraddr9  = testutils.TestAddress(\"controller9\")\n\t\tcontrolleraddr10 = testutils.TestAddress(\"controller10\")\n\t\tcontrolleraddr11 = testutils.TestAddress(\"controller11\")\n\n\t\ttest1addr = testutils.TestAddress(\"test1\")\n\t)\n\t// deposit 1000gnot to faucet contract\n\ttesting.IssueCoins(faucetaddr, chain.Coins{{\"ugnot\", 1_000_000_000}})\n\tassertBalance(t, faucetaddr, 1_000_000_000)\n\n\t// by default, balance is empty, and as a user I cannot call Transfer, or Admin commands.\n\tassertBalance(t, test1addr, 0)\n\ttesting.SetRealm(testing.NewUserRealm(test1addr))\n\tassertErr(t, Transfer(cross(cur), test1addr, 1_000_000))\n\n\tassertErr(t, AdminAddController(cross(cur), controlleraddr1))\n\ttesting.SetRealm(testing.NewUserRealm(controlleraddr1))\n\tassertErr(t, Transfer(cross(cur), test1addr, 1_000_000))\n\n\t// as an admin, add the controller to contract and deposit more 2000gnot to contract\n\ttesting.SetRealm(testing.NewUserRealm(adminaddr))\n\tassertNoErr(t, AdminAddController(cross(cur), controlleraddr1))\n\tassertBalance(t, faucetaddr, 1_000_000_000)\n\n\t// now, send some tokens as controller.\n\ttesting.SetRealm(testing.NewUserRealm(controlleraddr1))\n\tassertNoErr(t, Transfer(cross(cur), test1addr, 1_000_000))\n\tassertBalance(t, test1addr, 1_000_000)\n\tassertNoErr(t, Transfer(cross(cur), test1addr, 1_000_000))\n\tassertBalance(t, test1addr, 2_000_000)\n\tassertBalance(t, faucetaddr, 998_000_000)\n\n\t// remove controller\n\t// as an admin, remove controller\n\ttesting.SetRealm(testing.NewUserRealm(adminaddr))\n\tassertNoErr(t, AdminRemoveController(cross(cur), controlleraddr1))\n\ttesting.SetRealm(testing.NewUserRealm(controlleraddr1))\n\tassertErr(t, Transfer(cross(cur), test1addr, 1_000_000))\n\n\t// duplicate controller\n\ttesting.SetRealm(testing.NewUserRealm(adminaddr))\n\tassertNoErr(t, AdminAddController(cross(cur), controlleraddr1))\n\tassertErr(t, AdminAddController(cross(cur), controlleraddr1))\n\t// add more than more than allowed controllers\n\tassertNoErr(t, AdminAddController(cross(cur), controlleraddr2))\n\tassertNoErr(t, AdminAddController(cross(cur), controlleraddr3))\n\tassertNoErr(t, AdminAddController(cross(cur), controlleraddr4))\n\tassertNoErr(t, AdminAddController(cross(cur), controlleraddr5))\n\tassertNoErr(t, AdminAddController(cross(cur), controlleraddr6))\n\tassertNoErr(t, AdminAddController(cross(cur), controlleraddr7))\n\tassertNoErr(t, AdminAddController(cross(cur), controlleraddr8))\n\tassertNoErr(t, AdminAddController(cross(cur), controlleraddr9))\n\tassertNoErr(t, AdminAddController(cross(cur), controlleraddr10))\n\tassertErr(t, AdminAddController(cross(cur), controlleraddr11))\n\n\t// send more than per transfer limit\n\ttesting.SetRealm(testing.NewUserRealm(adminaddr))\n\tassertNoErr(t, AdminSetTransferLimit(cross(cur), 300_000_000))\n\ttesting.SetRealm(testing.NewUserRealm(controlleraddr1))\n\tassertErr(t, Transfer(cross(cur), test1addr, 301_000_000))\n\n\t// block transefer from the address not on the controllers list.\n\ttesting.SetRealm(testing.NewUserRealm(controlleraddr11))\n\tassertErr(t, Transfer(cross(cur), test1addr, 1_000_000))\n}\n\nfunc assertErr(t *testing.T, err string) {\n\tt.Helper()\n\n\tif err == \"\" {\n\t\tt.Logf(\"info: got err: %v\", err)\n\t\tt.Errorf(\"expected an error, got nil.\")\n\t}\n}\n\nfunc assertNoErr(t *testing.T, err string) {\n\tt.Helper()\n\tif err != \"\" {\n\t\tt.Errorf(\"got err: %v.\", err)\n\t}\n}\n\nfunc assertBalance(t *testing.T, addr address, expectedBal int64) {\n\tt.Helper()\n\tbanker_ := banker.NewReadonlyBanker()\n\tcoins := banker_.GetCoins(addr)\n\tgot := coins.AmountOf(\"ugnot\")\n\n\tif expectedBal != got {\n\t\tt.Errorf(\"invalid balance: expected %d, got %d.\", expectedBal, got)\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/gnoland/faucet\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7\"\n"},{"name":"z0_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/faucet/filetests/z0_filetest\n\npackage z0_filetest\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\t\"gno.land/r/gnoland/faucet\"\n)\n\n// mints ugnot to current realm\nfunc init() {\n\tfaucetaddr := chain.PackageAddress(\"gno.land/r/gnoland/faucet\")\n\ttesting.IssueCoins(faucetaddr, chain.Coins{{\"ugnot\", 200_000_000}})\n}\n\n// assert render with empty path and no controllers\nfunc main(cur realm) {\n\tprintln(faucet.Render(cross(cur), \"\"))\n}\n\n// Output:\n// # Community Faucet.\n//\n// Status: active.\n// Balance: 200000000ugnot.\n// Total transfers:  (in 0 times).\n//\n// Package address: g1ttrq7mp4zy6dssnmgyyktnn4hcj3ys8xhju0n7\n//\n// Admin: g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\n//\n//  Controllers:\n//\n//\n//\n// Per request limit: 350000000ugnot\n"},{"name":"z1_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/faucet/filetests/z1_filetest\n\npackage z1_filetest\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\t\"gno.land/r/gnoland/faucet\"\n)\n\n// mints ugnot to current realm\nfunc init() {\n\tfaucetaddr := chain.PackageAddress(\"gno.land/r/gnoland/faucet\")\n\ttesting.IssueCoins(faucetaddr, chain.Coins{{\"ugnot\", 200_000_000}})\n}\n\n// assert render with a path and no controllers\nfunc main(cur realm) {\n\tprintln(faucet.Render(cross(cur), \"path\"))\n}\n\n// Output:\n// # Community Faucet.\n//\n// Status: active.\n// Balance: 200000000ugnot.\n// Total transfers:  (in 0 times).\n//\n// Package address: g1ttrq7mp4zy6dssnmgyyktnn4hcj3ys8xhju0n7\n//\n// Admin: g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\n//\n//  Controllers:\n//\n//\n//\n// Per request limit: 350000000ugnot\n"},{"name":"z2_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/faucet/filetests/z2_filetest\n\npackage z2_filetest\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/r/gnoland/faucet\"\n)\n\n// mints ugnot to current realm\nfunc init() {\n\tfaucetaddr := chain.PackageAddress(\"gno.land/r/gnoland/faucet\")\n\ttesting.IssueCoins(faucetaddr, chain.Coins{{\"ugnot\", 200_000_000}})\n}\n\n// assert render with empty path and 2 controllers\nfunc main(cur realm) {\n\tvar (\n\t\tadminaddr       = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\")\n\t\tcontrolleraddr1 = testutils.TestAddress(\"controller1\")\n\t\tcontrolleraddr2 = testutils.TestAddress(\"controller2\")\n\t)\n\ttesting.SetRealm(testing.NewUserRealm(adminaddr))\n\terr := faucet.AdminAddController(cross(cur), controlleraddr1)\n\tif err != \"\" {\n\t\tpanic(err)\n\t}\n\terr = faucet.AdminAddController(cross(cur), controlleraddr2)\n\tif err != \"\" {\n\t\tpanic(err)\n\t}\n\tprintln(faucet.Render(cross(cur), \"\"))\n}\n\n// Output:\n// # Community Faucet.\n//\n// Status: active.\n// Balance: 200000000ugnot.\n// Total transfers:  (in 0 times).\n//\n// Package address: g1ttrq7mp4zy6dssnmgyyktnn4hcj3ys8xhju0n7\n//\n// Admin: g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\n//\n//  Controllers:\n//\n//  g1vdhkuarjdakxcetjx9047h6lta047h6lsdacav  g1vdhkuarjdakxcetjxf047h6lta047h6lnrev3v\n//\n// Per request limit: 350000000ugnot\n"},{"name":"z3_filetest.gno","body":"// PKGPATH: gno.land/r/gnoland/faucet/filetests/z3_filetest\n\npackage z3_filetest\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/r/gnoland/faucet\"\n)\n\n// mints coints to current realm\nfunc init() {\n\tfaucetaddr := chain.PackageAddress(\"gno.land/r/gnoland/faucet\")\n\ttesting.IssueCoins(faucetaddr, chain.Coins{{\"ugnot\", 200_000_000}})\n}\n\n// assert render with 2 controllers and 2 transfers\nfunc main(cur realm) {\n\tvar (\n\t\tadminaddr       = address(\"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\")\n\t\tcontrolleraddr1 = testutils.TestAddress(\"controller1\")\n\t\tcontrolleraddr2 = testutils.TestAddress(\"controller2\")\n\t\ttestaddr1       = testutils.TestAddress(\"test1\")\n\t\t_               = testutils.TestAddress(\"test2\")\n\t)\n\ttesting.SetRealm(testing.NewUserRealm(adminaddr))\n\terr := faucet.AdminAddController(cross(cur), controlleraddr1)\n\tif err != \"\" {\n\t\tpanic(err)\n\t}\n\terr = faucet.AdminAddController(cross(cur), controlleraddr2)\n\tif err != \"\" {\n\t\tpanic(err)\n\t}\n\ttesting.SetRealm(testing.NewUserRealm(controlleraddr1))\n\terr = faucet.Transfer(cross(cur), testaddr1, 1_000_000)\n\tif err != \"\" {\n\t\tpanic(err)\n\t}\n\ttesting.SetRealm(testing.NewUserRealm(controlleraddr2))\n\terr = faucet.Transfer(cross(cur), testaddr1, 2_000_000)\n\tif err != \"\" {\n\t\tpanic(err)\n\t}\n\tprintln(faucet.Render(cross(cur), \"\"))\n}\n\n// Output:\n// # Community Faucet.\n//\n// Status: active.\n// Balance: 197000000ugnot.\n// Total transfers: 3000000ugnot (in 2 times).\n//\n// Package address: g1ttrq7mp4zy6dssnmgyyktnn4hcj3ys8xhju0n7\n//\n// Admin: g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\n//\n//  Controllers:\n//\n//  g1vdhkuarjdakxcetjx9047h6lta047h6lsdacav  g1vdhkuarjdakxcetjxf047h6lta047h6lnrev3v\n//\n// Per request limit: 350000000ugnot\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"j7oZKQY+Qyvo67O/TWJnBD4u7V4thJCYFNkYKavAJ3Fb1/ksNfzJ9CPEJtm/cJFt6cWbFyH1VT+MZ+SJbrsaiA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7","package":{"name":"pages","path":"gno.land/r/gnoland/pages","files":[{"name":"admin.gno","body":"package pages\n\nimport (\n\t\"chain/runtime/unsafe\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/bptree/v0\"\n)\n\nvar (\n\tadminAddr     address\n\tmoderatorList bptree.BPTree\n\tinPause       bool\n)\n\nfunc init() {\n\t// adminAddr = unsafe.OriginCaller() // FIXME: find a way to use this from the main's genesis.\n\tadminAddr = \"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx\" // govdao t1 multisig\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 ModAddPost(_ realm, slug, title, body, publicationDate, authors, tags string) {\n\tassertIsModerator()\n\n\tcaller := unsafe.OriginCaller()\n\ttagList := strings.Split(tags, \",\")\n\tauthorList := strings.Split(authors, \",\")\n\n\terr := b.NewPost(caller, slug, title, body, publicationDate, authorList, tagList)\n\tcheckErr(err)\n}\n\nfunc ModEditPost(_ realm, slug, title, body, publicationDate, authors, tags string) {\n\tassertIsModerator()\n\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 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 assertIsAdmin() {\n\tcaller := unsafe.OriginCaller()\n\tif !isAdmin(caller) {\n\t\tpanic(\"access restricted.\")\n\t}\n}\n\nfunc assertIsModerator() {\n\tcaller := unsafe.OriginCaller()\n\tif isAdmin(caller) || isModerator(caller) {\n\t\treturn\n\t}\n\tpanic(\"access restricted\")\n}\n\nfunc assertNotInPause() {\n\tif inPause {\n\t\tpanic(\"access restricted (pause)\")\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/gnoland/pages\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7\"\n"},{"name":"page_about.gno","body":"package pages\n\nfunc init() {\n\tpath := \"about\"\n\ttitle := \"Gno.land Is A Platform To Write Smart Contracts In Gno\"\n\t// XXX: description := \"On Gno.land, developers write smart contracts and other blockchain apps using Gno without learning a language that’s exclusive to a single ecosystem.\"\n\tbody := `\nGno.land is a next-generation smart contract platform using Gno, an interpreted version of the general-purpose Go\nprogramming language. On Gno.land, smart contracts can be uploaded on-chain only by publishing their full source code,\nmaking it trivial to verify the contract or fork it into an improved version. With a system to publish reusable code\nlibraries on-chain, Gno.land serves as the “GitHub” of the ecosystem, with realms built using fully transparent,\nauditable code that anyone can inspect and reuse.\n\nGno.land addresses many pressing issues in the blockchain space, starting with the ease of use and intuitiveness of\nsmart contract platforms. Developers can write smart contracts without having to learn a new language that’s exclusive\nto a single ecosystem or limited by design. Go developers can easily port their existing web apps to Gno.land or build\nnew ones from scratch, making web3 vastly more accessible.\n\nSecured by Proof of Contribution (PoC), a DAO-managed Proof-of-Authority consensus mechanism, Gno.land prioritizes\nfairness and merit, rewarding the people most active on the platform. PoC restructures the financial incentives that\noften corrupt blockchain projects, opting instead to reward contributors for their work based on expertise, commitment, and\nalignment.\n\nOne of our inspirations for Gno.land is the gospels, which built a system of moral code that lasted thousands of years.\nBy observing a minimal production implementation, Gno.land’s design will endure over time and serve as a reference for\nfuture generations with censorship-resistant tools that improve their understanding of the world.\n`\n\tif err := b.NewPost(\"\", path, title, body, \"2022-05-20T13:17:22Z\", nil, nil); err != nil {\n\t\tpanic(err)\n\t}\n}\n"},{"name":"page_contribute.gno","body":"package pages\n\nfunc init() {\n\tpath := \"contribute\"\n\ttitle := \"Contributor Ecosystem: Call for Contributions\"\n\tbody := `\n\nGno.land puts at the center of its identity the contributors that help to create and shape the project into what it is; incentivizing those who contribute the most and help advance its vision. Eventually, contributions will be incentivized directly on-chain; in the meantime, this page serves to illustrate our current off-chain initiatives.\n\nGno.land is still in full-steam development. For now, we're looking for the earliest of adopters; curious to explore a new way to build smart contracts and eager to make an impact. Joining Gno.land's development now means you can help to shape the base of its development ecosystem, which will pave the way for the next generation of blockchain programming.\n\nAs an open-source project, we welcome all contributions. On this page you can find some pointers on where to get started; as well as some incentives for the most valuable and important contributions.\n\n## Where to get started\n\nIf you are interested in contributing to Gno.land, you can jump in on our [GitHub monorepo](https://github.com/gnolang/gno/blob/master/CONTRIBUTING.md) - where most development happens.\n\nA good place to start are the issues tagged [\"good first issue\"](https://github.com/gnolang/gno/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). They should allow you to make some impact on the Gno repository while you're still exploring the details of how everything works.\n\n## Gno Bounties\n\n\u003e **Note: The Gno Bounty Program is currently paused until further notice.**\n\nTwo reasons prompted this pause:\n\n- **Focus on Mainnet.** The team is fully focused on the Gno.land Beta Mainnet launch — the highest priority — as well as the critical post-beta work required to deliver a complete Mainnet.\n- **Decline in submission quality.** The bounty program was created to encourage innovative, creative, and technical contributions that meaningfully advance Gno.land. However, with the rise of generative AI, the overall quality of submissions has declined significantly. A large portion of entries are clearly AI-generated; others, though less obviously so, are often inaccurate upon review due to overreliance on AI. Reviewing and validating these submissions has become increasingly time-consuming, diverting focus from development work.\n\nCommunity contributions remain strongly encouraged, particularly:\n\n- Tooling and infrastructure projects\n- Application-focused development\n- Longer-term, high-quality initiatives\n\nThese types of contributions are better aligned with Gno's goals than short-term bounties, and are very welcome provided they follow the [contributing guidelines](https://github.com/gnolang/gno/blob/master/CONTRIBUTING.md). Sustained, high-quality contributions will be rewarded over time via the Gno.land GovDAO.\n\n## Gno.land Grants\n\nThe Gno.land grants program is to encourage and support the growth of the Gno.land contributor community, and build out the usability of the platform and smart contract library. The program provides financial resources to contributors to explore the Gno tech stack, and build dApps, tooling, infrastructure, products, and smart contract libraries in Gno.land.\n\nFor more details on Gno.land grants, suggested topics, and how to apply, visit our grants [repository](https://github.com/gnolang/grants).\n\n## Join Game of Realms\n\nGame of Realms is the overarching contributor network of gnomes, currently running off-chain, and will eventually transition on-chain. At this stage, a Game of Realms contribution is comprised of high-impact contributions identified as ['notable contributions'](https://github.com/gnolang/game-of-realms/tree/main/contributors).\n\nThese contributions are not linked to immediate financial rewards, but are notable in nature, in the sense they are a challenge, make a significant addition to the project, and require persistence, with minimal feedback loops from the core team.\n\nThe selection of a notable contribution or the sum of contributions that equal 'notable' is based on the impact it has on the development of the project. For now, it is focused on code contributions, and will evolve over time. The Gno development teams will initially qualify and evaluate notable contributions, and vote off-chain on adding them to the 'notable contributions' folder on GitHub.\n\nYou can always contribute to the project, and all contributions will be noticed. Contributing now is a way to build your personal contributor profile in Gno.land early on in the ecosystem, and signal your commitment to the project, the community, and its future.\n\nThere are a variety of ways to make your contributions count:\n\n- Core code contributions\n- Realm and pure package development\n- Validator tooling\n- Developer tooling\n- Tutorials and documentation\n\nTo start, we recommend you create a PR in the Game of Realms [repository](https://github.com/gnolang/game-of-realms) to create your profile page for all your contributions.`\n\n\tif err := b.NewPost(\"\", path, title, body, \"2024-09-05T00:00:00Z\", nil, nil); err != nil {\n\t\tpanic(err)\n\t}\n}\n"},{"name":"page_ecosystem.gno","body":"package pages\n\nfunc init() {\n\tvar (\n\t\tpath  = \"ecosystem\"\n\t\ttitle = \"Discover gno.land Ecosystem Projects \u0026 Initiatives\"\n\t\t// XXX: description = \"Dive further into the gno.land ecosystem and discover the core infrastructure, projects, smart contracts, and tooling we’re building.\"\n\t\tbody = `\n### [Gno Playground](https://play.gno.land)\n\nGno Playground is a simple web interface that lets you write, test, and experiment with your Gno code to improve your\nunderstanding of the Gno language. You can share your code, run unit tests, deploy your realms and packages, and execute\nfunctions in your code using the repo.\n\nVisit the playground at [play.gno.land](https://play.gno.land)!\n\n### [Gno Studio Connect](https://gno.studio/connect)\n\nGno Studio Connect provides seamless access to realms, making it simple to explore, interact, and engage\nwith gno.land’s smart contracts through function calls. Connect focuses on function calls, enabling users to interact\nwith any realm’s exposed function(s) on gno.land.\n\nSee your realm interactions in [Gno Studio Connect](https://gno.studio/connect)\n\n### [Gnoscan](https://gnoscan.io)\n\nDeveloped by the Onbloc team, Gnoscan is gno.land’s blockchain explorer. Anyone can use Gnoscan to easily find\ninformation that resides on the gno.land blockchain, such as wallet addresses, TX hashes, blocks, and contracts.\nGnoscan makes our on-chain data easy to read and intuitive to discover.\n\nExplore the gno.land blockchain at [gnoscan.io](https://gnoscan.io)!\n\n### Adena\n\nAdena is a user-friendly non-custodial wallet for gno.land. Open-source and developed by Onbloc, Adena allows gnomes to\ninteract easily with the chain. With an emphasis on UX, Adena is built to handle millions of realms and tokens with a\nhigh-quality interface, support for NFTs and custom tokens, and seamless integration. Install Adena via the [official website](https://www.adena.app/)\n\n### Gnoswap\n\nGnoswap is currently under development and led by the Onbloc team. Gnoswap will be the first DEX on gno.land and is an\nautomated market maker (AMM) protocol written in Gno that allows for permissionless token exchanges on the platform.\n\n### Flippando\n\nFlippando is a simple on-chain memory game, ported from Solidity to Gno, which starts with an empty matrix to flip tiles\non to see what’s underneath. If the tiles match, they remain uncovered; if not, they are briefly shown, and the player\nmust memorize their colors until the entire matrix is uncovered. The end result can be minted as an NFT, which can later\nbe assembled into bigger, more complex NFTs, creating a digital “painting” with the uncovered tiles.  Play the game at [Flippando](https://gno.flippando.xyz/flip)\n\n### Gno Native Kit\n\n[Gno Native Kit](https://github.com/gnolang/gnonative) is a framework that allows developers to build and port gno.land (d)apps written in the (d)app's native language.\n\n\n`\n\t)\n\tif err := b.NewPost(\"\", path, title, body, \"2022-05-20T13:17:23Z\", nil, nil); err != nil {\n\t\tpanic(err)\n\t}\n}\n"},{"name":"page_gnolang.gno","body":"package pages\n\nfunc init() {\n\tvar (\n\t\tpath  = \"gnolang\"\n\t\ttitle = \"About the Gno, the Language for Gno.land\"\n\t\t// TODO fix broken images\n\t\tbody = `\n\n[Gno](https://github.com/gnolang/gno) is an interpretation of the widely-used Go (Golang) programming language for blockchain created by Cosmos co-founder Jae Kwon in 2022 to mark a new era in smart contracting. Gno is ~99% identical to Go, so Go programmers can start coding in Gno right away, with a minimal learning curve. For example, Gno comes with blockchain-specific standard libraries, but any code that doesn’t use blockchain-specific logic can run in Go with minimal processing. Libraries that don’t make sense in the blockchain context, such as network or operating-system access, are not available in Gno. Otherwise, Gno loads and uses many standard libraries that power Go, so most of the parsing of the source code is the same.\n\nUnder the hood, the Gno code is parsed into an abstract syntax tree (AST) and the AST itself is used in the interpreter, rather than bytecode as in many virtual machines such as Java, Python, or Wasm. This makes even the GnoVM accessible to any Go programmer. The novel design of the intuitive GnoVM interpreter allows Gno to freeze and resume the program by persisting and loading the entire memory state. Gno is deterministic, auto-persisted, and auto-Merkle-ized, allowing (smart contract) programs to be succinct, as the programmer doesn’t have to serialize and deserialize objects to persist them into a database (unlike programming applications with the Cosmos SDK).\n\n## How Gno Differs from Go\n\n![Gno and Go differences](static/img/gno-language/go-and-gno.jpg)\n\nThe composable nature of Go/Gno allows for type-checked interactions between contracts, making Gno.land safer and more powerful, as well as operationally cheaper and faster. Smart contracts on Gno.land are light, simple, more focused, and easily interoperable—a network of interconnected contracts rather than siloed monoliths that limit interactions with other contracts.\n\n![Example of Gno code](static/img/gno-language/code-example.jpg)\n\n## Gno Inherits Go’s Built-in Security Features\n\nGo supports secure programming through exported/non-exported fields, enabling a “least-authority” design. It is easy to create objects and APIs that expose only what should be accessible to callers while hiding what should not be simply by the capitalization of letters, thus allowing a succinct representation of secure logic that can be called by multiple users.\n\nAnother major advantage of Go is that the language comes with an ecosystem of great tooling, like the compiler and third-party tools that statically analyze code. Gno inherits these advantages from Go directly to create a smart contract programming language that provides embedding, composability, type-check safety, and garbage collection, helping developers to write secure code relying on the compiler, parser, and interpreter to give warning alerts for common mistakes.\n\n## Gno vs Solidity\n\nThe most widely-adopted smart contract language today is Ethereum’s EVM-compatible Solidity. With bytecode built from the ground up and Turing complete, Solidity opened up a world of possibilities for decentralized applications (dApps) and there are currently more than 10 million contracts deployed on Ethereum. However, Solidity provides limited tooling and its EVM has a stack limit and computational inefficiencies.\n\nSolidity is designed for one purpose only (writing smart contracts) and is bound by the limitations of the EVM. In addition, developers have to learn several languages if they want to understand the whole stack or work across different ecosystems. Gno aspires to exceed Solidity on multiple fronts (and other smart contract languages like CosmWasm or Substrate) as every part of the stack is written in Gno. It’s easy for developers to understand the entire system just by studying a relatively small code base.\n\n## Gno Is Essential for the Wider Adoption of Web3\n\nGno makes imports as easy as they are in web2 with runtime-based imports for seamless dependency flow comprehension, and support for complex structs, beyond primitive types. Gno is ultimately cost-effective as dependencies are loaded once, enabling remote function calls as local, and providing automatic and independent per-realm state persistence.\n\nUsing Gno, developers can rapidly accelerate application development and adopt a modular structure by reusing and reassembling existing modules without building from scratch. They can embed one structure inside another in an intuitive way while preserving localism, and the language specification is simple, successfully balancing practicality and minimalism.\n\nThe Go language is so well designed that the Gno smart contract system will become the new gold standard for smart contract development and other blockchain applications. As a programming language that is universally adopted, secure, composable, and complete, Gno is essential for the broader adoption of web3 and its sustainable growth.`\n\t)\n\tif err := b.NewPost(\"\", path, title, body, \"2022-05-20T13:17:24Z\", nil, nil); err != nil {\n\t\tpanic(err)\n\t}\n}\n"},{"name":"page_license.gno","body":"package pages\n\nfunc init() {\n\tvar (\n\t\tpath  = \"license\"\n\t\ttitle = \"Gno Network General Public License\"\n\t\tbody  = `Copyright (C) 2024 NewTendermint, LLC\n\nThis program is free software: you can redistribute it and/or modify it under\nthe terms of the GNO Network General Public License as published by\nNewTendermint, LLC, either version 4 of the License, or (at your option) any\nlater version published by NewTendermint, LLC.\n\nThis program is distributed in the hope that it will be useful, but is provided\nas-is and WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNO Network\nGeneral Public License for more details.\n\nYou should have received a copy of the GNO Network General Public License along\nwith this program.  If not, see \u003chttps://gno.land/license\u003e.\n\nAttached below are the terms of the GNO Network General Public License, Version\n4 (a fork of the GNU Affero General Public License 3).\n\n## Additional Terms\n\n### Strong Attribution\n\nIf any of your user interfaces, such as websites and mobile applications, serve\nas the primary point of entry to a platform or blockchain that 1) offers users\nthe ability to upload their own smart contracts to the platform or blockchain,\nand 2) leverages any Covered Work (including the GNO virtual machine) to run\nthose smart contracts on the platform or blockchain (\"Applicable Work\"), then\nthe Applicable Work must prominently link to (1) gno.land or (2) any other URL\ndesignated by NewTendermint, LLC that has not been rejected by the governance of\nthe first chain known as gno.land, provided that the identity of the first chain\nis not ambiguous.  In the event the identity of the first chain is ambiguous,\nthen NewTendermint, LLC's designation shall control.  Such link must appear\nconspicuously in the header or footer of the Applicable Work, such that all\nusers may learn of gno.land or the URL designated by NewTendermint, LLC.\n\nThis additional attribution requirement shall remain in effect for (1) 7\nyears from the date of publication of the Applicable Work, or (2) 7 years from\nthe date of publication of the Covered Work (including republication of new\nversions), whichever is later, but no later than 12 years after the application\nof this strong attribution requirement to the publication of the Applicable\nWork.  For purposes of this Strong Attribution requirement, Covered Work shall\nmean any work that is licensed under the GNO Network General Public License,\nVersion 4 or later, by NewTendermint, LLC.\n\n\n# GNO NETWORK GENERAL PUBLIC LICENSE\n\nVersion 4, 7 May 2024\n\nModified from the GNU AFFERO GENERAL PUBLIC LICENSE.\nGNU is not affiliated with GNO or NewTendermint, LLC.\nCopyright (C) 2022 NewTendermint, LLC.\n\n## Preamble\n\nThe GNO Network General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\nThe licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\nWhen we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\nDevelopers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\nA secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\nThe GNO Network General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\nThe precise terms and conditions for copying, distribution and\nmodification follow.\n\n## TERMS AND CONDITIONS\n\n### 0. Definitions.\n\n\"This License\" refers to version 4 of the GNO Network General Public License.\n\n\"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n\"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\nTo \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\nA \"covered work\" means either the unmodified Program or a work based\non the Program.\n\nTo \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\nTo \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\nAn interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n### 1. Source Code.\n\nThe \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\nA \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\nThe \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\nThe \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\nThe Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\nThe Corresponding Source for a work in source code form is that\nsame work.\n\n### 2. Basic Permissions.\n\nAll rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\nYou may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\nConveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\nNo covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\nWhen you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n### 4. Conveying Verbatim Copies.\n\nYou may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\nYou may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n### 5. Conveying Modified Source Versions.\n\nYou may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n-   a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n-   b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n-   c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n-   d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\nA compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n### 6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n-   a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n-   b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n-   c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n-   d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n-   e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\nA separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\nA \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n\"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\nIf you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\nThe requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\nCorresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n### 7. Additional Terms.\n\n\"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\nWhen you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\nNotwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n-   a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n-   b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n-   c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n-   d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n-   e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n-   f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors; or\n-   g) Requiring strong attribution such as notices on any user interfaces\n    that run or convey any covered work, such as a prominent link to a URL\n    on the header of a website, such that all users of the covered work may\n    become aware of the notice, for a period no longer than 20 years.\n\nAll other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\nIf you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\nAdditional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n### 8. Termination.\n\nYou may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\nHowever, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\nMoreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\nTermination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n### 9. Acceptance Not Required for Having Copies.\n\nYou are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n### 10. Automatic Licensing of Downstream Recipients.\n\nEach time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\nAn \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\nYou may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n### 11. Patents.\n\nA \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\nA contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\nEach contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\nIn the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\nIf you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\nIf, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\nA patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\nNothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n### 12. No Surrender of Others' Freedom.\n\nIf conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to simultaneously satisfy your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n### 13. Remote Network Interaction; Use with the GNU General Public License.\n\nNotwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\nNotwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n### 14. Revised Versions of this License.\n\nNewTendermint LLC may publish revised and/or new versions of\nthe GNO Network General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\nEach version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNO Network General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Gno Software\nFoundation.  If the Program does not specify a version number of the\nGNO Network General Public License, you may choose any version ever published\nby NewTendermint LLC.\n\nIf the Program specifies that a proxy can decide which future\nversions of the GNO Network General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\nLater license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n### 15. Disclaimer of Warranty.\n\nTHERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n### 16. Limitation of Liability.\n\nIN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n### 17. Interpretation of Sections 15 and 16.\n\nIf the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\nEND OF TERMS AND CONDITIONS\n\n## How to Apply These Terms to Your New Programs\n\nIf you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\nTo do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    \u003cone line to give the program's name and a brief idea of what it does.\u003e\n    Copyright (C) \u003cyear\u003e  \u003cname of author\u003e\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNO Network General Public License as published by\n    NewTendermint LLC, either version 4 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNO Network General Public License for more details.\n\n    You should have received a copy of the GNO Network General Public License\n    along with this program.  If not, see \u003chttps://gno.land/license\u003e.\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n`\n\t)\n\tif err := b.NewPost(\"\", path, title, body, \"2024-04-22T00:00:00Z\", nil, nil); err != nil {\n\t\tpanic(err)\n\t}\n}\n"},{"name":"page_links.gno","body":"package pages\n\nfunc init() {\n\tpath := \"links\"\n\ttitle := \"Gno.land Linker\"\n\tbody := `\n\nBelow are Gno.land's most important links.\n\n#### [Gno GitHub, the project's main repository](https://github.com/gnolang/gno)\n#### [Getting Started with Gno](https://github.com/gnolang/getting-started/)\n#### [Gno.land Official Docs](https://docs.gno.land/)\n#### [Gnoverse Github, a community organization for Gnomes](https://github.com/gnoverse)\n#### [Gno.land Workshops, a repo for Gno resources](https://github.com/gnolang/workshops)\n#### [Gno.land Students Program](https://github.com/gnolang/student-contributors-program/)\n#### [Gno.land Contributor page](https://gno.land/contribute)\n#### [Gno.land Discord](https://discord.gg/gnoland)\n#### [Gno.land X](https://x.com/_gnoland)\n#### [Gno.land YouTube](https://www.youtube.com/@_gnoland)\n#### [Gno.land Playground, an online editor for exploring Gno.land](https://play.gno.land)\n#### [GnoScan, a Gno.land block explorer](https://gnoscan.io/)\n`\n\tif err := b.NewPost(\"\", path, title, body, \"2025-06-23T13:17:30Z\", nil, nil); err != nil {\n\t\tpanic(err)\n\t}\n}\n"},{"name":"page_newsletter.gno","body":"package pages\n\nimport (\n\t\"gno.land/p/leon/svgbtn/v0\"\n)\n\nfunc init() {\n\tpath := \"newsletter\"\n\ttitle := \"Sign up for Gno News\"\n\tbody := \"**Stay in the Gno by signing up for our newsletter. You'll get the scoop on dev updates, fresh content, and community news.**\\n\\n\"\n\tbody += 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\tif err := b.NewPost(\"\", path, title, body, \"2025-07-14T00:00:00Z\", nil, nil); err != nil {\n\t\tpanic(err)\n\t}\n}\n"},{"name":"page_partners.gno","body":"package pages\n\nfunc init() {\n\tpath := \"partners\"\n\ttitle := \"Partnerships\"\n\tbody := `### Fund and Grants Program\n\nAre you a builder, tinkerer, or researcher? If you’re looking to create awesome dApps, tooling, infrastructure, \nor smart contract libraries on Gno.land, you can apply for a grant. The Gno.land Ecosystem Fund and Grants program \nprovides financial contributions for individuals and teams to innovate on the platform.\n\nRead more about our Funds and Grants program [here](https://github.com/gnolang/grants).\n`\n\tif err := b.NewPost(\"\", path, title, body, \"2022-05-20T13:17:25Z\", nil, nil); err != nil {\n\t\tpanic(err)\n\t}\n}\n"},{"name":"page_start.gno","body":"package pages\n\nfunc init() {\n\tpath := \"start\"\n\ttitle := \"Getting Started with Gno\"\n\tbody := `## Getting Started with Gno\n\nVisit the [getting-started](https://github.com/gnolang/getting-started) repo to try Gno in 5 minutes!\n`\n\tif err := b.NewPost(\"\", path, title, body, \"2022-05-20T13:17:26Z\", nil, nil); err != nil {\n\t\tpanic(err)\n\t}\n}\n"},{"name":"page_testnets.gno","body":"package pages\n\nfunc init() {\n\tpath := \"testnets\"\n\ttitle := \"Gno.land Testnet List\"\n\tbody := `\n- [Staging](https://docs.gno.land/resources/gnoland-networks/#staging-environments-portal-loops) - a rolling testnet\n\nFor a list of RPC endpoints, see the [reference documentation](https://docs.gno.land/reference/rpc-endpoints).\n\n## Local development\n\nSee the \"Build on Gno.land\" section in the [Official Documentation](https://docs.gno.land/).\n`\n\tif err := b.NewPost(\"\", path, title, body, \"2022-05-20T13:17:27Z\", nil, nil); err != nil {\n\t\tpanic(err)\n\t}\n}\n"},{"name":"page_tokenomics.gno","body":"package pages\n\nfunc init() {\n\tvar (\n\t\tpath  = \"tokenomics\"\n\t\ttitle = \"Gno.land Tokenomics\"\n\t\t// XXX: description = \"\"\"\n\t\tbody = `Lorem Ipsum`\n\t)\n\tif err := b.NewPost(\"\", path, title, body, \"2022-05-20T13:17:28Z\", nil, nil); err != nil {\n\t\tpanic(err)\n\t}\n\n}\n"},{"name":"pages.gno","body":"package pages\n\nimport (\n\t\"gno.land/p/gnoland/blog/v0\"\n)\n\n// TODO: switch from p/blog to p/pages\n\nvar b = \u0026blog.Blog{\n\tTitle:        \"Gnoland's Pages\",\n\tPrefix:       \"/r/gnoland/pages:\",\n\tNoBreadcrumb: true,\n}\n\nfunc Render(path string) string {\n\treturn b.Render(path)\n}\n"},{"name":"pages_test.gno","body":"package pages\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestHome(t *testing.T) {\n\tprintedOnce := false\n\tgot := Render(\"\")\n\n\texpectedSubtrings := []string{\n\t\t\"/r/gnoland/pages:p/tokenomics\",\n\t\t\"/r/gnoland/pages:p/links\",\n\t\t\"/r/gnoland/pages:p/contribute\",\n\t\t\"/r/gnoland/pages:p/start\",\n\t\t\"/r/gnoland/pages:p/about\",\n\t\t\"/r/gnoland/pages:p/gnolang\",\n\t}\n\n\tfor _, substring := range expectedSubtrings {\n\t\tif !strings.Contains(got, substring) {\n\t\t\tif !printedOnce {\n\t\t\t\tprintedOnce = true\n\t\t\t}\n\t\t\tt.Errorf(\"expected %q, but not found.\", substring)\n\t\t}\n\t}\n}\n\nfunc TestAbout(t *testing.T) {\n\tprintedOnce := false\n\tgot := Render(\"p/about\")\n\texpectedSubtrings := []string{\n\t\t\"Gno.land Is A Platform To Write Smart Contracts In Gno\",\n\t\t\"Gno.land is a next-generation smart contract platform using Gno, an interpreted version of the general-purpose Go\\nprogramming language.\",\n\t}\n\tfor _, substring := range expectedSubtrings {\n\t\tif !strings.Contains(got, substring) {\n\t\t\tif !printedOnce {\n\t\t\t\tprintedOnce = true\n\t\t\t}\n\t\t\tt.Errorf(\"expected %q, but not found.\", substring)\n\t\t}\n\t}\n}\n"},{"name":"util.gno","body":"package pages\n\nfunc checkErr(err error) {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"J0Pzxnj1K+Ywsu1FvvI/a+W8DVu/qHTBq83HCyDG4iFBEESoqbD3mvnHbYmrA/g9pbSKsfZgk2audoUqRxiLmA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7","package":{"name":"monit","path":"gno.land/r/gnoland/monit","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/gnoland/monit\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7\"\n"},{"name":"monit.gno","body":"// Package monit links a monitoring system with the chain in both directions.\n//\n// The agent will periodically call Incr() and verify that the value is always\n// higher than the previously known one. The contract will store the last update\n// time and use it to detect whether or not the monitoring agent is functioning\n// correctly.\npackage monit\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/nt/ownable/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/nt/watchdog/v0\"\n)\n\nvar (\n\tcounter          int\n\tlastUpdate       time.Time\n\tlastCaller       address\n\twatchdogDuration = 5 * time.Minute\n\twd               = watchdog.Watchdog{Duration: watchdogDuration}\n\tOwnable          *ownable.Ownable\n)\n\nfunc init(cur realm) {\n\tif !cur.Previous().IsUserCall() {\n\t\tpanic(\"realm must be initialized by EOA\")\n\t}\n\tOwnable = ownable.NewWithAddress(cur.Previous().Address())\n}\n\n// Incr increments the counter and informs the watchdog that we're alive.\n// This function can be called by anyone.\nfunc Incr(cur realm) int {\n\tcounter++\n\tlastUpdate = time.Now()\n\tlastCaller = cur.Previous().Address()\n\twd.Alive()\n\treturn counter\n}\n\n// Reset resets the realm state.\n// This function can only be called by the admin.\nfunc Reset(cur realm) {\n\tOwnable.AssertOwnedBy(cur.Previous().Address())\n\tcounter = 0\n\tlastCaller = cur.Previous().Address()\n\tlastUpdate = time.Now()\n\twd = watchdog.Watchdog{Duration: watchdogDuration}\n}\n\nfunc Render(_ string) string {\n\tstatus := wd.Status()\n\treturn ufmt.Sprintf(\n\t\t\"counter=%d\\nlast update=%s\\nlast caller=%s\\nstatus=%s\",\n\t\tcounter, lastUpdate, lastCaller, status,\n\t)\n}\n"},{"name":"monit_test.gno","body":"package monit\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/nt/ownable/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/watchdog/v0\"\n)\n\nfunc initTest() {\n\tcounter = 0\n\tlastUpdate = time.Time{}\n\tlastCaller = address(\"\")\n\twd = watchdog.Watchdog{Duration: watchdogDuration}\n\tcreator := address(\"g1creator\")\n\tOwnable = ownable.NewWithAddress(creator)\n}\n\nfunc TestPackage(cur realm, t *testing.T) {\n\tinitTest()\n\n\ttesting.SetRealm(testing.NewUserRealm(\"g1user\"))\n\n\t// initial state, watchdog is KO.\n\t{\n\t\texpected := `counter=0\nlast update=0001-01-01 00:00:00 +0000 UTC\nlast caller=\nstatus=KO`\n\t\tgot := Render(\"\")\n\t\tuassert.Equal(t, expected, got)\n\t}\n\n\t// call Incr(), watchdog is OK.\n\tIncr(cross(cur))\n\tIncr(cross(cur))\n\tIncr(cross(cur))\n\t{\n\t\texpected := `counter=3\nlast update=2009-02-13 23:31:30 +0000 UTC\nlast caller=g1user\nstatus=OK`\n\t\tgot := Render(\"\")\n\t\tuassert.Equal(t, expected, got)\n\t}\n\n\t/* XXX: improve tests once we've the missing std.TestSkipTime feature\n\t\t// wait 1h, watchdog is KO.\n\t\tuse std.TestSkipTime(time.Hour)\n\t\t{\n\t\t\texpected := `counter=3\n\tlast update=2009-02-13 22:31:30 +0000 UTC\n\tlast caller=g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n\tstatus=KO`\n\t\t\tgot := Render(\"\")\n\t\t\tuassert.Equal(t, expected, got)\n\t\t}\n\n\t\t// call Incr(), watchdog is OK.\n\t\tIncr()\n\t\t{\n\t\t\texpected := `counter=4\n\tlast update=2009-02-13 23:31:30 +0000 UTC\n\tlast caller=g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n\tstatus=OK`\n\t\t\tgot := Render(\"\")\n\t\t\tuassert.Equal(t, expected, got)\n\t\t}\n\t*/\n}\n\nfunc TestReset(cur realm, t *testing.T) {\n\tinitTest()\n\n\t// Initial state check\n\tinitialCounter := counter\n\tinitialLastUpdate := lastUpdate\n\tinitialStatus := wd.Status()\n\n\t// Call Incr to change the state\n\tuser := address(\"g1user\")\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tIncr(cross(cur))\n\tuassert.True(t, counter \u003e initialCounter, \"counter should have increased after Incr\")\n\tuassert.True(t, lastUpdate.After(initialLastUpdate), \"lastUpdate should have been updated after Incr\")\n\tuassert.Equal(t, user, lastCaller, \"lastCaller mismatch\")\n\tuassert.NotEqual(t, initialStatus, wd.Status(), \"watchdog status should have changed after Incr\") // Status changes after Alive() is called\n\n\t// Call Reset as the owner\n\townerAddr := Ownable.Owner()\n\ttesting.SetRealm(testing.NewUserRealm(ownerAddr)) // Simulate call from the owner\n\tReset(cross(cur))\n\tuassert.Equal(t, 0, counter, \"counter should be 0 after Reset\")\n\tuassert.Equal(t, ownerAddr, lastCaller, \"lastCaller should be the owner address after Reset\")\n\tuassert.Equal(t, watchdogDuration.String(), wd.Duration.String(), \"watchdog duration mismatch after Reset\")\n\tuassert.Equal(t, \"KO\", wd.Status(), \"watchdog status should be KO after Reset\")\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"zYR+B8SzZdapYvn922tWjcx6hq39jdICxm0+7MCR/PhTH0T715fwYfmg1lIszvlpxG7XwTZTmSJNOajHYXmavw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"home","path":"gno.land/r/grepsuzette/home","files":[{"name":"README.md","body":"Dwilgelindildong, traveler.\n\n"},{"name":"contribs.gno","body":"package home\n\nfunc r3() string {\n\treturn `# Greps' notable contributions\n\nMy main contributions until the gno.land beta launch are listed below; most aren't in the monorepo, note.\n\n### Port Joeson from coffeescript to golang\n\nWorked on this from june 2022 until january 2023. Bounty [applied for](https://github.com/gnolang/bounties-old/issues/33) on Feb 2, 2023. \n\nHere is the port I did in Go: [grepsuzette/joeson](https://github.com/grepsuzette/joeson/).\n\n    4. Port JOESON to Go\n    github.com/jaekwon/joescript\n    The intent is to create an independent left-recursive PEG parser for Gno.\n    Optional: port Joescript or Javascript.\n    1000 ATOMs from @jaekwon\n    More GNOTs than from #3.\n\nThere have been many examples posted, including a minimal [LISP REPL](https://github.com/grepsuzette/joeson/tree/master/examples/lisp-repl) and a theorical [study on precedence](https://github.com/grepsuzette/joeson/blob/master/examples/precedence/precedence_test.go) (precedence is often problematic with PEG parsers, this allowed to find a solution, used in the next part). \n\n### GNO grammar - partial\n\nIn summer 2023, started to port the GNO grammar using Joeson (since there was no news about joeson, so this was an attempt to demonstrate it worked). Grammar was posted in [PR 1156](https://github.com/gnolang/gno/pull/1156). There are only 3 files, they are quite dense:\n\n1. [joeson_test.go](https://github.com/grepsuzette/gno/blob/joeson/gnovm/pkg/gnolang/joeson_test.go)\n1. [joeson_rules.go](https://github.com/grepsuzette/gno/blob/joeson/gnovm/pkg/gnolang/joeson_rules.go)\n1. [joeson_f.go](https://github.com/grepsuzette/gno/blob/joeson/gnovm/pkg/gnolang/joeson_f.go)\n\n### gnAsteroid\n\n![asteroid](https://raw.githubusercontent.com/grepsuzette/gfx/master/asteroid160.png)\n\n**gnAsteroid** is an asteroid creation-kit, it was started around the time the joeson port was published, but didn't have a name back then. \n\nAsteroids orbit gno.land, it's the same blockchain, but different frontend,\nthemable, working with wiki-like markdown files (enabling realms from gno.land\nto be rendered there).\n\n* [asteroid 0](https://gnAsteroid.com) - asteroid explaining what it is, containing instructions, to use, deploy on [Akash](https://gnasteroid.com/publishing/akash.md), [Vercel](https://gnasteroid.com/publishing/vercel.md).\n* [greps' asteroid](https://greps.gnAsteroid.com)\n* [gnAsteroid](https://github.com/gnAsteroid/gnAsteroid) - The github for gnAsteroid.\n\n### Research with markdown and gnoweb, mini-games, experiments (summer-oct 2024)\n\nA series of experiments with gnoweb 1.0 lead from the summer 2024, to try to advocate for keeping html\nand css enabled in gnoweb, or at least to try to determine what we could\npotentially miss without. Gnoweb1.0, markdown, html, css, js-less.\n\nNote those still work with [gnAsteroid](https://gnAsteroid.com), or with gnoweb\nrunning with the -web-html switch. As of now they are rendered through an\nasteroid.\n\n| 1                                                                                   | 2                                                                                     |\n| :-------------------:                                                               | :-------------------------:                                                           |\n| ![parrot](https://raw.githubusercontent.com/grepsuzette/gfx/master/parrot160.png)   | ![octopus](https://raw.githubusercontent.com/grepsuzette/gfx/master/octopus160.png)   |\n| [tic-tac-toe](https://greps.gnAsteroid.com/r/grepsuzette/pr2554/v6/games/tictactoe) | [minesweeper](https://greps.gnAsteroid.com/r/grepsuzette/pr2554/v6/games/minesweeper) |\n\nCheck the [other experiments here](/conjects/gnoweb.md).\n\n![octopus](https://raw.githubusercontent.com/grepsuzette/gfx/master/screen-minesweeper390.png)\n\n### Tendermint vuln retrospective (2023)\n\nAlso worked on an anthology of publicly knowned vulnerabilities that affected Tendermint. \n\n* [Cosmos-sdk vulnerability retrospective](https://github.com/gnolang/gno/issues/587)\n* found most vulns were not affecting our Tendermint version, however:\n* [demonstrated vulnerability to BSC 2022-10-07 hack](https://github.com/gnolang/gno/pull/583)\n* [proposed fix to vuln to BSC 2022-10-07 hack (merged)](https://github.com/gnolang/gno/pull/584)\n* not all of them were tested, as I was hoping some more feedback before to continue.\n\nThere is also a small [GNO mail](https://github.com/gnolang/gno/pull/641) which got no UI is discussed in [one of my articles](https://greps.gnasteroid.com/articles/encryptedmail.md).\n\nThanks for reading!\n`\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/grepsuzette/home\"\ngno = \"0.9\"\n"},{"name":"render.gno","body":"package home\n\nfunc Render(path string) string {\n\tswitch path {\n\tcase \"3\":\n\t\treturn r3()\n\tcase \"2\":\n\t\treturn r2()\n\tdefault:\n\t\treturn r1()\n\t}\n}\n\nconst tripleBackquote = \"```\"\nconst art = `\n                           (    )\n                          (    )\n                            )  )\n                           (  (                  /\\\n                            (_)                 /  \\  /\\\n                    ________[_]________      /\\/    \\/  \\\n           /\\      /\\        ______    \\    /   /\\/\\  /\\/\\\n          /  \\    //_\\       \\    /\\    \\  /\\/\\/    \\/    \\\n   /\\    / /\\/\\  //___\\       \\__/  \\    \\/      +     '   \n  /  \\  /\\/    \\//_____\\       \\ |[]|     \\    .    t     .\n /\\/\\/\\/       //_______\\       \\|__|      \\     p    e    \n/      \\      /XXXXXXXXXX\\                  \\      o    l  \n        \\    /_I_II  I__I_\\__________________\\  +    r    e\n               I_I|  I__I_____[]_|_[]_____I      .     t   \n               I_II  I__I_____[]_|_[]_____I          +    '\n               I II__I  I     XXXXXXX     I    \n            ~~~~~\"   \"~~~~~~~~~~~~~~~~~~~~~~~~   :*:*:*:*:*\n`\n\nfunc r1() string {\n\treturn \"# greps' (gn)home\" +\n\t\t`\nYou've reached the terrestrial realms of Grepsuzette on gno.land. \n\n` + tripleBackquote + art + tripleBackquote + `\n\nI am often on my [GNO asteroid](https://greps.gnAsteroid.com) too.\n\n* Public address: g1fjh9y7ausp27dqsdq0qrcsnmgvwm6829v2au7d\n* Contributor since summer 2022 ([notable contributions](/r/grepsuzette/home:3))\n* You can try my games in GNO (they use gnoweb -html):\n  * [tic-tac-toe](https://greps.gnasteroid.com/r/grepsuzette/pr2554/v6/games/tictactoe)\n  * [minesweeper](https://greps.gnasteroid.com/r/grepsuzette/pr2554/v6/games/minesweeper)\n`\n}\n\nfunc r2() string {\n\treturn `A manual index, until there's an automated way:\n\n* [home](home/): greps' home on gno.land\n* [games](games/): series of games\n\nI'm often on my [GNO asteroid][1] too.\n\n[1]: https://greps.gnAsteroid.com\n`\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"o0i1SPz3mto/KM2Owhnq0MqwIkzaUbJZwr00kLIUkDZvb498LRzEOCl/Kw5i/onZBvII38WljhMhVJ18A455Jg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"eventix","path":"gno.land/r/jjoptimist/eventix","files":[{"name":"eventix.gno","body":"package eventix\n\nimport (\n\t\"chain\"\n\t\"chain/runtime/unsafe\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"gno.land/p/nt/avl/pager/v0\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/grc20/v0\"\n\t\"gno.land/p/nt/grc721/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\ntype Event struct {\n\tname        string\n\tdescription string\n\tdate        time.Time\n\tmaxTickets  int\n\tprice       uint64\n\t// SECURITY XXX: paymentToken is `any` and accepted from the event\n\t// creator. We only check the dynamic type (*grc20.Token or \"ugnot\"),\n\t// not the token's identity. A caller can deploy their own grc20\n\t// token (the *grc20.Token type is canonical, so the type check passes)\n\t// where they hold an unlimited \"balance\" on every address. Buyers\n\t// then pay for tickets in worthless counterfeit tokens; the eventix\n\t// realm thinks it got paid and mints real NFT tickets. Free tickets\n\t// for the caller.\n\t// Fix: restrict paymentToken to tokens registered in r/nt/grc20reg/v0\n\t// (or a hardcoded allowlist of trusted token pkgPaths). Reject any\n\t// *grc20.Token that doesn't come from a known good source.\n\tpaymentToken any\n\tticketsSold  int\n}\n\nvar (\n\tevents              = avl.NewTree()\n\teventCounter uint64 = 0\n\ttickets      *grc721.Token\n\tticketLedger *grc721.PrivateLedger\n)\n\nfunc init(cur realm) {\n\ttickets, ticketLedger = grc721.NewToken(\"Event Ticket\", \"EVTIX\", 0, cur)\n}\n\nfunc CreateEvent(_ realm, name, description, dateStr string, paymentToken any, maxTickets int, price uint64) uint64 {\n\t// validate inputs\n\tif maxTickets \u003c= 0 {\n\t\tpanic(\"Maximum tickets must be greater than 0\")\n\t}\n\n\tdate, err := time.Parse(\"2006-01-02T15:04:05Z\", dateStr)\n\tif err != nil {\n\t\tpanic(\"Invalid date format. Use: YYYY-MM-DDThh:mm:ssZ\")\n\t}\n\n\tswitch pt := paymentToken.(type) {\n\tcase string:\n\t\tif pt != \"ugnot\" {\n\t\t\tpanic(\"Unsupported native token\")\n\t\t}\n\tcase *grc20.Token:\n\t\tif pt == nil {\n\t\t\tpanic(\"Invalid GRC20 token\")\n\t\t}\n\tdefault:\n\t\tpanic(\"Unsupported payment token type\")\n\t}\n\n\tnewID := eventCounter + 1\n\tevent := Event{\n\t\tname:         name,\n\t\tdescription:  description,\n\t\tdate:         date,\n\t\tmaxTickets:   maxTickets,\n\t\tprice:        price,\n\t\tticketsSold:  0,\n\t\tpaymentToken: paymentToken,\n\t}\n\tevents.Set(strconv.Itoa(int(newID)), event)\n\teventCounter = newID\n\tchain.Emit(\"EventCreated\", \"id\", strconv.FormatUint(newID, 10), \"name\", name)\n\treturn newID\n}\n\nfunc BuyTicket(cur realm, eventId uint64) {\n\tif !cur.Previous().IsUserCall() {\n\t\tpanic(\"only user-call (maketx call) accepted\")\n\t}\n\tevent, exists := getEvent(eventId)\n\tif !exists {\n\t\tpanic(\"Event does not exist\")\n\t}\n\n\tif event.ticketsSold \u003e= event.maxTickets {\n\t\tpanic(\"Event is sold out\")\n\t}\n\n\tbuyer := cur.Previous().Address()\n\n\tswitch pt := event.paymentToken.(type) {\n\tcase string:\n\t\tif pt != \"ugnot\" {\n\t\t\tpanic(\"Unsupported native token\")\n\t\t}\n\t\tif unsafe.OriginSend().AmountOf(\"ugnot\") != int64(event.price) {\n\t\t\tpanic(ufmt.Sprintf(\"Invalid payment amount: needs to be %dugnot\", event.price))\n\t\t}\n\tcase *grc20.Token:\n\t\t// eventix does not own this token, so it cannot use a frame-relative\n\t\t// teller to charge its caller — that shape is a confused deputy and is\n\t\t// exactly what the ledger-scoped CallerTeller now prevents. The buyer\n\t\t// approves eventix on the token first; eventix then spends that\n\t\t// allowance as itself, via a RealmTeller eagerly bound to its own\n\t\t// address.\n\t\tif err := pt.RealmTeller(0, cur).TransferFrom(0, cur, buyer, cur.Address(), int64(event.price)); err != nil {\n\t\t\tpanic(\"GRC20 transfer error: \" + err.Error())\n\t\t}\n\tdefault:\n\t\tpanic(\"Unsupported payment token type\")\n\t}\n\n\t// Mint NFT ticket\n\ttokenId := grc721.TokenID(ufmt.Sprintf(\"event_%d_ticket_%d\", eventId, event.ticketsSold+1))\n\tticketLedger.Mint(buyer, tokenId)\n\n\tevent.ticketsSold++\n\tevents.Set(strconv.Itoa(int(eventId)), event)\n}\n\nfunc Render(path string) string {\n\toutput := \"# Event Ticketing System\\n\\n\"\n\n\tpg := pager.NewPager(events, 10, true).MustGetPageByPath(path)\n\n\tfor _, item := range pg.Items {\n\t\tid, _ := strconv.ParseUint(item.Key, 10, 64)\n\t\tevent := item.Value.(Event)\n\n\t\toutput += ufmt.Sprintf(\"## Event #%d: %s\\n\", id, event.name)\n\t\toutput += ufmt.Sprintf(\"Description: %s\\n\", event.description)\n\t\toutput += ufmt.Sprintf(\"Date: %s\\n\", event.date.Format(time.RFC3339))\n\t\toutput += ufmt.Sprintf(\"Tickets: %d/%d\\n\", event.ticketsSold, event.maxTickets)\n\t\toutput += ufmt.Sprintf(\"Price: %d %v\\n\\n\", event.price, event.paymentToken)\n\n\t\tif event.ticketsSold \u003c event.maxTickets {\n\t\t\toutput += ufmt.Sprintf(\"[Buy Ticket](/r/jjoptimist/eventix/BuyTicket?eventId=%d)\\n\", id)\n\t\t} else {\n\t\t\toutput += \"**SOLD OUT**\\n\"\n\t\t}\n\t\toutput += \"---\\n\\n\"\n\t}\n\n\toutput += pg.Picker(path)\n\n\treturn output\n}\n\nfunc getEvent(eventId uint64) (Event, bool) {\n\tvalue := events.Get(strconv.Itoa(int(eventId)))\n\tif value == nil {\n\t\treturn Event{}, false\n\t}\n\treturn value.(Event), true\n}\n"},{"name":"eventix_test.gno","body":"package eventix\n\nimport (\n\t\"chain\"\n\t\"chain/runtime\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/grc20/v0\"\n\t\"gno.land/p/nt/grc721/v0\"\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\n// cur is a zero-value realm used as a placeholder when forwarding to\n// uassert/urequire dispatch helpers that gained an `rlm realm` param.\n// These tests pass `func()` callbacks (no crossing inside the callback),\n// so rlm is ignored — a nil realm here is safe.\nvar cur realm\n\nfunc TestCreateEvent(cur realm, t *testing.T) {\n\t// Test successful event creation\n\teventId := CreateEvent(\n\t\tcross(cur),\n\t\t\"Test Event\",\n\t\t\"A test event\",\n\t\t\"2024-12-31T23:59:59Z\",\n\t\t\"ugnot\",\n\t\t100,\n\t\t1000000,\n\t)\n\n\t// Convert eventId to int for comparison since CreateEvent returns uint64\n\tuassert.Equal(t, uint64(1), eventId)\n\n\tevent, exists := getEvent(eventId)\n\turequire.True(t, exists, \"Event was not created\")\n\tuassert.Equal(t, \"Test Event\", event.name)\n\n\t// Test invalid date format\n\turequire.AbortsWithMessage(t, cur, \"Invalid date format. Use: YYYY-MM-DDThh:mm:ssZ\", func() {\n\t\tCreateEvent(cross(cur), \"Test\", \"Test\", \"invalid-date\", \"ugnot\", 100, 1000000)\n\t})\n}\n\nfunc TestBuyTicket(cur realm, t *testing.T) {\n\t// Setup test event\n\teventId := CreateEvent(\n\t\tcross(cur),\n\t\t\"Test Event\",\n\t\t\"A test event\",\n\t\t\"2024-12-31T23:59:59Z\",\n\t\t\"ugnot\",\n\t\t2,\n\t\t1000000,\n\t)\n\n\t// Setup test buyer\n\tbuyer := address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\ttesting.SetOriginCaller(buyer)\n\n\tuassert.AbortsWithMessage(t, cur, \"Invalid payment amount: needs to be 1000000ugnot\", func() {\n\t\tBuyTicket(cross(cur), eventId)\n\t})\n\n\t// Test successful purchase\n\ttesting.SetOriginSend(chain.NewCoins(chain.NewCoin(\"ugnot\", 1000000)))\n\tBuyTicket(cross(cur), eventId)\n\n\tevent, _ := getEvent(eventId)\n\tif event.ticketsSold != 1 {\n\t\tt.Errorf(\"Expected 1 ticket sold, got %d\", event.ticketsSold)\n\t}\n\n\t// Verify NFT ownership\n\ttokenId := grc721.TokenID(ufmt.Sprintf(\"event_%d_ticket_%d\", eventId, 1))\n\towner, err := tickets.OwnerOf(tokenId)\n\turequire.NoError(t, err)\n\turequire.Equal(t, buyer, owner)\n\n\t// Test buying sold out event\n\ttesting.SetOriginSend(chain.NewCoins(chain.NewCoin(\"ugnot\", 1000000)))\n\tBuyTicket(cross(cur), eventId) // Buy second ticket\n\n\tuassert.AbortsWithMessage(t, cur, \"Event is sold out\", func() {\n\t\tBuyTicket(cross(cur), eventId)\n\t})\n}\n\nfunc TestBuyTicketWithGRC20(cur realm, t *testing.T) {\n\tpkgAddr := chain.PackageAddress(\"gno.land/r/jjoptimist/eventix\")\n\talice := testutils.TestAddress(\"alice\")\n\n\t// Token construction needs a realm-context cur (NewToken asserts\n\t// IsCurrent + non-empty PkgPath); SetRealm-to-EOA happens after.\n\tvar token *grc20.Token\n\tvar ledger *grc20.PrivateLedger\n\tfunc(cur realm) {\n\t\ttoken, ledger = grc20.NewToken(\"Test Token\", \"TEST\", 6, 0, cur)\n\t}(cross(cur))\n\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\tbuyer := address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\tadminAddr := address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf6\")\n\n\t// Set up admin and mint tokens\n\ttesting.SetOriginCaller(adminAddr)\n\ttesting.SetRealm(testing.NewUserRealm(adminAddr))\n\terr := ledger.Mint(buyer, 2000000) // Use ledger to mint\n\turequire.NoError(t, err)\n\n\t// Create event with GRC20 token payment\n\ttesting.SetOriginCaller(buyer)\n\ttesting.SetRealm(testing.NewUserRealm(buyer))\n\n\tvar eventId uint64\n\tcrossThrough(testing.NewCodeRealm(\"gno.land/r/jjoptimist/eventix\"), func() {\n\t\teventId = CreateEvent(\n\t\t\tcross(cur),\n\t\t\t\"GRC20 Event\",\n\t\t\t\"An event with GRC20 payment\",\n\t\t\t\"2024-12-31T23:59:59Z\",\n\t\t\ttoken,\n\t\t\t2,\n\t\t\t1000000,\n\t\t)\n\t})\n\n\t// Set up for approval\n\ttesting.SetOriginCaller(buyer)\n\ttesting.SetRealm(testing.NewUserRealm(buyer))\n\n\t// Approve using the ledger (following grc20factory_test pattern)\n\terr = ledger.Approve(buyer, pkgAddr, 1000000)\n\turequire.NoError(t, err)\n\n\t// Verify approval\n\tallowance := token.Allowance(buyer, pkgAddr)\n\turequire.Equal(t, int64(1000000), allowance, \"Approval should be set correctly\")\n\n\ttesting.SetOriginCaller(buyer)\n\ttesting.SetRealm(testing.NewUserRealm(buyer))\n\n\t// Buy ticket\n\tBuyTicket(cross(cur), eventId)\n\n\t// Verify purchase\n\tevent, exists := getEvent(eventId)\n\turequire.True(t, exists)\n\turequire.Equal(t, 1, event.ticketsSold)\n\n\t// Verify NFT ownership\n\ttokenId := grc721.TokenID(ufmt.Sprintf(\"event_%d_ticket_%d\", eventId, 1))\n\towner, err := tickets.OwnerOf(tokenId)\n\turequire.NoError(t, err)\n\turequire.Equal(t, buyer, owner)\n\n\t// Verify GRC20 balance changes\n\tbuyerBalance := token.BalanceOf(buyer)\n\turequire.Equal(t, int64(1000000), buyerBalance) // Should have 1M left after spending 1M\n}\n\nfunc crossThrough(rlm runtime.Realm, cr func()) {\n\ttesting.SetRealm(rlm)\n\tcr()\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/jjoptimist/eventix\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"LR7Gd6tEpjr1//Pv+j5d6MD8//oBdO3rNUR0ecdYG5k67ifR96qiPLP8L+YydN+xtowF8fTxsq+t8c6CMy4FDQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"home","path":"gno.land/r/jjoptimist/home","files":[{"name":"config.gno","body":"package home\n\nimport \"gno.land/p/nt/ownable/v0\"\n\ntype Config struct {\n\tTitle       string\n\tDescription string\n\tGithub      string\n}\n\nvar config = Config{\n\tTitle:       \"JJOptimist's Home Realm 🏠\",\n\tDescription: \"Exploring Gno and building on-chain\",\n\tGithub:      \"jjoptimist\",\n}\n\nvar Ownable = ownable.NewWithAddress(address(\"g16vfw3r7zuz43fhky3xfsuc2hdv9tnhvlkyn0nj\"))\n\nfunc GetConfig() Config {\n\treturn config\n}\n\nfunc UpdateConfig(cur realm, newTitle, newDescription, newGithub string) {\n\tOwnable.AssertOwnedBy(cur.Previous().Address())\n\tconfig.Title = newTitle\n\tconfig.Description = newDescription\n\tconfig.Github = newGithub\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/jjoptimist/home\"\ngno = \"0.9\"\n"},{"name":"home.gno","body":"package home\n\nimport (\n\t\"chain/runtime\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"gno.land/r/leon/hor\"\n)\n\nconst (\n\tgnomeArt1 = `   /\\\n  /  \\\n ,,,,,\n(o.o)\n(\\_/)\n-\"-\"-`\n\n\tgnomeArt2 = `   /\\\n  /  \\\n ,,,,,\n(^.^)\n(\\_/)\n -\"-`\n\n\tgnomeArt3 = `   /\\\n  /  \\\n ,,,,,\n(*.*)\n(\\_/)\n\"-\"-\"`\n\n\tgnomeArt4 = `   /\\\n  /  \\\n ,,,,,\n(o.~)\n(\\_/)\n -\"-`\n)\n\nvar creation time.Time\n\nfunc getGnomeArt(height int64) string {\n\tvar art string\n\tswitch {\n\tcase height%7 == 0:\n\t\tart = gnomeArt4 // winking gnome\n\tcase height%5 == 0:\n\t\tart = gnomeArt3 // starry-eyed gnome\n\tcase height%3 == 0:\n\t\tart = gnomeArt2 // happy gnome\n\tdefault:\n\t\tart = gnomeArt1 // regular gnome\n\t}\n\treturn \"```\\n\" + art + \"\\n```\\n\"\n}\n\nfunc init(cur realm) {\n\tcreation = time.Now()\n\thor.Register(cross(cur), \"JJoptimist's Home Realm\", \"\")\n}\n\nfunc Render(path string) string {\n\theight := runtime.ChainHeight()\n\n\toutput := \"# \" + config.Title + \"\\n\\n\"\n\n\toutput += \"## About Me\\n\"\n\toutput += \"- 👋 Hi, I'm JJOptimist\\n\"\n\toutput += getGnomeArt(height)\n\toutput += \"- 🌱 \" + config.Description + \"\\n\"\n\n\toutput += \"## Contact\\n\"\n\toutput += \"- 📫 GitHub: [\" + config.Github + \"](https://github.com/\" + config.Github + \")\\n\"\n\n\toutput += \"\\n---\\n\"\n\toutput += \"_Realm created: \" + creation.Format(\"2006-01-02 15:04:05 UTC\") + \"_\\n\"\n\toutput += \"_Owner: \" + Ownable.Owner().String() + \"_\\n\"\n\toutput += \"_Current Block Height: \" + strconv.Itoa(int(height)) + \"_\"\n\n\treturn output\n}\n"},{"name":"home_test.gno","body":"package home\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestConfig(t *testing.T) {\n\tcfg := GetConfig()\n\n\tif cfg.Title != \"JJOptimist's Home Realm 🏠\" {\n\t\tt.Errorf(\"Expected title to be 'JJOptimist's Home Realm 🏠', got %s\", cfg.Title)\n\t}\n\tif cfg.Description != \"Exploring Gno and building on-chain\" {\n\t\tt.Errorf(\"Expected description to be 'Exploring Gno and building on-chain', got %s\", cfg.Description)\n\t}\n\tif cfg.Github != \"jjoptimist\" {\n\t\tt.Errorf(\"Expected github to be 'jjoptimist', got %s\", cfg.Github)\n\t}\n}\n\nfunc TestRender(t *testing.T) {\n\toutput := Render(\"\")\n\n\t// Test that required sections are present\n\tif !strings.Contains(output, \"# \"+config.Title) {\n\t\tt.Error(\"Rendered output missing title\")\n\t}\n\tif !strings.Contains(output, \"## About Me\") {\n\t\tt.Error(\"Rendered output missing About Me section\")\n\t}\n\tif !strings.Contains(output, \"## Contact\") {\n\t\tt.Error(\"Rendered output missing Contact section\")\n\t}\n\tif !strings.Contains(output, config.Description) {\n\t\tt.Error(\"Rendered output missing description\")\n\t}\n\tif !strings.Contains(output, config.Github) {\n\t\tt.Error(\"Rendered output missing github link\")\n\t}\n}\n\nfunc TestGetGnomeArt(t *testing.T) {\n\ttests := []struct {\n\t\theight   int64\n\t\texpected string\n\t}{\n\t\t{7, gnomeArt4}, // height divisible by 7\n\t\t{5, gnomeArt3}, // height divisible by 5\n\t\t{3, gnomeArt2}, // height divisible by 3\n\t\t{2, gnomeArt1}, // default case\n\t}\n\n\tfor _, tt := range tests {\n\t\tart := getGnomeArt(tt.height)\n\t\tif !strings.Contains(art, tt.expected) {\n\t\t\tt.Errorf(\"For height %d, expected art containing %s, got %s\", tt.height, tt.expected, art)\n\t\t}\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"WbxjD1DMrhIb+JIDRB9hkqUTmAr0rVoEqkIg82pAMoclLL9OXdKYjxFBbUSRfDfMmgeY+elbkBkU+iEPJ8rdcw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5","package":{"name":"derive","path":"gno.land/r/leon/derive","files":[{"name":"derive.gno","body":"package derive\n\nimport (\n\t\"chain\"\n\t\"chain/runtime\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/leon/hor\"\n)\n\nvar cd = runtime.ChainDomain()\n\nfunc Render(path string) string {\n\tout := \"# Derive Realm Address\\n\\n\"\n\n\tout += `\u003cgno-form\u003e\n \t \u003cgno-input name=\"pkgpath\" placeholder=\"gno.land/{r,p}/...\" /\u003e\n\t\u003c/gno-form\u003e\n`\n\n\tif strings.Contains(path, \"pkgpath\") {\n\t\tparsed := parsePkgPath(path)\n\t\tif parsed == \"\" {\n\t\t\tout += \"Please input a valid pkgpath.\"\n\t\t\treturn out\n\t\t}\n\t\tout += ufmt.Sprintf(\"### [%s](%s) matches %s\", parsed, strings.TrimPrefix(parsed, \"gno.land\"), chain.PackageAddress(parsed))\n\t}\n\n\treturn out\n}\n\nfunc parsePkgPath(path string) string {\n\tu, err := url.Parse(path)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tpkgpath := u.Query().Get(\"pkgpath\")\n\tif pkgpath == \"\" {\n\t\treturn \"\"\n\t}\n\n\tswitch {\n\tcase strings.HasPrefix(pkgpath, \"gno.land/r/\"),\n\t\tstrings.HasPrefix(pkgpath, \"gno.land/p/\"):\n\t\t// already absolute and valid\n\t\treturn pkgpath\n\n\tcase strings.HasPrefix(pkgpath, \"r/\"),\n\t\tstrings.HasPrefix(pkgpath, \"p/\"):\n\t\t// relative path, normalize\n\t\treturn \"gno.land/\" + pkgpath\n\n\tcase strings.HasPrefix(pkgpath, \"/r/\"),\n\t\tstrings.HasPrefix(pkgpath, \"/p/\"):\n\t\t// relative with leading slash\n\t\treturn \"gno.land\" + pkgpath\n\n\tdefault:\n\t\t// reject invalid\n\t\treturn \"\"\n\t}\n}\n\nfunc init(cur realm) {\n\thor.Register(cross(cur), \"Derive\", \"Quickly derive a Gno address from a pkg path.\")\n}\n"},{"name":"derive_test.gno","body":"package derive\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestParsePkgPath(t *testing.T) {\n\tcases := []struct {\n\t\tname     string\n\t\tpath     string\n\t\texpected string\n\t}{\n\t\t// r/ namespace\n\t\t{\"full r path\", \"gno.land/r/leon/home\", \"gno.land/r/leon/home\"},\n\t\t{\"relative r path with slash\", \"/r/leon/home\", \"gno.land/r/leon/home\"},\n\t\t{\"relative r path no slash\", \"r/leon/home\", \"gno.land/r/leon/home\"},\n\n\t\t// p/ namespace\n\t\t{\"full p path\", \"gno.land/p/leon/home\", \"gno.land/p/leon/home\"},\n\t\t{\"relative p path with slash\", \"/p/leon/home\", \"gno.land/p/leon/home\"},\n\t\t{\"relative p path no slash\", \"p/leon/home\", \"gno.land/p/leon/home\"},\n\n\t\t// invalid cases\n\t\t{\"random text\", \"randomtext\", \"\"},\n\t\t{\"random text\", \"randomtext with space\", \"\"},\n\t\t{\"empty\", \"\", \"\"},\n\t\t{\"wrong namespace q/\", \"q/leon/home\", \"\"},\n\t\t{\"no pkgpath query param\", \"\", \"\"},\n\t\t{\"malformed url\", \"::::\", \"\"},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.name, func(t *testing.T) {\n\t\t\turl := \"gno.land/r/leon/derive?pkgpath=\" + c.path\n\t\t\tresult := parsePkgPath(url)\n\t\t\tif result != c.expected {\n\t\t\t\tt.Errorf(\"for input %q expected %q, got %q\", c.path, c.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDerive(t *testing.T) {\n\tgot := Render(\"?pkgpath=gno.land/r/leon/home\")\n\tif !strings.Contains(got, \"g1n0z9kw63c6ze8fgle93s2e86hfm2qz025cgkey\") {\n\t\t// manually checked the above address\n\t\tt.Fatal(\"Failed derivation!\")\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/leon/derive\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"cEZzQ4Zy7WK4iwmWgD8HlrQlxIIVrGAeY2gFpGAXRL5WdfBui0I+QzuT+ajHVFFS2n/mwtWPkEZpnwd/m1O2zg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5","package":{"name":"home","path":"gno.land/r/leon/home","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/leon/home\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\"\n"},{"name":"home.gno","body":"package home\n\nimport (\n\t\"chain/banker\"\n\t\"chain/runtime\"\n\t\"chain/runtime/unsafe\"\n\t\"strconv\"\n\n\t\"gno.land/p/leon/svgbtn/v0\"\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/nt/svg/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\n\t\"gno.land/r/demo/art/gnoface\"\n\t\"gno.land/r/demo/art/millipede\"\n\t\"gno.land/r/demo/mirror\"\n\t\"gno.land/r/leon/config\"\n\t\"gno.land/r/leon/hor\"\n)\n\nvar (\n\tpfp        string // link to profile picture\n\tpfpCaption string // profile picture caption\n\tabtMe      [2]string\n)\n\nfunc Render(path string) string {\n\tout := \"# Leon's Homepage\\n\\n\"\n\n\tif path == \"buttons\" {\n\t\treturn renderButtonPage()\n\t}\n\n\tout += renderAboutMe()\n\tout += renderArt()\n\tout += config.Banner()\n\tout += \"\\n\\n\"\n\tout += svgbtn.Button(\n\t\t1200,\n\t\t50,\n\t\tgnomeBodyColors[int(runtime.ChainHeight()+1)%len(gnomeBodyColors)],\n\t\t\"#ffffff\",\n\t\t\"Support my work!\",\n\t\t\"/r/leon/home$help\u0026func=Donate\u0026.send=1000000ugnot\",\n\t)\n\n\treturn out\n}\n\nfunc init(cur realm) {\n\thor.Register(cross(cur), \"Leon's Home Realm\", \"\")\n\tmirror.Register(cross(cur), cur.PkgPath(), Render)\n\n\tpfp = \"https://i.imgflip.com/91vskx.jpg\"\n\tpfpCaption = \"[My favourite painting \u0026 pfp](https://en.wikipedia.org/wiki/Wanderer_above_the_Sea_of_Fog)\"\n\tabtMe = [2]string{\n\t\t`### About me\nHi, I'm Leon, a DevRel Engineer at gno.land. I am a tech enthusiast, \nlife-long learner, and sharer of knowledge.`,\n\t\t`### Contributions\nMy contributions to Gno.land can mainly be found \n[on GitHub](https://github.com/gnolang/gno/issues?q=sort:updated-desc+author:leohhhn), and on [the chain](/u/leon).\n\nTODO import r/gh`,\n\t}\n}\n\nfunc UpdatePFP(cur realm, url, caption string) {\n\tif !config.IsAuthorized(cur.Previous().Address()) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\n\tpfp = url\n\tpfpCaption = caption\n}\n\nfunc UpdateAboutMe(cur realm, col1, col2 string) {\n\tif !config.IsAuthorized(cur.Previous().Address()) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\n\tabtMe[0] = col1\n\tabtMe[1] = col2\n}\n\nfunc Donate(cur realm) string {\n\tsent := unsafe.OriginSend()\n\tif len(sent) == 0 {\n\t\treturn \":c\"\n\t}\n\n\tbanker.NewBanker(banker.BankerTypeOriginSend, cur).SendCoins(\n\t\tcur.Address(),\n\t\tconfig.OwnableMain.Owner(),\n\t\tsent,\n\t) // wish this was prettier :)\n\n\treturn \"Thanks for donating \" + sent.String() + \" \u003c3\"\n}\n\nfunc renderAboutMe() string {\n\treturn md.Columns([]string{\n\t\tufmt.Sprintf(\"![my profile pic](%s)\\n\\n%s\\n\", pfp, pfpCaption),\n\t\tabtMe[0],\n\t\tabtMe[1],\n\t}, false)\n}\n\nfunc renderArt() string {\n\tout := \"# Gno Art\\n\"\n\n\tout += md.Columns([]string{\n\t\tgnoface.Render(strconv.Itoa(int(runtime.ChainHeight()))),\n\t\trenderMillipede(),\n\t\t\"SVG Gnome\\n\" + RenderSVGGnome(),\n\t}, false)\n\n\tout += \"This art is dynamic; it will change with every new block.\\n\\n\"\n\n\treturn out\n}\n\nfunc renderMillipede() string {\n\tout := \"Millipede\\n\\n\"\n\tout += \"```\\n\" + millipede.Draw(int(runtime.ChainHeight())%10+1) + \"```\\n\"\n\treturn out\n}\n\nfunc renderBlogPosts() string {\n\tout := \"\"\n\t// out += \"## Leon's Blog Posts\"\n\n\t// todo fetch blog posts authored by @leohhhn\n\t// and render them\n\treturn out\n}\n\nfunc RenderSVGGnome() string { // exported for your pleasure :)\n\tc := svg.NewCanvas(430, 430).WithViewBox(13, 25, 75, 75)\n\n\t// Body: blue triangle\n\tbody := svg.NewPolygon(\"50,50 30,100 70,100\", gnomeBodyColors[int(runtime.ChainHeight())%len(gnomeBodyColors)])\n\n\t// Head: peach circle (overlaps body)\n\thead := svg.NewCircle(50, 60, 10, \"#FAD7B6\")\n\n\t// Hat: red triangle on top of head\n\that := svg.NewPolygon(\"50,30 35,55 65,55\", \"#E53935\")\n\n\t// Eyes: two small black dots\n\tleftEye := svg.NewCircle(46, 59, 1, \"#000\")\n\trightEye := svg.NewCircle(54, 59, 1, \"#000\")\n\n\t// Beard: small white triangle under head\n\tbeard := svg.NewPolygon(\"50,85 42,63 58,63\", \"#FFF\")\n\n\t// Layering order matters (bottom to top)\n\tc.Append(body, head, beard, hat, leftEye, rightEye)\n\n\treturn c.Render(\"svg gnome\")\n}\n\nfunc renderButtonPage() string {\n\tout := \"# Buttons Demo\\n\\n\"\n\n\tout += md.ColumnsN([]string{\n\t\tsvgbtn.PrimaryButton(140, 45, \"Click Me\", \"/r/leon/home:click\") + \"\\n\\n\",\n\t\tsvgbtn.DangerButton(140, 45, \"Delete\", \"/delete\") + \"\\n\\n\",\n\t\tsvgbtn.SuccessButton(140, 45, \"Go Home\", \"/r/leon/home\") + \"\\n\\n\",\n\t\tsvgbtn.SmallButton(100, 45, \"Edit\", \"/edit\") + \"\\n\\n\",\n\t\tsvgbtn.WideButton(200, 40, \"Big Action\", \"/big\") + \"\\n\\n\",\n\t\tsvgbtn.TextButton(100, 30, \"More Info\", \"/r/leon/home:info\") + \"\\n\\n\",\n\t\tsvgbtn.IconButton(100, 40, \"Config\", \"/r/leon/config\") + \"\\n\\n\",\n\t}, 3, true)\n\n\treturn out\n}\n\nvar gnomeBodyColors = []string{\n\t\"#4CAF50\", // Green\n\t\"#2196F3\", // Blue\n\t\"#9C27B0\", // Purple\n\t\"#FF5722\", // Orange\n\t\"#795548\", // Brown\n\t\"#607D8B\", // Grayish Blue\n\t\"#E91E63\", // Pink\n\t\"#FFC107\", // Amber\n\t\"#00BCD4\", // Cyan\n\t\"#8BC34A\", // Light Green\n\t\"#FF9800\", // Deep Orange\n\t\"#3F51B5\", // Indigo\n\t\"#673AB7\", // Deep Purple\n\t\"#009688\", // Teal\n\t\"#F44336\", // Red\n\t\"#CDDC39\", // Lime\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"0E4issyGwnHg5HXeDqn9nV+5rNFRkDQwMGiXNtkdyKt7Koccl4gVwzkBzakGJmMPRQsY5G7naLYRYUbEClK9Ww=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"blog","path":"gno.land/r/lou/blog","files":[{"name":"actions.gno","body":"package blog\n\nimport (\n\t\"chain/runtime/unsafe\"\n\t\"strings\"\n\n\t\"gno.land/p/lou/blog\"\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/moul/txlink/v0\"\n)\n\nfunc CreatePost(cur realm, slug, title, body, publicationDate, authors, tags string) {\n\tauthorsField := strings.Split(authors, \" \")\n\ttagsField := strings.Split(tags, \" \")\n\n\tpost, err := blog.NewPost(\n\t\tslug,\n\t\ttitle,\n\t\tbody,\n\t\tpublicationDate,\n\t\tunsafe.OriginCaller().String(),\n\t\tauthorsField,\n\t\ttagsField,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tactionLinks := md.Link(\"Like\", txlink.Call(\"LikePostBySlug\", \"slug\", post.Slug()))\n\tpost.SetPreviewFooter(actionLinks + \"\\n\\n\")\n\tif err := myBlog.AddPost(0, cur, post); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc UpdatePost(cur realm, id, slug, title, body, publicationDate, authors, tags string) {\n\tvar err error\n\tvar post *blog.Post\n\tauthorsField := strings.Split(authors, \" \")\n\ttagsField := strings.Split(tags, \" \")\n\n\t_, err = myBlog.GetPostById(id)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tpost, err = blog.NewPost(\n\t\tslug,\n\t\ttitle,\n\t\tbody,\n\t\tpublicationDate,\n\t\tunsafe.OriginCaller().String(),\n\t\tauthorsField,\n\t\ttagsField,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif err := myBlog.UpdatePostById(0, cur, id, post); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc UpdatePostBySlug(cur realm, slug, title, body, publicationDate, authors, tags string) {\n\tvar err error\n\tvar post *blog.Post\n\tauthorsField := strings.Split(authors, \" \")\n\ttagsField := strings.Split(tags, \" \")\n\n\t_, err = myBlog.GetPostBySlug(slug)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tpost, err = blog.NewPost(\n\t\tslug,\n\t\ttitle,\n\t\tbody,\n\t\tpublicationDate,\n\t\tunsafe.OriginCaller().String(),\n\t\tauthorsField,\n\t\ttagsField,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif err := myBlog.UpdatePostBySlug(0, cur, slug, post); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc DeletePost(cur realm, slug string) {\n\tif err := myBlog.DeletePostBySlug(0, cur, slug); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc ToggleLikePostBySlug(cur realm, slug string) {\n\tif err := myBlog.LikePostBySlug(0, cur, slug); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc AddCommentToPostBySlug(cur realm, slug, comment string) {\n\tvar err error\n\tpost, err := myBlog.GetPostBySlug(slug)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tcomm, err := blog.NewComment(cur.Previous().Address().String(), comment)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif err := post.AddComment(comm); err != nil {\n\t\tpanic(err)\n\t}\n}\n"},{"name":"blog.gno","body":"package blog\n\nimport (\n\t\"gno.land/p/lou/blog\"\n\n\t\"gno.land/r/leon/hor\"\n\t\"gno.land/r/sys/users\"\n)\n\nvar (\n\tmyBlog *blog.Blog\n\tadmin  = address(\"g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs\")\n)\n\nfunc init(cur realm) {\n\tmyBlog, _ = blog.NewBlog(\n\t\t0, cur,\n\t\t\"Lou's Blog\",\n\t\tadmin,\n\t\tblog.WithUserResolver(myResolver),\n\t)\n\thor.Register(cross(cur), \"Lou's blog realm\", \"\")\n}\n\nfunc Render(path string) string {\n\tout := \"\"\n\tout += myBlog.Render(path)\n\treturn out\n}\n\nfunc myResolver(input string) (string, bool) {\n\tdata, ok := users.ResolveAny(input)\n\tif !ok || data == nil {\n\t\treturn \"\", false\n\t}\n\treturn data.Name(), true\n}\n"},{"name":"blog_test.gno","body":"package blog\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/lou/blog\"\n)\n\nfunc clearState(_ int, rlm realm, t *testing.T) {\n\tt.Helper()\n\tvar err error\n\n\tadmin = address(\"g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs\")\n\tmyBlog, err = blog.NewBlog(\n\t\t0, rlm,\n\t\t\"Lou's Blog\",\n\t\tadmin,\n\t\tblog.WithUserResolver(myResolver),\n\t)\n\tif err != nil {\n\t\tt.Errorf(err.Error())\n\t}\n\t// inPause = false\n}\n\nfunc TestPackage(cur realm, t *testing.T) {\n\tclearState(0, cur, t)\n\n\ttesting.SetOriginCaller(admin)\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\n\t{\n\t\tprintln(\"Creating posts and rendering previews...\")\n\t\tCreatePost(cross(cur), \"slug1\", \"title1\", \"body1\", \"2022-05-20T13:17:22Z\", \"lou\", \"tag1 tag2\")\n\t\tCreatePost(cross(cur), \"slug2\", \"title2\", \"body2\", \"2022-05-20T13:17:23Z\", \"lou\", \"tag1 tag3\")\n\t\tgot := Render(\"\")\n\t\texpected := `# Lou's Blog\n\n[⊞ grid](/r/lou/blog:?mode=grid) | [≔ list](/r/lou/blog:?mode=list) | [⧖ relative](/r/lou/blog:?time=short) | [↕ alphabetical \\(A\\-Z\\)](/r/lou/blog:?order=asc\u0026sort=alpha) | [↕ recent](/r/lou/blog:?order=asc\u0026sort=recent) | [↕ last updated](/r/lou/blog:?order=asc\u0026sort=update) | [past year](/r/lou/blog:?end=2009-02-13\u0026start=2008-02-13), [this year](/r/lou/blog:?end=2009-02-13\u0026start=2009-01-01), [last 30 days](/r/lou/blog:?end=2009-02-13\u0026start=2009-01-14) | [⟳ reset](/r/lou/blog)\n\n\u003cgno-columns\u003e\n\u003cgno-columns\u003e\n## [title2](/r/lou/blog:posts/slug2)\n\n\n##### */slug2*\n\n\njust now\n\n**tags:** ` + \"`\" + `tag1` + \"`\" + `, ` + \"`\" + `tag3` + \"`\" + `\n\n**[comments \\(0](/r/lou/blog:posts/slug2#comments)**) | ❤️ (0)\n\n[Like](/r/lou/blog$help\u0026func=LikePostBySlug\u0026slug=slug2)\n\n\n\n---\n\u003cgno-columns-sep\u003e\n## [title1](/r/lou/blog:posts/slug1)\n\n\n##### */slug1*\n\n\njust now\n\n**tags:** ` + \"`\" + `tag1` + \"`\" + `, ` + \"`\" + `tag2` + \"`\" + `\n\n**[comments \\(0](/r/lou/blog:posts/slug1#comments)**) | ❤️ (0)\n\n[Like](/r/lou/blog$help\u0026func=LikePostBySlug\u0026slug=slug1)\n\n\n\n---\n\u003cgno-columns-sep\u003e\n\u003c/gno-columns\u003e`\n\t\tassertMDEquals(t, got, expected)\n\t}\n\n\t{\n\t\tprintln(\"Viewing one of the post...\")\n\t\tgot := Render(\"posts/slug2\")\n\t\texpected := `# title2\n\n\n*Author(s):* [lou](/r/lou/blog:authors/lou)\n\n\nbody2\n\n---\n**Created on:** May 20, 2022 at 1:17 PM\n\n**Published on:** February 13, 2009 at 11:31 PM\n\n**Publisher:** [g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs](/u/g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs)\n\n**Tags:** [\\#tag1](/r/lou/blog:tags/tag1), [\\#tag3](/r/lou/blog:tags/tag3)\n\n\n❤️ 0\n\n---\n### Comments (0)\n\n\nNo comments yet.`\n\t\tassertMDEquals(t, got, expected)\n\t}\n\n\t{\n\t\tprintln(\"Listing of tags...\")\n\t\tprintln(\"Listing\")\n\t\tgot := Render(\"authors\")\n\t\texpected := `## [/r/lou/blog](/r/lou/blog)/authors\n[↕ alphabetical \\(A\\-Z\\)](/r/lou/blog:authors?order=asc\u0026sort=alpha) | [↕ recent](/r/lou/blog:authors?order=asc\u0026sort=recent) | [↕ most common](/r/lou/blog:authors?order=asc\u0026sort=common) | [past year](/r/lou/blog:authors?end=2009-02-13\u0026start=2008-02-13), [this year](/r/lou/blog:authors?end=2009-02-13\u0026start=2009-01-01), [last 30 days](/r/lou/blog:authors?end=2009-02-13\u0026start=2009-01-14) | [⟳ reset](/r/lou/blog)\n\n### [lou](/r/lou/blog:authors/lou) (2)`\n\t\tassertMDEquals(t, got, expected)\n\n\t\tprintln(\"Invalid tag\")\n\t\tgot = Render(\"tags/invalid\")\n\t\texpected = `## [/r/lou/blog](/r/lou/blog)/[tags](/r/lou/blog:tags)/invalid\n[⊞ grid](/r/lou/blog:tags/invalid?mode=grid) | [≔ list](/r/lou/blog:tags/invalid?mode=list) | [⧖ relative](/r/lou/blog:tags/invalid?time=short) | [↕ alphabetical \\(A\\-Z\\)](/r/lou/blog:tags/invalid?order=asc\u0026sort=alpha) | [↕ recent](/r/lou/blog:tags/invalid?order=asc\u0026sort=recent) | [↕ last updated](/r/lou/blog:tags/invalid?order=asc\u0026sort=update) | [past year](/r/lou/blog:tags/invalid?end=2009-02-13\u0026start=2008-02-13), [this year](/r/lou/blog:tags/invalid?end=2009-02-13\u0026start=2009-01-01), [last 30 days](/r/lou/blog:tags/invalid?end=2009-02-13\u0026start=2009-01-14) | [⟳ reset](/r/lou/blog)\n\nNo posts found for this tag.\u003cgno-columns\u003e`\n\t\tassertMDEquals(t, got, expected)\n\n\t\tprintln(\"Valid tag\")\n\t\tgot = Render(\"tags/tag2\")\n\t\texpected = `## [/r/lou/blog](/r/lou/blog)/[tags](/r/lou/blog:tags)/tag2\n[⊞ grid](/r/lou/blog:tags/tag2?mode=grid) | [≔ list](/r/lou/blog:tags/tag2?mode=list) | [⧖ relative](/r/lou/blog:tags/tag2?time=short) | [↕ alphabetical \\(A\\-Z\\)](/r/lou/blog:tags/tag2?order=asc\u0026sort=alpha) | [↕ recent](/r/lou/blog:tags/tag2?order=asc\u0026sort=recent) | [↕ last updated](/r/lou/blog:tags/tag2?order=asc\u0026sort=update) | [past year](/r/lou/blog:tags/tag2?end=2009-02-13\u0026start=2008-02-13), [this year](/r/lou/blog:tags/tag2?end=2009-02-13\u0026start=2009-01-01), [last 30 days](/r/lou/blog:tags/tag2?end=2009-02-13\u0026start=2009-01-14) | [⟳ reset](/r/lou/blog)\n\n\u003cgno-columns\u003e\n\u003cgno-columns\u003e\n## [title1](/r/lou/blog:posts/slug1)\n\n\n##### */slug1*\n\n\njust now\n\n**author(s):** lou\n\n**tags:** ` + \"`tag1`, `tag2`\" + `\n\n**[comments \\(0](/r/lou/blog:posts/slug1#comments)**) | ❤️ (0)\n\n[Like](/r/lou/blog$help\u0026func=LikePostBySlug\u0026slug=slug1)\n\n\n\n---\n\n\u003c/gno-columns\u003e`\n\t\tassertMDEquals(t, got, expected)\n\t}\n\n\t{\n\t\tprintln(\"Listing of authors...\")\n\t\tprintln(\"Listing\")\n\t\tgot := Render(\"authors\")\n\t\texpected := `## [/r/lou/blog](/r/lou/blog)/authors\n[↕ alphabetical \\(A\\-Z\\)](/r/lou/blog:authors?order=asc\u0026sort=alpha) | [↕ recent](/r/lou/blog:authors?order=asc\u0026sort=recent) | [↕ most common](/r/lou/blog:authors?order=asc\u0026sort=common) | [past year](/r/lou/blog:authors?end=2009-02-13\u0026start=2008-02-13), [this year](/r/lou/blog:authors?end=2009-02-13\u0026start=2009-01-01), [last 30 days](/r/lou/blog:authors?end=2009-02-13\u0026start=2009-01-14) | [⟳ reset](/r/lou/blog)\n\n### [lou](/r/lou/blog:authors/lou) (2)`\n\t\tassertMDEquals(t, got, expected)\n\n\t\tprintln(\"Invalid author\")\n\t\tgot = Render(\"authors/invalid\")\n\t\texpected = `## [/r/lou/blog](/r/lou/blog)/[authors](/r/lou/blog:authors)/invalid\n#### [@invalid](/u/invalid)'s profile\n[⊞ grid](/r/lou/blog:authors/invalid?mode=grid) | [≔ list](/r/lou/blog:authors/invalid?mode=list) | [⧖ relative](/r/lou/blog:authors/invalid?time=short) | [↕ alphabetical \\(A\\-Z\\)](/r/lou/blog:authors/invalid?order=asc\u0026sort=alpha) | [↕ recent](/r/lou/blog:authors/invalid?order=asc\u0026sort=recent) | [↕ last updated](/r/lou/blog:authors/invalid?order=asc\u0026sort=update) | [past year](/r/lou/blog:authors/invalid?end=2009-02-13\u0026start=2008-02-13), [this year](/r/lou/blog:authors/invalid?end=2009-02-13\u0026start=2009-01-01), [last 30 days](/r/lou/blog:authors/invalid?end=2009-02-13\u0026start=2009-01-14) | [⟳ reset](/r/lou/blog)\n\nNo posts found for this author.\u003cgno-columns\u003e`\n\t\tassertMDEquals(t, got, expected)\n\n\t\tprintln(\"Valid author\")\n\t\tgot = Render(\"authors/lou\")\n\t\texpected = `## [/r/lou/blog](/r/lou/blog)/[authors](/r/lou/blog:authors)/lou\n#### [@lou](/u/lou)'s profile\n[⊞ grid](/r/lou/blog:authors/lou?mode=grid) | [≔ list](/r/lou/blog:authors/lou?mode=list) | [⧖ relative](/r/lou/blog:authors/lou?time=short) | [↕ alphabetical \\(A\\-Z\\)](/r/lou/blog:authors/lou?order=asc\u0026sort=alpha) | [↕ recent](/r/lou/blog:authors/lou?order=asc\u0026sort=recent) | [↕ last updated](/r/lou/blog:authors/lou?order=asc\u0026sort=update) | [past year](/r/lou/blog:authors/lou?end=2009-02-13\u0026start=2008-02-13), [this year](/r/lou/blog:authors/lou?end=2009-02-13\u0026start=2009-01-01), [last 30 days](/r/lou/blog:authors/lou?end=2009-02-13\u0026start=2009-01-14) | [⟳ reset](/r/lou/blog)\n\n\u003cgno-columns\u003e\n\u003cgno-columns\u003e\n## [title2](/r/lou/blog:posts/slug2)\n\n\n##### */slug2*\n\n\njust now\n\n**author(s):** lou\n\n**tags:** ` + \"`tag1`, `tag3`\" + `\n\n**[comments \\(0](/r/lou/blog:posts/slug2#comments)**) | ❤️ (0)\n\n[Like](/r/lou/blog$help\u0026func=LikePostBySlug\u0026slug=slug2)\n\n\n\n---\n\n## [title1](/r/lou/blog:posts/slug1)\n\n\n##### */slug1*\n\n\njust now\n\n**author(s):** lou\n\n**tags:** ` + \"`tag1`, `tag2`\" + `\n\n**[comments \\(0](/r/lou/blog:posts/slug1#comments)**) | ❤️ (0)\n\n[Like](/r/lou/blog$help\u0026func=LikePostBySlug\u0026slug=slug1)\n\n\n\n---\n\n\u003c/gno-columns\u003e`\n\t\tassertMDEquals(t, got, expected)\n\t}\n\n\t{\n\t\tprintln(\"Adding comments on a post...\")\n\t\tAddCommentToPostBySlug(cross(cur), \"slug1\", \"comment1\")\n\t\tAddCommentToPostBySlug(cross(cur), \"slug2\", \"comment2\")\n\t\tAddCommentToPostBySlug(cross(cur), \"slug1\", \"comment3\")\n\t\tAddCommentToPostBySlug(cross(cur), \"slug2\", \"comment4\")\n\t\tAddCommentToPostBySlug(cross(cur), \"slug1\", \"comment5\")\n\t\tgot := Render(\"posts/slug2\")\n\t\texpected := `# title2\n\n\n[2 Comment\\(s\\)](#comments)\n\n*Author(s):* [lou](/r/lou/blog:authors/lou)\n\n\nbody2\n\n---\n**Created on:** May 20, 2022 at 1:17 PM\n\n**Published on:** February 13, 2009 at 11:31 PM\n\n**Publisher:** [g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs](/u/g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs)\n\n**Tags:** [\\#tag1](/r/lou/blog:tags/tag1), [\\#tag3](/r/lou/blog:tags/tag3)\n\n\n❤️ 0\n\n---\n### Comments (2)\n\n\n**[@g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs](/r/lou/blog:commenters/g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs)** *just now*\n\ncomment4\n❤️ 0 \n\n\n\n**[@g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs](/r/lou/blog:commenters/g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs)** *just now*\n\ncomment2\n❤️ 0`\n\t\tassertMDEquals(t, got, expected)\n\t}\n\n\t{\n\t\tprintln(\"Listing of commenters...\")\n\t\tprintln(\"Listing invalid\")\n\t\tgot := Render(\"commenters\")\n\t\texpected := `Commenter slug is required.`\n\t\tassertMDEquals(t, got, expected)\n\n\t\tprintln(\"Invalid commenter\")\n\t\tgot = Render(\"commenters/invalid\")\n\t\texpected = `## [/r/lou/blog](/r/lou/blog)/commenters/invalid\n#### [@invalid](/u/invalid)'s profile\n[⊞ grid](/r/lou/blog:commenters/invalid?mode=grid) | [≔ list](/r/lou/blog:commenters/invalid?mode=list) | [⧖ relative](/r/lou/blog:commenters/invalid?time=short) | [↕ alphabetical \\(A\\-Z\\)](/r/lou/blog:commenters/invalid?order=asc\u0026sort=alpha) | [↕ recent](/r/lou/blog:commenters/invalid?order=asc\u0026sort=recent) | [↕ last updated](/r/lou/blog:commenters/invalid?order=asc\u0026sort=update) | [past year](/r/lou/blog:commenters/invalid?end=2009-02-13\u0026start=2008-02-13), [this year](/r/lou/blog:commenters/invalid?end=2009-02-13\u0026start=2009-01-01), [last 30 days](/r/lou/blog:commenters/invalid?end=2009-02-13\u0026start=2009-01-14) | [⟳ reset](/r/lou/blog)\n\nNo posts found for this commenter.\u003cgno-columns\u003e`\n\t\tassertMDEquals(t, got, expected)\n\n\t\tprintln(\"Valid commenter\")\n\t\tgot = Render(\"commenters/g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs\")\n\t\texpected = `## [/r/lou/blog](/r/lou/blog)/commenters/g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs\n#### [@g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs](/u/g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs)'s profile\n[⊞ grid](/r/lou/blog:commenters/g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs?mode=grid) | [≔ list](/r/lou/blog:commenters/g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs?mode=list) | [⧖ relative](/r/lou/blog:commenters/g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs?time=short) | [↕ alphabetical \\(A\\-Z\\)](/r/lou/blog:commenters/g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs?order=asc\u0026sort=alpha) | [↕ recent](/r/lou/blog:commenters/g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs?order=asc\u0026sort=recent) | [↕ last updated](/r/lou/blog:commenters/g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs?order=asc\u0026sort=update) | [past year](/r/lou/blog:commenters/g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs?end=2009-02-13\u0026start=2008-02-13), [this year](/r/lou/blog:commenters/g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs?end=2009-02-13\u0026start=2009-01-01), [last 30 days](/r/lou/blog:commenters/g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs?end=2009-02-13\u0026start=2009-01-14) | [⟳ reset](/r/lou/blog)\n\n\u003cgno-columns\u003e\n\u003cgno-columns\u003e\n#### **[@g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs](/r/lou/blog:commenters/g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs)**\n\n\ncomment1\n\n*just now*\n\nin [title1](/r/lou/blog:posts/slug1)\n\n---\n#### **[@g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs](/r/lou/blog:commenters/g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs)**\n\n\ncomment3\n\n*just now*\n\nin [title1](/r/lou/blog:posts/slug1)\n\n---\n#### **[@g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs](/r/lou/blog:commenters/g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs)**\n\n\ncomment5\n\n*just now*\n\nin [title1](/r/lou/blog:posts/slug1)\n\n---\n\u003c/gno-columns\u003e\n**1** | [2](?page=2)`\n\t\tassertMDEquals(t, got, expected)\n\t}\n\n\t{\n\t\tprintln(\"Invalid paths...\")\n\t\tnotFoundPaths := []string{\n\t\t\t\"p/slug3\",\n\t\t\t\"p\",\n\t\t\t\"p/\",\n\t\t\t\"x/x\",\n\t\t\t\"t\",\n\t\t\t\"t/\",\n\t\t\t\"/\",\n\t\t\t\"p/slug1/\",\n\t\t}\n\t\tfor _, notFoundPath := range notFoundPaths {\n\t\t\tgot := Render(notFoundPath)\n\t\t\texpected := \"404\"\n\t\t\tif got != expected {\n\t\t\t\tt.Errorf(\"path %q: expected %q, got %q.\", notFoundPath, expected, got)\n\t\t\t} else {\n\t\t\t\tprintln(\"PASSED\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// {\n\t// \tprintln(\"Updating a post...\")\n\t// \tnewTitle := \"title3\"\n\t// \tnewDate := \"2022-05-20T13:17:23Z\"\n\n\t// \tCreatePost(cross, \"slug1\", \"title1\", \"body1\", \"2022-05-20T13:17:22Z\", \"lou\", \"tag1 tag2\")\n\t// \tUpdatePostBySlug(cur, \"slug2\", newTitle, \"body2++\", newDate, \"lou\", \"tag1 tag4\")\n\t// \tgot := Render(\"posts/slug2\")\n\t// \texpected := ``\n\t// \tassertMDEquals(t, got, expected)\n\n\t// \thome := Render(\"\")\n\n\t// \tif strings.Count(home, newTitle) != 1 {\n\t// \t\tt.Errorf(\"post not edited properly\")\n\t// \t}\n\t// }\n\n\t// {\n\t// \t// fix\n\t// \tprintln(\"Deleting a post...\")\n\t// \ttitle := \"example title\"\n\t// \tslug := \"testSlug1\"\n\t// \tCreatePost(cross, slug, title, \"body1\", \"2022-05-25T13:17:22Z\", \"lou\", \"tag1,tag2\")\n\n\t// \tgot := Render(\"\")\n\n\t// \tif !strings.Contains(got, title) {\n\t// \t\tt.Errorf(\"post was not added properly\")\n\t// \t}\n\n\t// \tpostRender := Render(\"p/\" + slug)\n\n\t// \tif !strings.Contains(postRender, title) {\n\t// \t\tt.Errorf(\"post not rendered properly\")\n\t// \t}\n\n\t// \tDeletePost(cur, slug)\n\t// \tgot = Render(\"\")\n\n\t// \tif strings.Contains(got, title) {\n\t// \t\tt.Errorf(\"post was not removed\")\n\t// \t}\n\n\t// \tpostRender = Render(\"p/\" + slug)\n\n\t// \tassertMDEquals(t, postRender, \"404\")\n\t// }\n\t//\n\t//\t// TODO: ?mode=...\n\t//\n}\n\nfunc assertMDEquals(t *testing.T, got, expected string) {\n\tt.Helper()\n\texpected = strings.TrimSpace(expected)\n\tgot = strings.TrimSpace(got)\n\tif expected != got {\n\t\tt.Errorf(\"invalid render output.\\nexpected %q.\\ngot      %q.\", expected, got)\n\t} else {\n\t\tprintln(\"PASSED\")\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/lou/blog\"\ngno = \"0.9\"\n"},{"name":"sample.gno","body":"package blog\n\nimport (\n\t\"gno.land/p/lou/blog\"\n)\n\nfunc CreateSampleBlog(_ int, rlm realm) *blog.Blog {\n\tpost1, err := blog.NewPost(\n\t\t\"ai-future\",\n\t\t\"The Future of AI\",\n\t\t\"Artificial Intelligence is transforming every industry, from healthcare to entertainment.\",\n\t\t\"2024-03-01T10:00:00Z\",\n\t\t\"g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs\",\n\t\t[]string{\"g1abcde12345\"},\n\t\t[]string{\"ai\", \"technology\", \"future\"},\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tcomm, err := blog.NewComment(\n\t\t\"g1abcde12345\",\n\t\t\"Exciting times ahead!\",\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tpost1.AddComment(comm)\n\n\tpost2, err := blog.NewPost(\n\t\t\"blockchain-real-world\",\n\t\t\"Blockchain in the Real World\",\n\t\t\"Beyond cryptocurrencies, blockchain is finding uses in supply chains, voting systems, and more.\",\n\t\t\"2023-12-15T08:30:00Z\",\n\t\t\"g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs\",\n\t\t[]string{\"g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs\"},\n\t\t[]string{\"blockchain\", \"tech\", \"supplychain\"},\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treply, err := blog.NewComment(\n\t\t\"g1ffzxha57dh0qgv9ma5v393ur0zexfvp6lsjpae\",\n\t\t\"heyyy\",\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tcomm2, err := blog.NewComment(\n\t\t\"g14x2crt492f84shxa9s7e5ulk7lnf2p3euz7r9n\",\n\t\t\"This tech still needs better UX.\",\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tcomm3, err := blog.NewComment(\n\t\t\"g1fjnxhv3v5x02j49fyl49epmhjyzscm8e4au94t\",\n\t\t\"Absolutely, usability is key for adoption.\",\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tcomm4, err := blog.NewComment(\n\t\t\"g14x2crt492f84shxa9s7e5ulk7lnf2p3euz7r9n\",\n\t\t\"... still needs better UX.\",\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tpost2.AddComment(comm2)\n\tpost2.AddComment(comm3)\n\tpost2.AddReply(comm2.ID(), reply)\n\tpost2.AddReply(reply.ID(), comm4)\n\n\tpost3, err := blog.NewPost(\n\t\t\"getting-things-done\",\n\t\t\"Getting Things Done in 2025\",\n\t\t\"With distractions everywhere, productivity tools are more essential than ever.\",\n\t\t\"2025-01-10T09:15:00Z\",\n\t\t\"g1pfyhn0d7g4tnp6wft9ge4cuu88ppr9u8mdggfs\",\n\t\t[]string{\"g1produser1\"},\n\t\t[]string{\"productivity\", \"lifehacks\", \"focus\", \"ai\"},\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tmyBlog.AddPost(0, rlm, post1)\n\tmyBlog.AddPost(0, rlm, post2)\n\tmyBlog.AddPost(0, rlm, post3)\n\treturn myBlog\n}\n\nfunc Test(cur realm) {\n\tCreateSampleBlog(0, cur)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"hkzjlFIV7j/InRAAOWPhEOYZTGmzZ+MeSRL1T8NEfckoMipZ4Nk3M2xCYbcGd/H78AS4uyuuUWPOZBW7b7DDMQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"home","path":"gno.land/r/manfred/home","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/manfred/home\"\ngno = \"0.9\"\n"},{"name":"home.gno","body":"package home\n\nfunc Render(path string) string {\n\treturn \"Moved to r/moul\"\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"/71vHDHTWTxTG63Q7YyT9FKb+zBzXK3Nrg1Of1PVJKIX0MArHpwDeKYHRFl5v1IMviG3gND0QpDuosHSiCUb+A=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"home","path":"gno.land/r/mason/home","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/mason/home\"\ngno = \"0.9\"\n"},{"name":"home.gno","body":"package home\n\nimport (\n\t\"gno.land/p/mason/md/v0\"\n)\n\nconst (\n\tgnomeArt1 = `   /\\\n                  /  \\\n                  ,,,,,\n                  (o.o)\n                  (\\_/)\n                  -\"-\"-`\n\tdgnonut = `\n                        #$$$$$$$$*                         \n                     #$$@@@@@@@@@@$$$#                      \n                   #$$@@@@@@@@@@@@@$$$$#                    \n                 #$$$@@@@$$$$$$$$$$$$$$$$*                  \n                #$$$$$$$$$$$$$$$$$$$$$$$$$#!                \n               #$$$$$$$$$############$$$$$##*               \n             !##$$$$$$####**********#####$###*              \n            =##$$$$$###****!!!!!!!!!***######*!             \n            *##$$$###***!!!!!!==!!!!!!**######*=            \n           !*#######***!!!=;;;;;====!!!!**####**            \n          !*#######**!!!==;;::::::;;==!!!**###**!           \n          !*######***!==;::~~~~~~:::;;=!!!***#***=          \n         =**#####**!!==;::~-,,,,,--~:;;=!!!******!          \n         !**####***!==;:~-,..  ..,,-~:;==!!******!;         \n        ;!**###***!!=;:~-,.       ..-~:;==!!*****!=         \n        =!*******!!==::-.          .,-::==!!*****!=         \n        =!*******!!=;:~,            .-~:;=!!!****!=:        \n       ~=!*******!==;:-.            .,-:;=!!!****!=;        \n       :=!*******!==;~,.             ,-:;==!!!***!=;        \n       :=!******!!==:~,              ,-:;=!!!***!!=;        \n       :=!!*****!!=;:~,              ,~:;=!!****!!=;-       \n       :=!!!****!!==;~,              -~;==!!****!!=;-       \n       :;=!!*****!!=;:-              -:;=!!*****!!=:-       \n       ~;=!!!****!!==;~              :;=!!*****!!!;:-       \n       ~;==!!****!!!==:              ;=!!******!!=;:,       \n       ~:==!!!****!!!=;~            :=!********!!=;:.       \n       -:;==!!*****!!!!;            =!*********!==;:        \n       ,~;==!!*******!!==          =**#####****!==:~        \n       ,~:;=!!!!*********!        **#######***!!=;~-        \n        -~;;=!!!!**********!    *##$$$$$###***!!=:~.        \n        ,~:;==!!!****##########$$$$$$$$###****!=;:~         \n         -~:;==!!!***####$$$$$$@@@@@$$$###**!!=;:~,         \n         ,-~:;=!!!***####$$$$@@@@@@$$$$##**!!!=;:-.         \n          -~:;;=!!!***###$$$$$@@@@$$$$##***!!=;:-.          \n          .-~:;;=!!!***###$$$$$$$$$$$##***!==;:~-           \n           .-~:;==!!!!**####$$$$$$$###**!!==;:~-            \n            ,-~::;==!!!!***########****!!==;:~-.            \n             ,-~:;;==!!!!!***********!!!==;:~,.             \n              ,,~~::;====!!!!!!!!!!!!!==;::~,.              \n               .,-~::;;;===!!!!!!!!===;::~-,.               \n                 ,--~~:;;;;========;;::~--.                 \n                  .,,-~~:::::::::::~~~-,,.                  \n                    ..,---~~~~~~~~~--,..                    \n                       ..,,,,,,,,,...                       \n                             ...`\n)\n\nfunc Render(path string) string {\n\thome := md.New()\n\thome.H1(\"Mason's Realm\")\n\n\thome.Im(\"https://cdn.esawebb.org/archives/images/screen/weic2428a.jpg\", \"Placeholder\")\n\thome.P(\"Welcome to my realm. \" + md.Link(\"github\", \"https://github.com/masonmcbride\"))\n\n\thome.H3(\"Dgnonut\")\n\thome.Code(dgnonut)\n\n\thome.H3(\"More\")\n\thome.Code(gnomeArt1)\n\thome.Bullet(\"Credit to \" + md.Link(\"JJOptimist\", \"https://gno.land/r/jjoptimist/home\") + \" for this gnome art.\")\n\thome.Bullet(\"I'm testing out my markdown system.\")\n\treturn home.Render()\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"M36/WCFSUzJd6nJCXiAzVEqqc4J4Klys4BXdb8MBFN5oLClrNoTyFN/MJPGcAx4FgbZ3ojXBcqO9og7xIBZ65g=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"home","path":"gno.land/r/matijamarjanovic/home","files":[{"name":"config.gno","body":"package home\n\nimport (\n\t\"gno.land/p/moul/authz/v0\"\n)\n\nvar (\n\tmainAddr   = address(\"g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y\")\n\tbackupAddr = address(\"g1kjsl2ungmc95mgluq96w8dqlep8d4n8cxdfthk\")\n\tauth       = authz.NewMemberAuthority(mainAddr, backupAddr)\n\tAuth       = authz.NewWithAuthority(auth)\n)\n\nfunc Authorize(cur realm, addr string) {\n\tauth.AddMember(0, cur, address(addr))\n}\n\nfunc Address() address {\n\treturn mainAddr\n}\n\nfunc Backup() address {\n\treturn backupAddr\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/matijamarjanovic/home\"\ngno = \"0.9\"\n"},{"name":"home.gno","body":"package home\n\nimport (\n\t\"chain/banker\"\n\t\"chain/runtime/unsafe\"\n\t\"strings\"\n\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/moul/txlink/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/leon/hor\"\n)\n\nvar (\n\tpfp        string\n\tpfpCaption string\n\tabtMe      string\n\n\tmodernVotes  int64\n\tclassicVotes int64\n\tminimalVotes int64\n\tcurrentTheme string\n\n\tmodernLink  string\n\tclassicLink string\n\tminimalLink string\n)\n\nfunc init(cur realm) {\n\tpfp = \"https://avatars.githubusercontent.com/u/93043005?s=60\u0026v=4\"\n\tpfpCaption = \"My github profile picture\"\n\tabtMe = `Blockchain \u0026 Full Stack Developer and a Computer Science Student who enjoys hackathons and building cool stuff. My current passion is DeFi. Outside of work, I enjoy travelling, weightlifting and combat sports.`\n\n\tmodernVotes = 0\n\tclassicVotes = 0\n\tminimalVotes = 0\n\tcurrentTheme = \"classic\"\n\tmodernLink = txlink.NewLink(\"VoteModern\").URL()\n\tclassicLink = txlink.NewLink(\"VoteClassic\").URL()\n\tminimalLink = txlink.NewLink(\"VoteMinimal\").URL()\n\thor.Register(cross(cur), \"Matija Marijanovic's Home Realm\", \"\")\n}\n\nfunc UpdatePFP(cur realm, url, caption string) {\n\tif err := Auth.DoByPrevious(0, cur, \"update_pfp\", func() error {\n\t\tpfp = url\n\t\tpfpCaption = caption\n\t\treturn nil\n\t}); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc UpdateAboutMe(cur realm, col1 string) {\n\tif err := Auth.DoByPrevious(0, cur, \"update_about_me\", func() error {\n\t\tabtMe = col1\n\t\treturn nil\n\t}); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc VoteModern(cur realm) {\n\tif !cur.Previous().IsUserCall() {\n\t\tpanic(\"only user-call (maketx call) accepted\")\n\t}\n\tugnotAmount := unsafe.OriginSend().AmountOf(\"ugnot\")\n\tvotes := ugnotAmount\n\tmodernVotes += votes\n\tupdateCurrentTheme()\n}\n\nfunc VoteClassic(cur realm) {\n\tif !cur.Previous().IsUserCall() {\n\t\tpanic(\"only user-call (maketx call) accepted\")\n\t}\n\tugnotAmount := unsafe.OriginSend().AmountOf(\"ugnot\")\n\tvotes := ugnotAmount\n\tclassicVotes += votes\n\tupdateCurrentTheme()\n}\n\nfunc VoteMinimal(cur realm) {\n\tif !cur.Previous().IsUserCall() {\n\t\tpanic(\"only user-call (maketx call) accepted\")\n\t}\n\tugnotAmount := unsafe.OriginSend().AmountOf(\"ugnot\")\n\tvotes := ugnotAmount\n\tminimalVotes += votes\n\tupdateCurrentTheme()\n}\n\nfunc CollectBalance(cur realm) {\n\tif err := Auth.DoByPrevious(0, cur, \"collect_balance\", func() error {\n\t\tbanker_ := banker.NewBanker(banker.BankerTypeRealmSend, cur)\n\t\townerAddr := Address()\n\t\trealmAddr := cur.Address()\n\t\tbanker_.SendCoins(realmAddr, ownerAddr, banker_.GetCoins(realmAddr))\n\t\treturn nil\n\t}); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc Render(path string) string {\n\tvar sb strings.Builder\n\n\tswitch currentTheme {\n\tcase \"modern\":\n\t\t// Modern theme - Clean and minimalist with emojis\n\t\tsb.WriteString(md.H1(\"🚀 Matija's Space\"))\n\t\tsb.WriteString(md.Image(pfpCaption, pfp))\n\t\tsb.WriteString(\"\\n\")\n\t\tsb.WriteString(md.Italic(pfpCaption))\n\t\tsb.WriteString(\"\\n\")\n\t\tsb.WriteString(md.HorizontalRule())\n\t\tsb.WriteString(abtMe)\n\t\tsb.WriteString(\"\\n\")\n\n\tcase \"minimal\":\n\t\t// Minimal theme - No emojis, minimal formatting\n\t\tsb.WriteString(md.H1(\"Matija Marjanovic\"))\n\t\tsb.WriteString(\"\\n\")\n\t\tsb.WriteString(abtMe)\n\t\tsb.WriteString(\"\\n\")\n\t\tsb.WriteString(md.Image(pfpCaption, pfp))\n\t\tsb.WriteString(\"\\n\")\n\t\tsb.WriteString(pfpCaption)\n\t\tsb.WriteString(\"\\n\")\n\n\tdefault:\n\t\t// Classic theme - Traditional blog style with decorative elements\n\t\tsb.WriteString(md.H1(\"✨ Welcome to Matija's Homepage ✨\"))\n\t\tsb.WriteString(\"\\n\")\n\t\tsb.WriteString(md.Image(pfpCaption, pfp))\n\t\tsb.WriteString(\"\\n\")\n\t\tsb.WriteString(pfpCaption)\n\t\tsb.WriteString(\"\\n\")\n\t\tsb.WriteString(md.HorizontalRule())\n\t\tsb.WriteString(md.H2(\"About me\"))\n\t\tsb.WriteString(\"\\n\")\n\t\tsb.WriteString(abtMe)\n\t\tsb.WriteString(\"\\n\")\n\t}\n\n\tswitch currentTheme {\n\tcase \"modern\":\n\t\tsb.WriteString(md.HorizontalRule())\n\t\tsb.WriteString(md.H2(\"🎨 Theme Selector\"))\n\t\tsb.WriteString(\"Choose your preferred viewing experience:\\n\")\n\t\titems := []string{\n\t\t\tmd.Link(ufmt.Sprintf(\"Modern Design (%d votes)\", modernVotes), modernLink),\n\t\t\tmd.Link(ufmt.Sprintf(\"Classic Style (%d votes)\", classicVotes), classicLink),\n\t\t\tmd.Link(ufmt.Sprintf(\"Minimal Look (%d votes)\", minimalVotes), minimalLink),\n\t\t}\n\t\tsb.WriteString(md.BulletList(items))\n\n\tcase \"minimal\":\n\t\tsb.WriteString(\"\\n\")\n\t\tsb.WriteString(md.H3(\"Theme Selection\"))\n\t\tsb.WriteString(ufmt.Sprintf(\"Current theme: %s\\n\", currentTheme))\n\t\tsb.WriteString(ufmt.Sprintf(\"Votes - Modern: %d | Classic: %d | Minimal: %d\\n\",\n\t\t\tmodernVotes, classicVotes, minimalVotes))\n\t\tsb.WriteString(md.Link(\"Modern\", modernLink))\n\t\tsb.WriteString(\" | \")\n\t\tsb.WriteString(md.Link(\"Classic\", classicLink))\n\t\tsb.WriteString(\" | \")\n\t\tsb.WriteString(md.Link(\"Minimal\", minimalLink))\n\t\tsb.WriteString(\"\\n\")\n\n\tdefault:\n\t\tsb.WriteString(md.HorizontalRule())\n\t\tsb.WriteString(md.H2(\"✨ Theme Customization ✨\"))\n\t\tsb.WriteString(md.Bold(\"Choose Your Preferred Theme:\"))\n\t\tsb.WriteString(\"\\n\\n\")\n\t\titems := []string{\n\t\t\tufmt.Sprintf(\"Modern 🚀 (%d votes) - %s\", modernVotes, md.Link(\"Vote\", modernLink)),\n\t\t\tufmt.Sprintf(\"Classic ✨ (%d votes) - %s\", classicVotes, md.Link(\"Vote\", classicLink)),\n\t\t\tufmt.Sprintf(\"Minimal ⚡ (%d votes) - %s\", minimalVotes, md.Link(\"Vote\", minimalLink)),\n\t\t}\n\t\tsb.WriteString(md.BulletList(items))\n\t}\n\n\t// Theme-specific footer/links section\n\tswitch currentTheme {\n\tcase \"modern\":\n\t\tsb.WriteString(md.HorizontalRule())\n\t\tsb.WriteString(md.Link(\"GitHub\", \"https://github.com/matijamarjanovic\"))\n\t\tsb.WriteString(\" | \")\n\t\tsb.WriteString(md.Link(\"LinkedIn\", \"https://www.linkedin.com/in/matijamarjanovic\"))\n\t\tsb.WriteString(\"\\n\")\n\n\tcase \"minimal\":\n\t\tsb.WriteString(\"\\n\")\n\t\tsb.WriteString(md.Link(\"GitHub\", \"https://github.com/matijamarjanovic\"))\n\t\tsb.WriteString(\" | \")\n\t\tsb.WriteString(md.Link(\"LinkedIn\", \"https://www.linkedin.com/in/matijamarjanovic\"))\n\t\tsb.WriteString(\"\\n\")\n\n\tdefault:\n\t\tsb.WriteString(md.HorizontalRule())\n\t\tsb.WriteString(md.H3(\"✨ Connect With Me\"))\n\t\titems := []string{\n\t\t\tmd.Link(\"🌟 GitHub\", \"https://github.com/matijamarjanovic\"),\n\t\t\tmd.Link(\"💼 LinkedIn\", \"https://www.linkedin.com/in/matijamarjanovic\"),\n\t\t}\n\t\tsb.WriteString(md.BulletList(items))\n\t}\n\n\treturn sb.String()\n}\n\nfunc maxOfThree(a, b, c int64) int64 {\n\tmax := a\n\tif b \u003e max {\n\t\tmax = b\n\t}\n\tif c \u003e max {\n\t\tmax = c\n\t}\n\treturn max\n}\n\nfunc updateCurrentTheme() {\n\tmaxVotes := maxOfThree(modernVotes, classicVotes, minimalVotes)\n\n\tif maxVotes == modernVotes {\n\t\tcurrentTheme = \"modern\"\n\t} else if maxVotes == classicVotes {\n\t\tcurrentTheme = \"classic\"\n\t} else {\n\t\tcurrentTheme = \"minimal\"\n\t}\n}\n"},{"name":"home_test.gno","body":"package home\n\nimport (\n\t\"chain\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestUpdatePFP(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y\")))\n\tpfp = \"\"\n\tpfpCaption = \"\"\n\n\tUpdatePFP(cross(cur), \"https://example.com/pic.png\", \"New Caption\")\n\n\turequire.Equal(t, pfp, \"https://example.com/pic.png\", \"Profile picture URL should be updated\")\n\turequire.Equal(t, pfpCaption, \"New Caption\", \"Profile picture caption should be updated\")\n}\n\nfunc TestUpdateAboutMe(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y\")))\n\tabtMe = \"\"\n\n\tUpdateAboutMe(cross(cur), \"This is my new bio.\")\n\n\turequire.Equal(t, abtMe, \"This is my new bio.\", \"About Me should be updated\")\n}\n\nfunc TestVoteModern(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y\")))\n\tmodernVotes, classicVotes, minimalVotes = 0, 0, 0\n\n\tcoinsSent := chain.NewCoins(chain.NewCoin(\"ugnot\", 75000000))\n\tcoinsSpent := chain.NewCoins(chain.NewCoin(\"ugnot\", 1))\n\n\ttesting.SetOriginSend(coinsSent)\n\ttesting.SetOriginSpend(coinsSpent)\n\tVoteModern(cross(cur))\n\n\tuassert.Equal(t, int64(75000000), modernVotes, \"Modern votes should be calculated correctly\")\n\tuassert.Equal(t, \"modern\", currentTheme, \"Theme should be updated to modern\")\n}\n\nfunc TestVoteClassic(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y\")))\n\tmodernVotes, classicVotes, minimalVotes = 0, 0, 0\n\n\tcoinsSent := chain.NewCoins(chain.NewCoin(\"ugnot\", 75000000))\n\tcoinsSpent := chain.NewCoins(chain.NewCoin(\"ugnot\", 1))\n\n\ttesting.SetOriginSend(coinsSent)\n\ttesting.SetOriginSpend(coinsSpent)\n\tVoteClassic(cross(cur))\n\n\tuassert.Equal(t, int64(75000000), classicVotes, \"Classic votes should be calculated correctly\")\n\tuassert.Equal(t, \"classic\", currentTheme, \"Theme should be updated to classic\")\n}\n\nfunc TestVoteMinimal(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y\")))\n\tmodernVotes, classicVotes, minimalVotes = 0, 0, 0\n\n\tcoinsSent := chain.NewCoins(chain.NewCoin(\"ugnot\", 75000000))\n\tcoinsSpent := chain.NewCoins(chain.NewCoin(\"ugnot\", 1))\n\n\ttesting.SetOriginSend(coinsSent)\n\ttesting.SetOriginSpend(coinsSpent)\n\tVoteMinimal(cross(cur))\n\n\tuassert.Equal(t, int64(75000000), minimalVotes, \"Minimal votes should be calculated correctly\")\n\tuassert.Equal(t, \"minimal\", currentTheme, \"Theme should be updated to minimal\")\n}\n\nfunc TestRender(t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y\")))\n\tmodernVotes, classicVotes, minimalVotes = 0, 0, 0\n\tcurrentTheme = \"classic\"\n\tpfp = \"https://example.com/pic.png\"\n\tpfpCaption = \"Test Caption\"\n\tabtMe = \"Test About Me\"\n\n\tout := Render(\"\")\n\turequire.NotEqual(t, out, \"\", \"Render output should not be empty\")\n\n\tuassert.True(t, strings.Contains(out, \"✨ Welcome to Matija's Homepage ✨\"), \"Classic theme should have correct header\")\n\tuassert.True(t, strings.Contains(out, pfp), \"Should contain profile picture URL\")\n\tuassert.True(t, strings.Contains(out, pfpCaption), \"Should contain profile picture caption\")\n\tuassert.True(t, strings.Contains(out, \"About me\"), \"Should contain About me section\")\n\tuassert.True(t, strings.Contains(out, abtMe), \"Should contain about me content\")\n\tuassert.True(t, strings.Contains(out, \"Theme Customization\"), \"Should contain theme customization section\")\n\tuassert.True(t, strings.Contains(out, \"Connect With Me\"), \"Should contain connect section\")\n}\n\nfunc TestRenderModernTheme(t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y\")))\n\tmodernVotes, classicVotes, minimalVotes = 100, 0, 0\n\tcurrentTheme = \"modern\"\n\tupdateCurrentTheme()\n\n\tout := Render(\"\")\n\tuassert.True(t, strings.Contains(out, \"🚀 Matija's Space\"), \"Modern theme should have correct header\")\n}\n\nfunc TestRenderMinimalTheme(t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(address(\"g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y\")))\n\tmodernVotes, classicVotes, minimalVotes = 0, 0, 100\n\tcurrentTheme = \"minimal\"\n\tupdateCurrentTheme()\n\n\tout := Render(\"\")\n\tuassert.True(t, strings.Contains(out, \"Matija Marjanovic\"), \"Minimal theme should have correct header\")\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"Ei3/BS17/41jFEhnU7AnaARdBQNGNtObrBSPis57RhJ1CScogVyx7VGn9hlkF8vkR0H1S19m7QMAjUg34A+lMw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"tokenhub","path":"gno.land/r/matijamarjanovic/tokenhub","files":[{"name":"errors.gno","body":"package tokenhub\n\nimport (\n\t\"errors\"\n)\n\nvar (\n\tErrNFTAlreadyRegistered = errors.New(\"NFT already registered\")\n\tErrNFTNotFound          = errors.New(\"NFT not found\")\n\tErrMTAlreadyRegistered  = errors.New(\"multi-token already registered\")\n\tErrMTNotFound           = errors.New(\"multi-token not found\")\n\tErrMTInfoNotFound       = errors.New(\"multi-token info not found\")\n\tErrNFTtokIDNotExists    = errors.New(\"NFT token ID does not exists\")\n)\n"},{"name":"getters.gno","body":"package tokenhub\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/nt/grc1155/v0\"\n\t\"gno.land/p/nt/grc20/v0\"\n\t\"gno.land/p/nt/grc721/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/nt/grc20reg/v0\"\n\t\"gno.land/r/sys/users\"\n)\n\n// GetUserTokenBalances returns a string of all the grc20 tokens the user owns\nfunc GetUserTokenBalances(userNameOrAddress string) string {\n\treturn getTokenBalances(userNameOrAddress, false)\n}\n\n// GetUserTokenBalancesNonZero returns a string of all the grc20 tokens the user owns, but only the ones that have a balance greater than 0\nfunc GetUserTokenBalancesNonZero(userNameOrAddress string) string {\n\treturn getTokenBalances(userNameOrAddress, true)\n}\n\n// GetUserNFTBalances returns a string of all the NFTs the user owns\nfunc GetUserNFTBalances(userNameOrAddress string) string {\n\treturn getNFTBalances(userNameOrAddress)\n}\n\n// GetUserMultiTokenBalances returns a string of all the multi-tokens the user owns\nfunc GetUserMultiTokenBalances(userNameOrAddress string) string {\n\treturn getMultiTokenBalances(userNameOrAddress, false)\n}\n\n// GetUserMultiTokenBalancesNonZero returns a string of all the multi-tokens the user owns, but only the ones that have a balance greater than 0\nfunc GetUserMultiTokenBalancesNonZero(userNameOrAddress string) string {\n\treturn getMultiTokenBalances(userNameOrAddress, true)\n}\n\n// GetToken returns a token instance for a given key\nfunc GetToken(key string) *grc20.Token {\n\treturn grc20reg.Get(key)\n}\n\n// MustGetToken returns a token instance for a given key, panics if the token is not found\nfunc MustGetToken(key string) *grc20.Token {\n\treturn grc20reg.MustGet(key)\n}\n\n// GetNFT returns a reader-only view of an NFT for a given key.\n//\n// SECURITY: the returned IGRC721Reader is data-untrusted. A malicious\n// NFTGetter registered via RegisterNFT can return a Reader that lies\n// about OwnerOf / BalanceOf / etc. Treat aggregator results as display\n// data, not authority data — never gate a transfer or mint on these\n// values. Writes are concrete methods on the owning realm's *basicNFT;\n// to mutate an NFT, call the owning realm's wrapper directly.\nfunc GetNFT(key string) *grc721.Token {\n\tnftGetter := registeredNFTs.Get(key)\n\tif nftGetter == nil {\n\t\treturn nil\n\t}\n\treturn nftGetter.(*grc721.Token)\n}\n\n// MustGetNFT returns a reader-only view of an NFT for a given key,\n// panics if the NFT is not found. See GetNFT for data-trust notes.\nfunc MustGetNFT(key string) *grc721.Token {\n\tnftGetter := GetNFT(key)\n\tif nftGetter == nil {\n\t\tpanic(\"unknown NFT: \" + key)\n\t}\n\treturn nftGetter\n}\n\n// GetMultiToken returns a multi-token instance for a given key\nfunc GetMultiToken(key string) grc1155.IGRC1155 {\n\tinfo := registeredMTs.Get(key)\n\tif info == nil {\n\t\treturn nil\n\t}\n\tmt := info.(GRC1155TokenInfo).Collection\n\treturn mt()\n}\n\n// MustGetMultiToken returns a multi-token instance for a given key, panics if the multi-token is not found\nfunc MustGetMultiToken(key string) grc1155.IGRC1155 {\n\tinfo := GetMultiToken(key)\n\tif info == nil {\n\t\tpanic(\"unknown multi-token: \" + key)\n\t}\n\treturn info\n}\n\n// GetAllNFTs returns a string of all the NFTs registered\nfunc GetAllNFTs() string {\n\tvar out string\n\tregisteredNFTs.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tout += ufmt.Sprintf(\"NFT:%s,\", key)\n\t\treturn false\n\t})\n\treturn out\n}\n\n// GetAllTokens returns a string of all the tokens registered\nfunc GetAllTokens() string {\n\tvar out string\n\tgrc20reg.GetRegistry().Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tout += \"Token:\" + key + \",\"\n\t\treturn false\n\t})\n\treturn out\n}\n\n// GetAllTokenWithDetails returns a string of all the tokens registered with their details\nfunc GetAllTokenWithDetails() string {\n\tvar out string\n\tgrc20reg.GetRegistry().Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\ttoken := value.(*grc20.Token)\n\t\tout += ufmt.Sprintf(\"Token:%s,Name:%s,Symbol:%s,Decimals:%d;\", key, token.GetName(), token.GetSymbol(), token.GetDecimals())\n\t\treturn false\n\t})\n\treturn out\n}\n\n// GetAllMultiTokens returns a string of all the multi-tokens registered\nfunc GetAllMultiTokens() string {\n\tvar out string\n\tregisteredMTs.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tout += \"MultiToken:\" + key + \",\"\n\t\treturn false\n\t})\n\treturn out\n}\n\n// GetAllRegistered returns a string of all the registered tokens, NFTs and multi-tokens\nfunc GetAllRegistered() string {\n\treturn GetAllNFTs() + GetAllTokens() + GetAllMultiTokens()\n}\n\n// getNFTBalances returns a string of all the NFTs the user owns\nfunc getNFTBalances(input string) string {\n\taddr := getAddressForUsername(input)\n\tif !addr.IsValid() {\n\t\tpanic(\"invalid address or username: \" + input)\n\t}\n\tvar out string\n\n\tregisteredNFTs.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tnft := value.(*grc721.Token)\n\t\tkey_parts := strings.Split(key, \".\")\n\t\towner, err := nft.OwnerOf(grc721.TokenID(key_parts[len(key_parts)-1]))\n\t\tif err == nil \u0026\u0026 addr == owner {\n\t\t\tout += \"NFT:\" + key + \",\"\n\t\t}\n\t\treturn false\n\t})\n\n\treturn out\n}\n\n// getTokenBalances returns a string of all the tokens the user owns\nfunc getTokenBalances(input string, nonZero bool) string {\n\n\taddr := getAddressForUsername(input)\n\tif !addr.IsValid() {\n\t\tpanic(\"invalid address or username: \" + input)\n\t}\n\tvar out string\n\tgrc20reg.GetRegistry().Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\n\t\ttoken := value.(*grc20.Token)\n\t\tbalance := token.BalanceOf(addr)\n\t\tif !nonZero || balance \u003e 0 {\n\t\t\tout += ufmt.Sprintf(\"Token:%s:%d,\", key, balance)\n\t\t}\n\t\treturn false\n\t})\n\n\treturn out\n}\n\n// getMultiTokenBalances returns a string of all the multi-tokens the user owns\nfunc getMultiTokenBalances(input string, nonZero bool) string {\n\taddr := getAddressForUsername(input)\n\tif !addr.IsValid() {\n\t\tpanic(\"invalid address or username: \" + input)\n\t}\n\tvar out string\n\n\tregisteredMTs.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tinfo := value.(GRC1155TokenInfo)\n\t\tmt := info.Collection()\n\t\tbalance, err := mt.BalanceOf(addr, grc1155.TokenID(info.TokenID))\n\t\tif err == nil {\n\t\t\tif !nonZero || balance \u003e 0 {\n\t\t\t\tout += ufmt.Sprintf(\"MultiToken:%s:%d,\", key, balance)\n\t\t\t}\n\t\t}\n\t\treturn false\n\t})\n\n\treturn out\n}\n\n// getAddressForUsername returns an address for a given username or address\nfunc getAddressForUsername(addrOrName string) address {\n\taddr := address(addrOrName)\n\tif addr.IsValid() {\n\t\treturn addr\n\t}\n\n\tif userData, _ := users.ResolveName(addrOrName); userData != nil {\n\t\treturn userData.Addr()\n\t}\n\n\treturn \"\"\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/matijamarjanovic/tokenhub\"\ngno = \"0.9\"\n"},{"name":"render.gno","body":"package tokenhub\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/nt/avl/pager/v0\"\n\t\"gno.land/p/nt/fqname/v0\"\n\t\"gno.land/p/nt/grc20/v0\"\n\t\"gno.land/p/nt/grc721/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/nt/grc20reg/v0\"\n)\n\nconst (\n\ttoken = \"token\" // grc20\n\tnft   = \"nft\"   // grc721\n\tmt    = \"mt\"    // grc1155\n)\n\nfunc Render(path string) string {\n\tvar out string\n\n\tswitch {\n\tcase path == \"\":\n\t\tout = renderHome()\n\n\tcase strings.HasPrefix(path, token):\n\t\tout = renderToken(path)\n\n\tcase strings.HasPrefix(path, nft):\n\t\tout = renderNFT(path)\n\n\tcase strings.HasPrefix(path, mt):\n\t\tout = renderMT(path)\n\t}\n\n\treturn out\n}\n\nfunc renderHome() string {\n\tout := md.H1(\"Token Hub\")\n\tout += md.Paragraph(\"Token Hub provides listings of all existing token types on Gno.land - GRC20 tokens, GRC721 NFTs, and GRC1155 multi-tokens. You can browse these listings to find available tokens, check balances, and access token metadata. If you're developing wallets or interfaces, you can query this registry to display token information to your users.\")\n\n\tlinks := []string{\n\t\t\"[GRC20 Tokens](/r/matijamarjanovic/tokenhub:tokens)\",\n\t\t\"[GRC721 NFTs](/r/matijamarjanovic/tokenhub:nfts)\",\n\t\t\"[GRC1155 Multi-Tokens](/r/matijamarjanovic/tokenhub:mts)\",\n\t}\n\tout += md.BulletList(links)\n\n\tout += md.H2(\"How to Register Your Tokens\")\n\tout += md.Paragraph(\"You can register your tokens with the following import and function calls:\")\n\n\tregisterCode := `// Import packages\nimport (\n\t\"gno.land/r/matijamarjanovic/tokenhub\"\n\t\"gno.land/p/nt/grc20/v0\"\n\t\"gno.land/p/nt/grc721/v0\"\n\t\"gno.land/p/nt/grc1155/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n)\n\nvar nextTokenID seqid.ID\n\n// grc20.NewToken and grc721.NewToken must be constructed from a realm\n// context (init or a crossing function) because they assert cur.IsCurrent().\nfunc init(cur realm) {\n\t// GRC20 token\n\tmyToken, myLedger := grc20.NewToken(\"My Token\", \"MTK\", 6, nextTokenID.Next(), cur)\n\tmyTokenPath := tokenhub.RegisterToken(myToken, \"my_token\")\n\n\t// GRC721 NFT\n\tmyNFT, myNFTLedger := grc721.NewToken(\"My NFT Collection\", \"MNFT\", 0, cur)\n\tmyNFTLedger.Mint(\"g1your_address_here\", \"1\")\n\tnftErr := tokenhub.RegisterNFT(myNFT, \"my_collection\", \"1\")\n\n\t// GRC1155 multi-token\n\tmyMultiToken := grc1155.NewBasicGRC1155Token(\"https://metadata.example.com/\")\n\tmyMultiToken.SafeMint(\"g1your_address_here\", \"123\", 10)\n\tmultiErr := tokenhub.RegisterMultiToken(myMultiToken.Getter(), \"123\")\n}`\n\n\tout += md.LanguageCodeBlock(\"go\", registerCode)\n\n\tout += \"\\n\"\n\tout += md.H2(\"Querying Token Information\")\n\tout += md.Paragraph(\"You can query token information and balances using functions like:\")\n\n\tqueryCode := `// Get all registered tokens\nallTokens := tokenhub.GetAllTokens()\n\n// Get token balances for a user\nbalances := tokenhub.GetUserTokenBalances(\"g1...\")\n\n// Get non-zero token balances\nnonZeroBalances := tokenhub.GetUserTokenBalancesNonZero(\"g1...\")`\n\n\tout += md.LanguageCodeBlock(\"go\", queryCode)\n\n\treturn out\n}\n\nfunc renderToken(path string) string {\n\tout := md.H1(\"GRC20 Tokens\")\n\tout += md.Paragraph(\"Below is a list of all registered GRC20 tokens and their registry keys (keys are used to query token information).\")\n\n\tvar tokenItems []string\n\n\ttokenPager := pager.NewPager(grc20reg.GetRegistry(), pageSize, false)\n\tpage := tokenPager.MustGetPageByPath(path)\n\n\tfor _, item := range page.Items {\n\t\ttoken := item.Value.(*grc20.Token)\n\t\tpkgPath, _ := fqname.Parse(item.Key)\n\t\tlinkURL := formatLinkURL(pkgPath, 0)\n\n\t\ttokenItems = append(tokenItems, ufmt.Sprintf(\"%s (%s) - %s\",\n\t\t\tmd.Link(token.GetName(), linkURL),\n\t\t\tmd.InlineCode(token.GetSymbol()),\n\t\t\tmd.InlineCode(item.Key)))\n\t}\n\n\tout += renderItemsList(tokenItems, page, \"No tokens registered yet\")\n\treturn out\n}\n\nfunc renderNFT(path string) string {\n\tout := md.H1(\"GRC721 NFTs\")\n\tout += md.Paragraph(\"Below is a list of all registered GRC721 NFT collections and their registry keys (keys are used to query token information).\")\n\n\tvar nftItems []string\n\tnftPager := pager.NewPager(registeredNFTs, pageSize, false)\n\tpage := nftPager.MustGetPageByPath(path)\n\n\tfor _, item := range page.Items {\n\t\tnft := item.Value.(*grc721.Token)\n\n\t\tpkgPath, _ := fqname.Parse(item.Key)\n\t\tlinkURL := formatLinkURL(pkgPath, 2)\n\n\t\tnftItems = append(nftItems, ufmt.Sprintf(\"%s (%s) - %s\",\n\t\t\tmd.Link(nft.GetName(), linkURL),\n\t\t\tmd.InlineCode(nft.GetSymbol()),\n\t\t\tmd.InlineCode(item.Key)))\n\t}\n\n\tout += renderItemsList(nftItems, page, \"No NFTs registered yet\")\n\treturn out\n}\n\nfunc renderMT(path string) string {\n\tout := md.H1(\"GRC1155 Multi-Tokens\")\n\tout += md.Paragraph(\"Below is a list of all registered GRC1155 multi-tokens and their registry keys (keys are used to query token information).\")\n\n\tvar mtItems []string\n\n\tmtPager := pager.NewPager(registeredMTs, pageSize, false)\n\tpage := mtPager.MustGetPageByPath(path)\n\n\tfor _, item := range page.Items {\n\t\tinfo := item.Value.(GRC1155TokenInfo)\n\t\tpkgPath, _ := fqname.Parse(item.Key)\n\t\tlinkURL := formatLinkURL(pkgPath, 1)\n\n\t\tmtItems = append(mtItems, ufmt.Sprintf(\"%s %s - %s\",\n\t\t\tmd.Bold(\"TokenID:\"),\n\t\t\tmd.Link(info.TokenID, linkURL),\n\t\t\tmd.InlineCode(item.Key)))\n\t}\n\n\tout += renderItemsList(mtItems, page, \"No multi-tokens registered yet\")\n\treturn out\n}\n\nfunc renderItemsList(items []string, page *pager.Page, emptyMessage string) string {\n\tvar out string\n\tif len(items) == 0 {\n\t\tout += md.Italic(emptyMessage)\n\t\tout += \"\\n\"\n\t\treturn out\n\t}\n\n\tout += md.BulletList(items)\n\tout += \"\\n\"\n\tout += md.HorizontalRule()\n\n\tpicker := page.Picker(page.Pager.PageQueryParam)\n\tif picker != \"\" {\n\t\tout += md.Paragraph(picker)\n\t}\n\n\treturn out\n}\n\nfunc formatLinkURL(pkgPath string, trailingSegmentsToRemove int) string {\n\tre1 := regexp.MustCompile(`gno\\.land/r/matijamarjanovic/tokenhub\\.`)\n\tpkgPath = re1.ReplaceAllString(pkgPath, \"\")\n\n\tre2 := regexp.MustCompile(`gno\\.land`)\n\turl := re2.ReplaceAllString(pkgPath, \"\")\n\n\tif trailingSegmentsToRemove \u003e 0 {\n\t\tre3 := regexp.MustCompile(`\\.`)\n\t\tparts := re3.Split(url, -1)\n\t\tif len(parts) \u003e trailingSegmentsToRemove {\n\t\t\turl = strings.Join(parts[:len(parts)-trailingSegmentsToRemove], \".\")\n\t\t}\n\t}\n\n\treturn url\n}\n"},{"name":"tokenhub.gno","body":"package tokenhub\n\nimport (\n\t\"gno.land/p/nt/grc20/v0\"\n\t\"gno.land/r/nt/grc20reg/v0\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/grc1155/v0\"\n\t\"gno.land/p/nt/grc721/v0\"\n\n\t\"gno.land/r/leon/hor\"\n)\n\ntype GRC1155TokenInfo struct {\n\tCollection grc1155.MultiTokenGetter\n\tTokenID    string\n}\n\nvar (\n\tregisteredTokens = avl.NewTree() // rlmPath[.slug] -\u003e *grc20.Token\n\tregisteredNFTs   = avl.NewTree() // rlmPath[.slug] -\u003e *grc721.Token\n\tregisteredMTs    = avl.NewTree() // rlmPath[.slug] -\u003e GRC1155TokenInfo\n)\n\nconst pageSize = 10\n\nfunc init(cur realm) {\n\thor.Register(cross(cur), \"Token Hub\", \"Registry for tokens and NFTs on gno.land\")\n}\n\n// RegisterToken registers a token in grc20reg.\n// Returns the registry key (rlmPath.symbol) that can be used to retrieve the\n// token; this is grc20reg's canonical key, not the token's full ID (which\n// carries a trailing sequence id).\nfunc RegisterToken(cur realm, token *grc20.Token, slug string) string {\n\treturn grc20reg.Register(cross(cur), token, slug)\n}\n\n// RegisterNFT stores the concrete *grc721.Token (not an interface getter), so\n// an entry cannot be a lying reader implementation.\nfunc RegisterNFT(cur realm, token *grc721.Token, collection string, tokenId string) error {\n\tif token == nil {\n\t\treturn ErrNFTNotFound\n\t}\n\n\tnftOwner, err := token.OwnerOf(grc721.TokenID(tokenId))\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !nftOwner.IsValid() {\n\t\treturn ErrNFTtokIDNotExists\n\t}\n\n\trlmPath := cur.Previous().PkgPath()\n\tkey := rlmPath + \".\" + collection + \".\" + tokenId\n\n\tif registeredNFTs.Has(key) {\n\t\treturn ErrNFTAlreadyRegistered\n\t}\n\n\tregisteredNFTs.Set(key, token)\n\treturn nil\n}\n\n// RegisterMultiToken is a function that registers a multi-token in an avl.Tree\n// The avl.Tree value is a struct defined in this realm. It contains not only the getter (like other token types) but also the tokenID\nfunc RegisterMultiToken(cur realm, mtGetter grc1155.MultiTokenGetter, tokenID string) error {\n\trlmPath := cur.Previous().PkgPath()\n\tkey := rlmPath + \".\" + tokenID\n\n\tif registeredMTs.Has(key) {\n\t\treturn ErrMTAlreadyRegistered\n\t}\n\n\tregisteredMTs.Set(key, GRC1155TokenInfo{\n\t\tCollection: mtGetter,\n\t\tTokenID:    tokenID,\n\t})\n\treturn nil\n}\n"},{"name":"tokenhub_test.gno","body":"package tokenhub\n\nimport (\n\t\"chain/runtime/unsafe\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/grc1155/v0\"\n\t\"gno.land/p/nt/grc20/v0\"\n\t\"gno.land/p/nt/grc721/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nconst testRealmPkgPath = \"gno.land/r/matijamarjanovic/testrealm\"\n\nvar nextGRC20ID seqid.ID\n\n// Same-realm cross so NewToken's IsCurrent + non-empty PkgPath checks pass.\nfunc newTestNFT(name, symbol string, rlm realm) (tok *grc721.Token, led *grc721.PrivateLedger) {\n\tfunc(cur realm) {\n\t\ttok, led = grc721.NewToken(name, symbol, 0, cur)\n\t}(cross(rlm))\n\treturn\n}\n\nfunc TestTokenRegistration(cur realm, t *testing.T) {\n\tresetState(t)\n\n\ttoken, _ := grc20.NewToken(\"Test Token\", \"TEST\", 6, nextGRC20ID.Next(), cur)\n\tkey := RegisterToken(cross(cur), token, \"test_token\")\n\n\tuassert.True(t, strings.HasPrefix(token.ID(), key+\".\"), \"registry key should be the token's rlmPath.symbol prefix\")\n\n\tretrievedToken := GetToken(key)\n\n\turequire.True(t, retrievedToken != nil, \"Should retrieve registered token\")\n\tuassert.Equal(t, \"Test Token\", retrievedToken.GetName(), \"Token name should match\")\n\tuassert.Equal(t, \"TEST\", retrievedToken.GetSymbol(), \"Token symbol should match\")\n}\n\nfunc TestNFTRegistration(cur realm, t *testing.T) {\n\tresetState(t)\n\ttesting.SetRealm(testing.NewCodeRealm(testRealmPkgPath))\n\n\tnft, nftLedger := newTestNFT(\"Test NFT\", \"TNFT\", cur)\n\tnftLedger.Mint(unsafe.CurrentRealm().Address(), grc721.TokenID(\"1\"))\n\terr := RegisterNFT(cross(cur), nft, \"test_nft\", \"1\")\n\n\turequire.NoError(t, err, \"Should register NFT without error\")\n\n\tretrievedNFT := GetNFT(testRealmPkgPath + \".test_nft.1\")\n\n\turequire.True(t, retrievedNFT != nil, \"Should retrieve registered NFT\")\n\n\tuassert.Equal(t, \"Test NFT\", retrievedNFT.GetName(), \"NFT name should match\")\n\tuassert.Equal(t, \"TNFT\", retrievedNFT.GetSymbol(), \"NFT symbol should match\")\n}\n\nfunc TestGRC1155Registration(cur realm, t *testing.T) {\n\tresetState(t)\n\ttesting.SetRealm(testing.NewCodeRealm(testRealmPkgPath))\n\n\tmt := grc1155.NewBasicGRC1155Token(\"test-uri\")\n\terr := RegisterMultiToken(cross(cur), mt.Getter(), \"1\")\n\turequire.NoError(t, err, \"Should register multi-token without error\")\n\n\tmultiToken := GetMultiToken(testRealmPkgPath + \".1\")\n\turequire.True(t, multiToken != nil, \"Should retrieve multi-token\")\n\t_, ok := multiToken.(grc1155.IGRC1155)\n\turequire.True(t, ok, \"Retrieved multi-token should implement IGRC1155\")\n\n\terr = RegisterMultiToken(cross(cur), mt.Getter(), \"1\")\n\tuassert.True(t, err != nil, \"Should not allow duplicate registration\")\n}\n\nfunc TestBalanceRetrieval(cur realm, t *testing.T) {\n\tresetState(t)\n\n\ttoken, ledger := grc20.NewToken(\"Balance Token\", \"BAL\", 6, nextGRC20ID.Next(), cur)\n\tkey := RegisterToken(cross(cur), token, \"balance_token\")\n\tholder := unsafe.CurrentRealm().Address()\n\tledger.Mint(holder, 1000)\n\n\tbalances := GetUserTokenBalances(holder.String())\n\tuassert.True(t, strings.Contains(balances,\n\t\t\"Token:\"+key+\":1000\"), \"Should show correct GRC20 balance\")\n\n\ttesting.SetRealm(testing.NewCodeRealm(testRealmPkgPath))\n\n\tnft, nftLedger := newTestNFT(\"Test NFT\", \"TNFT\", cur)\n\tnftLedger.Mint(unsafe.CurrentRealm().Address(), grc721.TokenID(\"1\"))\n\tuassert.NoError(t, RegisterNFT(cross(cur), nft, \"test_nft\", \"1\"))\n\n\tbalances = GetUserNFTBalances(unsafe.CurrentRealm().Address().String())\n\tuassert.True(t, strings.Contains(balances, \"NFT:\"+testRealmPkgPath+\".test_nft.1\"), \"Should show correct NFT balance\")\n\n\tmt := grc1155.NewBasicGRC1155Token(\"test-uri\")\n\terr := RegisterMultiToken(cross(cur), mt.Getter(), \"1\")\n\turequire.NoError(t, err, \"Should register multi-token without error\")\n\n\tbalances = GetUserMultiTokenBalances(unsafe.CurrentRealm().Address().String())\n\tuassert.True(t, strings.Contains(balances, \"MultiToken:\"+testRealmPkgPath+\".1:0\"), \"Should show multi-token balance\")\n\n\tnonZeroBalances := GetUserTokenBalancesNonZero(holder.String())\n\tuassert.True(t, strings.Contains(nonZeroBalances,\n\t\t\"Token:\"+key+\":1000\"), \"Should show non-zero GRC20 balance\")\n}\n\nfunc TestErrorCases(cur realm, t *testing.T) {\n\tresetState(t)\n\ttesting.SetRealm(testing.NewCodeRealm(testRealmPkgPath))\n\n\tnft, nftLedger := newTestNFT(\"Test NFT\", \"TNFT\", cur)\n\tnftLedger.Mint(unsafe.CurrentRealm().Address(), grc721.TokenID(\"1\"))\n\terr := RegisterNFT(cross(cur), nft, \"test_nft\", \"1\")\n\tuassert.NoError(t, err, \"First registration should succeed\")\n\n\terr = RegisterNFT(cross(cur), nft, \"test_nft\", \"1\")\n\tuassert.True(t, err != nil, \"Should not allow duplicate registration\")\n\n\tmt := grc1155.NewBasicGRC1155Token(\"test-uri\")\n\terr = RegisterMultiToken(cross(cur), mt.Getter(), \"1\")\n\tuassert.NoError(t, err, \"First multi-token registration should succeed\")\n\n\terr = RegisterMultiToken(cross(cur), mt.Getter(), \"1\")\n\tuassert.True(t, err != nil, \"Should not allow duplicate registration\")\n}\n\nfunc TestTokenListingFunctions(cur realm, t *testing.T) {\n\tresetState(t)\n\n\tgrc20Token, _ := grc20.NewToken(\"Listing Token\", \"LST\", 6, nextGRC20ID.Next(), cur)\n\tgrc20Key := RegisterToken(cross(cur), grc20Token, \"listing_token\")\n\n\tgrc20List := GetAllTokens()\n\tuassert.True(t, strings.Contains(grc20List, \"Token:\"+grc20Key),\n\t\t\"GetAllGRC20Tokens should list registered token\")\n\n\ttesting.SetRealm(testing.NewCodeRealm(testRealmPkgPath))\n\n\tnftToken, nftLedger := newTestNFT(\"Listing NFT\", \"LNFT\", cur)\n\tnftLedger.Mint(address(\"g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y\"), grc721.TokenID(\"1\"))\n\tRegisterNFT(cross(cur), nftToken, \"listing_nft\", \"1\")\n\n\tnftList := GetAllNFTs()\n\tuassert.True(t, strings.Contains(nftList, \"NFT:\"+testRealmPkgPath+\".listing_nft.1\"),\n\t\t\"GetAllNFTs should list registered NFT\")\n\n\tmultiToken := grc1155.NewBasicGRC1155Token(\"test-uri\")\n\tRegisterMultiToken(cross(cur), multiToken.Getter(), \"listing_mt\")\n\n\tgrc1155List := GetAllMultiTokens()\n\tuassert.True(t, strings.Contains(grc1155List, \"MultiToken:\"+testRealmPkgPath+\".listing_mt\"),\n\t\t\"GetAllMultiTokens should list registered multi-token\")\n\n\tcompleteList := GetAllRegistered()\n\tuassert.True(t, strings.Contains(completeList, \"NFT:\"+testRealmPkgPath+\".listing_nft.1\"),\n\t\t\"GetAllTokens should list NFTs\")\n\tuassert.True(t, strings.Contains(completeList, \"Token:\"+grc20Key),\n\t\t\"GetAllTokens should list GRC20 tokens\")\n\tuassert.True(t, strings.Contains(completeList, \"MultiToken:\"+testRealmPkgPath+\".listing_mt\"),\n\t\t\"GetAllTokens should list multi-tokens\")\n}\n\nfunc TestMustGetFunctions(cur realm, t *testing.T) {\n\tresetState(t)\n\n\ttoken, _ := grc20.NewToken(\"Must Token\", \"MUST\", 6, nextGRC20ID.Next(), cur)\n\tkey := RegisterToken(cross(cur), token, \"must_token\")\n\n\tretrievedToken := MustGetToken(key)\n\tuassert.Equal(t, \"Must Token\", retrievedToken.GetName(), \"Token name should match\")\n\n\t// MustGetToken dispatches into grc20reg across a borrow; that abort is not\n\t// catchable by defer/recover, so capture it with revive().\n\tr := revive(func() { MustGetToken(\"non_existent_token\") })\n\tuassert.True(t, r != nil, \"MustGetToken should panic for non-existent token\")\n\tuassert.True(t, strings.Contains(r.(string), \"unknown token\"), \"Panic message should mention unknown token\")\n\n\ttesting.SetRealm(testing.NewCodeRealm(testRealmPkgPath))\n\n\tnft, nftLedger := newTestNFT(\"Must NFT\", \"MNFT\", cur)\n\tnftLedger.Mint(unsafe.CurrentRealm().Address(), grc721.TokenID(\"1\"))\n\tRegisterNFT(cross(cur), nft, \"must_nft\", \"1\")\n\n\tretrievedNFT := MustGetNFT(testRealmPkgPath + \".must_nft.1\")\n\tuassert.Equal(t, \"Must NFT\", retrievedNFT.GetName(), \"NFT name should match\")\n\n\t// MustGetNFT's panic is local (no borrow crossed), so defer/recover catches it.\n\tfunc() {\n\t\tdefer func() {\n\t\t\tr := recover()\n\t\t\tuassert.True(t, r != nil, \"MustGetNFT should panic for non-existent NFT\")\n\t\t\tuassert.True(t, strings.Contains(r.(string), \"unknown NFT\"), \"Panic message should mention unknown NFT\")\n\t\t}()\n\t\tMustGetNFT(\"non_existent_nft\")\n\t}()\n\n\tmt := grc1155.NewBasicGRC1155Token(\"must-uri\")\n\tRegisterMultiToken(cross(cur), mt.Getter(), \"must_mt\")\n\n\tretrievedMT := MustGetMultiToken(testRealmPkgPath + \".must_mt\")\n\t_, ok := retrievedMT.(grc1155.IGRC1155)\n\turequire.True(t, ok, \"Retrieved multi-token should implement IGRC1155\")\n\n\t// Local panic, same as MustGetNFT above.\n\tfunc() {\n\t\tdefer func() {\n\t\t\tr := recover()\n\t\t\tuassert.True(t, r != nil, \"MustGetMultiToken should panic for non-existent multi-token\")\n\t\t\tuassert.True(t, strings.Contains(r.(string), \"unknown multi-token\"), \"Panic message should mention unknown multi-token\")\n\t\t}()\n\t\tMustGetMultiToken(\"non_existent_mt\")\n\t}()\n}\n\nfunc resetState(t *testing.T) {\n\tt.Helper()\n\n\tregisteredTokens = avl.NewTree()\n\tregisteredNFTs = avl.NewTree()\n\tregisteredMTs = avl.NewTree()\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"aDd+NQp4JrG7SRl3AsJw3Q1ZSe25YuESCJcOdp77YAZlCk6jLe3//Y5RHoSbekdAyLICBtCUivlhjy4KbetsyA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"calculator","path":"gno.land/r/miko/calculator","files":[{"name":"calculator.gno","body":"package calculator\n\nimport (\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/moul/mdtable/v0\"\n\t\"gno.land/p/moul/realmpath/v0\"\n\t\"gno.land/r/leon/hor\"\n)\n\ntype Node struct {\n\tvalue string // Value of the current node\n\tleft  *Node\n\tright *Node\n}\n\nconst (\n\tspecialCharacters             = \"+-*/.\"\n\tspecialCharactersWithoutMinus = \"+*/.\"\n\ttopPriority                   = \"*/\"\n\tlowPriority                   = \"+-\"\n\trealmPath                     = \"/r/miko/calculator\"\n)\n\nvar (\n\tval        float64\n\tdisplayVal string\n\n\toperationMap = map[string]func(left float64, right float64) float64{\n\t\t\"+\": func(left float64, right float64) float64 { return left + right },\n\t\t\"-\": func(left float64, right float64) float64 { return left - right },\n\t\t\"*\": func(left float64, right float64) float64 { return left * right },\n\t\t\"/\": func(left float64, right float64) float64 {\n\t\t\tif right == 0 {\n\t\t\t\tpanic(\"Division by 0 is forbidden\")\n\t\t\t}\n\t\t\treturn left / right\n\t\t},\n\t}\n)\n\nfunc init(cur realm) {\n\thor.Register(cross(cur), \"Miko's calculator\", \"\")\n}\n\nfunc evaluateValidity(line string) (bool, string) {\n\tif len(line) == 0 {\n\t\treturn false, \"Invalid empty input\"\n\t} // edge case empty line\n\tif strings.Index(specialCharactersWithoutMinus, string(line[0])) != -1 ||\n\t\tstrings.Index(specialCharacters, string(line[len(line)-1])) != -1 {\n\t\treturn false, \"Invalid equation\"\n\t} // edge case special character at begining or end\n\n\tisPriorSpecial := false\n\tcountParenthesis := 0\n\n\tfor i := 0; i \u003c len(line); i++ {\n\t\tif line[i] == '(' {\n\t\t\tcountParenthesis += 1\n\t\t\tcontinue\n\t\t}\n\t\tif line[i] == ')' {\n\t\t\tif isPriorSpecial == true {\n\t\t\t\treturn false, \"Invalid equation\"\n\t\t\t}\n\t\t\tcountParenthesis -= 1\n\t\t\tisPriorSpecial = false\n\t\t\tcontinue\n\t\t}\n\t\tif strings.Index(specialCharacters, string(line[i])) != -1 {\n\t\t\tif isPriorSpecial \u0026\u0026 !(line[i] == '-' \u0026\u0026 i \u003c (len(line)-1) \u0026\u0026 line[i+1] \u003e= '0' \u0026\u0026 line[i+1] \u003c= '9') { // if we have two subsequent operator and the second one isn't a - before a number (negative number)\n\t\t\t\treturn false, \"Invalid equation\"\n\t\t\t}\n\t\t\tisPriorSpecial = true\n\t\t\tcontinue\n\t\t}\n\t\tif line[i] \u003c '0' || line[i] \u003e '9' {\n\t\t\treturn false, \"Invalid character encountered \"\n\t\t}\n\t\tisPriorSpecial = false\n\t}\n\n\tif countParenthesis != 0 {\n\t\treturn false, \"Invalid equation\"\n\t}\n\treturn true, \"\"\n}\n\nfunc searchForPriority(priorityList string, line string) *Node {\n\tcountParenthesis := 0\n\tfor iPrio := 0; iPrio \u003c len(priorityList); iPrio++ {\n\t\tfor idx := 0; idx \u003c len(line); idx++ {\n\t\t\tif line[idx] == '(' {\n\t\t\t\tcountParenthesis += 1\n\t\t\t}\n\t\t\tif line[idx] == ')' {\n\t\t\t\tcountParenthesis -= 1\n\t\t\t}\n\t\t\tif countParenthesis == 0 \u0026\u0026 line[idx] == priorityList[iPrio] \u0026\u0026\n\t\t\t\t!(line[idx] == '-' \u0026\u0026 (idx == 0 || strings.Index(specialCharacters, string(line[idx-1])) != -1)) { // - is not a substract sign if at the begining or after another sign\n\t\t\t\treturn \u0026Node{string(line[idx]), createTree(line[:idx]), createTree(line[idx+1:])}\n\t\t\t}\n\n\t\t}\n\t}\n\treturn nil\n}\n\n// checks if the expression in line is contained in one big parenthesis\nfunc isInOneParenthesis(line string) bool {\n\tif line[0] != '(' || line[len(line)-1] != ')' {\n\t\treturn false\n\t}\n\tcountParenthesis := 1\n\tfor i := 1; i \u003c len(line)-1; i++ {\n\t\tif line[i] == '(' {\n\t\t\tcountParenthesis += 1\n\t\t}\n\t\tif line[i] == ')' {\n\t\t\tcountParenthesis -= 1\n\t\t}\n\t\tif countParenthesis == 0 {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc createTree(line string) *Node {\n\tif isInOneParenthesis(line) {\n\t\treturn createTree(line[1 : len(line)-1])\n\t}\n\tnode := searchForPriority(lowPriority, line) // we put the lowest priority at the top of the tree, these operations will be executed last\n\tif node != nil {\n\t\treturn node\n\t}\n\tnode = searchForPriority(topPriority, line)\n\tif node != nil {\n\t\treturn node\n\t}\n\n\t// if this code is reached, the only value possible in line is a number\n\treturn \u0026Node{line, nil, nil}\n}\n\nfunc readTree(tree *Node) float64 {\n\toperation, exists := operationMap[tree.value]\n\n\tif exists { // check if the current node is an operator\n\t\treturn operation(readTree(tree.left), readTree(tree.right))\n\t}\n\n\tparsedValue, _ := strconv.ParseFloat(tree.value, 64)\n\n\treturn parsedValue\n}\n\n// expression is the equation you want to solve (p replaces the + symbol)\n// exemple: 2p4/2\nfunc ComputeResult(expression string) string {\n\tvalid, errString := evaluateValidity(expression)\n\n\tif !valid { // If a basic error is encountered, return the expression without the = at the end and display the same expression\n\t\tprintln(errString) // display error for debug\n\t\treturn expression\n\t}\n\ttree := createTree(expression)\n\tval = readTree(tree)\n\tdisplayVal = strconv.FormatFloat(val, 'g', 6, 64)\n\treturn displayVal\n}\n\nfunc removeLast(path string) string {\n\tlenPath := len(path)\n\tif lenPath \u003e 0 {\n\t\tpath = path[:lenPath-1]\n\t}\n\treturn path\n}\n\nfunc createTable(req *realmpath.Request, query url.Values, expression string) mdtable.Table {\n\tline := make([]string, 0, 4)\n\tquery.Set(\"expression\", \"\")\n\tline = append(line, md.Link(\"res\", req.String()))\n\tfor _, str := range []string{\"(\", \")\"} {\n\t\tquery.Set(\"expression\", expression+str)\n\t\tline = append(line, md.Link(str, req.String()))\n\t}\n\tquery.Set(\"expression\", removeLast(expression))\n\tline = append(line, md.Link(\"del\", req.String())) // req and del are two special cases\n\ttable := mdtable.Table{\n\t\tHeaders: line,\n\t}\n\tline = []string{}\n\tfor _, c := range \"789+456-123*0.=/\" {\n\t\tquery.Set(\"expression\", expression+string(c))\n\t\tline = append(line, md.Link(string(c), req.String()))\n\t\tif len(line) == 4 {\n\t\t\ttable.Append(line)\n\t\t\tline = []string{}\n\t\t}\n\t}\n\treturn table\n}\n\nfunc Render(path string) string {\n\treq := realmpath.Parse(path)\n\tquery := req.Query\n\texpression := query.Get(\"expression\")\n\n\tif expression == \"\" {\n\t\tdisplayVal = \"0\"\n\t} else {\n\t\tif expression[len(expression)-1] == '=' {\n\t\t\texpression = ComputeResult(expression[:len(expression)-1])\n\t\t} else {\n\t\t\tdisplayVal = expression\n\t\t}\n\t}\n\tout := md.H1(\"Calculator page\")\n\tout += md.H3(\"Have you ever wanted to do maths but never actually found a calculator ?\")\n\tout += md.H3(\"Do I have the realm for you...\")\n\tout += \"---------------\\n\"\n\tout += md.H2(\"Result: \" + displayVal)\n\n\ttable := createTable(req, query, expression)\n\tout += table.String()\n\treturn out\n}\n"},{"name":"calculator_test.gno","body":"package calculator\n\nimport \"testing\"\n\nfunc Test_Addition(t *testing.T) {\n\t// Increment the value\n\tvalue := ComputeResult(\"1+1\")\n\t// Verify the value is equal to 2\n\tif value != \"2\" {\n\t\tt.Fatalf(\"1 + 1 is not equal to 2\")\n\t}\n}\n\nfunc Test_Subtraction(t *testing.T) {\n\t// Increment the value\n\tvalue := ComputeResult(\"1-1\")\n\t// Verify the value is equal to 0\n\tif value != \"0\" {\n\t\tt.Fatalf(\"1 - 1 is not equal to 0\")\n\t}\n}\n\nfunc Test_Multiplication(t *testing.T) {\n\t// Increment the value\n\tvalue := ComputeResult(\"1*4\")\n\t// Verify the value is equal to 4\n\tif value != \"4\" {\n\t\tt.Fatalf(\"1 * 4 is not equal to 4\")\n\t}\n}\n\nfunc Test_Division(t *testing.T) {\n\t// Increment the value\n\tvalue := ComputeResult(\"4/2\")\n\t// Verify the value is equal to 2\n\tif value != \"2\" {\n\t\tt.Fatalf(\"4 / 2 is not equal to 2\")\n\t}\n}\n\nfunc Test_AdditionDecimal(t *testing.T) {\n\t// Increment the value\n\tvalue := ComputeResult(\"1.2+1.3\")\n\t// Verify the value is equal to 2.5\n\tif value != \"2.5\" {\n\t\tt.Fatalf(\"1.2 + 1.3 is not equal to 2.5\")\n\t}\n}\n\nfunc Test_SubtractionDecimal(t *testing.T) {\n\t// Increment the value\n\tvalue := ComputeResult(\"1.3-1.2\")\n\t// Verify the value is equal to 0.1\n\tif value != \"0.1\" {\n\t\tt.Fatalf(\"1.3 - 1.2 is not equal to 0.1\")\n\t}\n}\n\nfunc Test_MultiplicationDecimal(t *testing.T) {\n\t// Increment the value\n\tvalue := ComputeResult(\"3*1.5\")\n\t// Verify the value is equal to 4.5\n\tif value != \"4.5\" {\n\t\tt.Fatalf(\"3 * 1.5 is not equal to 4.5\")\n\t}\n}\n\nfunc Test_DivisionDecimal(t *testing.T) {\n\t// Increment the value\n\tvalue := ComputeResult(\"2/0.5\")\n\t// Verify the value is equal to 4\n\tif value != \"4\" {\n\t\tt.Fatalf(\"2 / 0.5 is not equal to 4\")\n\t}\n}\n\nfunc Test_BaseParenthesis(t *testing.T) {\n\t// Increment the value\n\tvalue := ComputeResult(\"2*(3+4)\")\n\t// Verify the value is equal to 14\n\tif value != \"14\" {\n\t\tt.Fatalf(\"2 * (3 + 4) is not equal to 14\")\n\t}\n}\n\nfunc Test_AdvancedParenthesis(t *testing.T) {\n\t// Increment the value\n\tvalue := ComputeResult(\"2*(3+4/(6-2))\")\n\t// Verify the value is equal to 8\n\tif value != \"8\" {\n\t\tt.Fatalf(\"2 * (3 + 4 / (6 - 2)) is not equal to 8\")\n\t}\n}\n\nfunc Test_Negative(t *testing.T) {\n\t// Increment the value\n\tvalue := ComputeResult(\"-2+10\")\n\t// Verify the value is equal to 8\n\tif value != \"8\" {\n\t\tt.Fatalf(\"-2 + 10 is not equal to 8\")\n\t}\n}\n\nfunc Test_Negative2(t *testing.T) {\n\t// Increment the value\n\tvalue := ComputeResult(\"-2*-10\")\n\t// Verify the value is equal to 20\n\tif value != \"20\" {\n\t\tt.Fatalf(\"-2 * -10 is not equal to 20\")\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/miko/calculator\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"rsGdGtmJlxigF7jaL6Hza2gy+loAPhK8ulh/MTH2PollSI7tQbYwB+5UrteUkm9BPIBGDCdE/1ohDwrvJ7A8VQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"chess","path":"gno.land/r/morgan/chess","files":[{"name":"chess.gno","body":"// Realm chess implements a Gno chess server.\npackage chess\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"gno.land/p/morgan/chess\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n)\n\n// realm state\nvar (\n\t// (not \"games\" because that's too useful a variable name)\n\tgameStore     avl.Tree // string (game ID) -\u003e *Game\n\tgameIDCounter seqid.ID\n\n\t// Value must be sorted by game ID, descending\n\tuser2Games avl.Tree // address -\u003e []*Game\n)\n\n// Game represents a chess game.\ntype Game struct {\n\tID string `json:\"id\"`\n\n\tWhite    address        `json:\"white\"`\n\tBlack    address        `json:\"black\"`\n\tPosition chess.Position `json:\"position\"`\n\tState    GameState      `json:\"state\"`\n\tWinner   Winner         `json:\"winner\"`\n\n\tCreator     address   `json:\"creator\"`\n\tCreatedAt   time.Time `json:\"created_at\"`\n\tDrawOfferer *address  `json:\"draw_offerer\"` // set on draw offers\n\tConcluder   *address  `json:\"concluder\"`    // set on non-auto draws, and aborts\n\n\tTime *TimeControl `json:\"time\"`\n}\n\nfunc (g Game) json() string {\n\ts, err := g.MarshalJSON()\n\tcheckErr(err)\n\treturn string(s)\n}\n\n// Winner represents the \"direct\" outcome of a game\n// (white, black or draw?)\ntype Winner byte\n\nconst (\n\tWinnerNone Winner = iota\n\tWinnerWhite\n\tWinnerBlack\n\tWinnerDraw\n)\n\nvar winnerString = [...]string{\n\tWinnerNone:  \"none\",\n\tWinnerWhite: \"white\",\n\tWinnerBlack: \"black\",\n\tWinnerDraw:  \"draw\",\n}\n\n// GameState represents the current game state.\ntype GameState byte\n\nconst (\n\tGameStateInvalid = iota\n\n\tGameStateOpen\n\n\t// \"automatic\" endgames following moves\n\tGameStateCheckmated\n\tGameStateStalemate\n\tGameStateDrawn75Move\n\tGameStateDrawn5Fold\n\n\t// single-party draws\n\tGameStateDrawn50Move\n\tGameStateDrawn3Fold\n\tGameStateDrawnInsufficient\n\n\t// timeout by either player\n\tGameStateTimeout\n\t// aborted within first two moves\n\tGameStateAborted\n\t// resignation by either player\n\tGameStateResigned\n\t// draw by agreement\n\tGameStateDrawnByAgreement\n)\n\nvar gameStatesSnake = [...]string{\n\tGameStateInvalid:           \"invalid\",\n\tGameStateOpen:              \"open\",\n\tGameStateCheckmated:        \"checkmated\",\n\tGameStateStalemate:         \"stalemate\",\n\tGameStateDrawn75Move:       \"drawn_75_move\",\n\tGameStateDrawn5Fold:        \"drawn_5_fold\",\n\tGameStateDrawn50Move:       \"drawn_50_move\",\n\tGameStateDrawn3Fold:        \"drawn_3_fold\",\n\tGameStateDrawnInsufficient: \"drawn_insufficient\",\n\tGameStateTimeout:           \"timeout\",\n\tGameStateAborted:           \"aborted\",\n\tGameStateResigned:          \"resigned\",\n\tGameStateDrawnByAgreement:  \"drawn_by_agreement\",\n}\n\n// IsFinished returns whether the game is in a finished state.\nfunc (g GameState) IsFinished() bool {\n\treturn g != GameStateOpen\n}\n\n// NewGame initialized a new game with the given opponent.\n// opponent may be a bech32 address or \"@user\" (r/demo/users).\n//\n// seconds and increment specifies the time control for the given game.\n// seconds is the amount of time given to play to each player; increment\n// is by how many seconds the player's time should be increased when they make a move.\n// seconds \u003c= 0 means no time control (correspondence).\n//\n// XXX: Disabled for GnoChess production temporarily. (prefixed with x for unexported)\n// Ideally, we'd need this to work either by not forcing users not to have\n// parallel games OR by introducing a \"request\" system, so that a game is not\n// immediately considered \"open\" when calling NewGame.\nfunc xNewGame(cur realm, opponentRaw string, seconds, increment int) string {\n\tassertOriginCall(0, cur)\n\tif seconds \u003e= 0 \u0026\u0026 increment \u003c 0 {\n\t\tpanic(\"negative increment invalid\")\n\t}\n\n\topponent := parsePlayer(0, cur, opponentRaw)\n\tcaller := cur.Previous().Address()\n\tassertUserNotInLobby(caller)\n\n\treturn newGame(caller, opponent, seconds, increment).json()\n}\n\nfunc getUserGames(user address) []*Game {\n\tval := user2Games.Get(user.String())\n\tif val == nil {\n\t\treturn nil\n\t}\n\treturn val.([]*Game)\n}\n\nfunc assertGamesFinished(games []*Game) {\n\tfor _, g := range games {\n\t\tif g.State.IsFinished() {\n\t\t\tcontinue\n\t\t}\n\t\terr := g.claimTimeout()\n\t\tif err != nil {\n\t\t\tpanic(\"can't start new game: game \" + g.ID + \" is not yet finished\")\n\t\t}\n\t}\n}\n\nfunc newGame(caller, opponent address, seconds, increment int) *Game {\n\tgames := getUserGames(caller)\n\t// Ensure player has no ongoing games.\n\tassertGamesFinished(games)\n\tassertGamesFinished(getUserGames(opponent))\n\n\tif caller == opponent {\n\t\tpanic(\"can't create a game with yourself\")\n\t}\n\n\tisBlack := determineColor(games, caller, opponent)\n\n\t// id is zero-padded to work well with avl's alphabetic order.\n\tid := gameIDCounter.Next().String()\n\tg := \u0026Game{\n\t\tID:        id,\n\t\tWhite:     caller,\n\t\tBlack:     opponent,\n\t\tPosition:  chess.NewPosition(),\n\t\tState:     GameStateOpen,\n\t\tCreator:   caller,\n\t\tCreatedAt: time.Now(),\n\t\tTime:      NewTimeControl(seconds, increment),\n\t}\n\tif isBlack {\n\t\tg.White, g.Black = g.Black, g.White\n\t}\n\n\tgameStore.Set(g.ID, g)\n\taddToUser2Games(caller, g)\n\taddToUser2Games(opponent, g)\n\n\treturn g\n}\n\nfunc addToUser2Games(addr address, game *Game) {\n\tvar games []*Game\n\tv := user2Games.Get(string(addr))\n\tif v != nil {\n\t\tgames = v.([]*Game)\n\t}\n\t// game must be at top, because it is the latest ID\n\tgames = append([]*Game{game}, games...)\n\tuser2Games.Set(string(addr), games)\n}\n\nfunc determineColor(games []*Game, caller, opponent address) (isBlack bool) {\n\t// fast path for no games\n\tif len(games) == 0 {\n\t\treturn false\n\t}\n\n\t// Determine color of player. If the player has already played with\n\t// opponent, invert from last game played among them.\n\t// Otherwise invert from last game played by the player.\n\tisBlack = games[0].White == caller\n\n\t// \"try\" to save gas if the user has really a lot of past games\n\tif len(games) \u003e 256 {\n\t\tgames = games[:256]\n\t}\n\tfor _, game := range games {\n\t\tif game.White == opponent || game.Black == opponent {\n\t\t\treturn game.White == caller\n\t\t}\n\t}\n\treturn\n}\n\n// GetGame returns a game, knowing its ID.\nfunc GetGame(id string) string {\n\treturn getGame(id, false).json()\n}\n\nfunc getGame(id string, wantOpen bool) *Game {\n\tgraw := gameStore.Get(id)\n\tif graw == nil {\n\t\tpanic(\"game not found\")\n\t}\n\tg := graw.(*Game)\n\tif wantOpen \u0026\u0026 g.State.IsFinished() {\n\t\tpanic(\"game is already finished\")\n\t}\n\treturn g\n}\n\n// MakeMove specifies a move to be done on the given game, specifying in\n// algebraic notation the square where to move the piece.\n// If the piece is a pawn which is moving to the last row, a promotion piece\n// must be specified.\n// Castling is specified by indicating the king's movement.\nfunc MakeMove(cur realm, gameID, from, to string, promote chess.Piece) string {\n\tassertOriginCall(0, cur)\n\tg := getGame(gameID, true)\n\n\t// determine if this is a black move\n\tisBlack := len(g.Position.Moves)%2 == 1\n\n\tcaller := cur.Previous().Address()\n\tif (isBlack \u0026\u0026 g.Black != caller) ||\n\t\t(!isBlack \u0026\u0026 g.White != caller) {\n\t\t// either not a player involved; or not the caller's turn.\n\t\tpanic(\"you are not allowed to make a move at this time\")\n\t}\n\n\t// game is time controlled? add move to time control\n\tif g.Time != nil {\n\t\tvalid := g.Time.AddMove()\n\t\tif !valid \u0026\u0026 len(g.Position.Moves) \u003c 2 {\n\t\t\tg.State = GameStateAborted\n\t\t\tg.Concluder = \u0026caller\n\t\t\tg.Winner = WinnerNone\n\t\t\treturn g.json()\n\t\t}\n\t\tif !valid {\n\t\t\tg.State = GameStateTimeout\n\t\t\tif caller == g.White {\n\t\t\t\tg.Winner = WinnerBlack\n\t\t\t} else {\n\t\t\t\tg.Winner = WinnerWhite\n\t\t\t}\n\t\t\tg.saveResult()\n\t\t\treturn g.json()\n\t\t}\n\t}\n\n\t// validate move\n\tm := chess.Move{\n\t\tFrom: chess.SquareFromString(from),\n\t\tTo:   chess.SquareFromString(to),\n\t}\n\tif m.From == chess.SquareInvalid || m.To == chess.SquareInvalid {\n\t\tpanic(\"invalid from/to square\")\n\t}\n\tif promote \u003e 0 \u0026\u0026 promote \u003c= chess.PieceKing {\n\t\tm.Promotion = promote\n\t}\n\tnewp, ok := g.Position.ValidateMove(m)\n\tif !ok {\n\t\tpanic(\"illegal move\")\n\t}\n\n\t// add move and record new board\n\tg.Position = newp\n\n\to := newp.IsFinished()\n\tif o == chess.NotFinished {\n\t\t// opponent of draw offerer has made a move. take as implicit rejection of draw.\n\t\tif g.DrawOfferer != nil \u0026\u0026 *g.DrawOfferer != caller {\n\t\t\tg.DrawOfferer = nil\n\t\t}\n\n\t\treturn g.json()\n\t}\n\n\tswitch {\n\tcase o == chess.Checkmate \u0026\u0026 isBlack:\n\t\tg.State = GameStateCheckmated\n\t\tg.Winner = WinnerBlack\n\tcase o == chess.Checkmate \u0026\u0026 !isBlack:\n\t\tg.State = GameStateCheckmated\n\t\tg.Winner = WinnerWhite\n\tcase o == chess.Stalemate:\n\t\tg.State = GameStateStalemate\n\t\tg.Winner = WinnerDraw\n\n\tcase o == chess.Drawn75Move:\n\t\tg.State = GameStateDrawn75Move\n\t\tg.Winner = WinnerDraw\n\tcase o == chess.Drawn5Fold:\n\t\tg.State = GameStateDrawn5Fold\n\t\tg.Winner = WinnerDraw\n\t}\n\tg.DrawOfferer = nil\n\tg.saveResult()\n\n\treturn g.json()\n}\n\nfunc (g *Game) claimTimeout() error {\n\t// no assert origin call or caller check: anyone can claim a game to have\n\t// finished in timeout.\n\n\tif g.Time == nil {\n\t\treturn errors.New(\"game is not time controlled\")\n\t}\n\n\t// game is time controlled? add move to time control\n\tto := g.Time.TimedOut()\n\tif !to {\n\t\treturn errors.New(\"game is not timed out\")\n\t}\n\n\tif nmov := len(g.Position.Moves); nmov \u003c 2 {\n\t\tg.State = GameStateAborted\n\t\tif nmov == 1 {\n\t\t\tg.Concluder = \u0026g.Black\n\t\t} else {\n\t\t\tg.Concluder = \u0026g.White\n\t\t}\n\t\tg.Winner = WinnerNone\n\t\treturn nil\n\t}\n\n\tg.State = GameStateTimeout\n\tif len(g.Position.Moves)\u00261 == 0 {\n\t\tg.Winner = WinnerBlack\n\t} else {\n\t\tg.Winner = WinnerWhite\n\t}\n\tg.DrawOfferer = nil\n\tg.saveResult()\n\n\treturn nil\n}\n\n// ClaimTimeout should be called when the caller believes the game has resulted\n// in a timeout.\nfunc ClaimTimeout(cur realm, gameID string) string {\n\tg := getGame(gameID, true)\n\terr := g.claimTimeout()\n\tcheckErr(err)\n\n\treturn g.json()\n}\n\nfunc Abort(cur realm, gameID string) string {\n\tassertOriginCall(0, cur)\n\tg := getGame(gameID, true)\n\terr := abort(0, cur, g)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\treturn g.json()\n}\n\nfunc abort(_ int, rlm realm, g *Game) error {\n\tif !rlm.IsCurrent() {\n\t\treturn errors.New(\"unauthorized: rlm is not the caller's live cur\")\n\t}\n\tif len(g.Position.Moves) \u003e= 2 {\n\t\treturn errors.New(\"game can no longer be aborted; if you wish to quit, resign\")\n\t}\n\n\tcaller := rlm.Previous().Address()\n\tif caller != g.White \u0026\u0026 caller != g.Black {\n\t\treturn errors.New(\"you are not involved in this game\")\n\t}\n\tg.State = GameStateAborted\n\tg.Concluder = \u0026caller\n\tg.DrawOfferer = nil\n\tg.Winner = WinnerNone\n\n\treturn nil\n}\n\nfunc Resign(cur realm, gameID string) string {\n\tassertOriginCall(0, cur)\n\tg := getGame(gameID, true)\n\terr := resign(0, cur, g)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\treturn g.json()\n}\n\nfunc resign(_ int, rlm realm, g *Game) error {\n\tif !rlm.IsCurrent() {\n\t\treturn errors.New(\"unauthorized: rlm is not the caller's live cur\")\n\t}\n\tif len(g.Position.Moves) \u003c 2 {\n\t\treturn abort(0, rlm, g)\n\t}\n\tcaller := rlm.Previous().Address()\n\tswitch caller {\n\tcase g.Black:\n\t\tg.State = GameStateResigned\n\t\tg.Winner = WinnerWhite\n\tcase g.White:\n\t\tg.State = GameStateResigned\n\t\tg.Winner = WinnerBlack\n\tdefault:\n\t\treturn errors.New(\"you are not involved in this game\")\n\t}\n\tg.DrawOfferer = nil\n\tg.saveResult()\n\n\treturn nil\n}\n\n// DrawOffer creates a draw offer in the current game, if one doesn't already\n// exist.\nfunc DrawOffer(cur realm, gameID string) string {\n\tassertOriginCall(0, cur)\n\tg := getGame(gameID, true)\n\tcaller := cur.Previous().Address()\n\n\tswitch {\n\tcase caller != g.Black \u0026\u0026 caller != g.White:\n\t\tpanic(\"you are not involved in this game\")\n\tcase g.DrawOfferer != nil:\n\t\tpanic(\"a draw offer in this game already exists\")\n\t}\n\n\tg.DrawOfferer = \u0026caller\n\treturn g.json()\n}\n\n// DrawRefuse refuse a draw offer in the given game.\nfunc DrawRefuse(cur realm, gameID string) string {\n\tassertOriginCall(0, cur)\n\tg := getGame(gameID, true)\n\tcaller := cur.Previous().Address()\n\n\tswitch {\n\tcase caller != g.Black \u0026\u0026 caller != g.White:\n\t\tpanic(\"you are not involved in this game\")\n\tcase g.DrawOfferer == nil:\n\t\tpanic(\"no draw offer present\")\n\tcase *g.DrawOfferer == caller:\n\t\tpanic(\"can't refuse an offer you sent yourself\")\n\t}\n\n\tg.DrawOfferer = nil\n\treturn g.json()\n}\n\n// Draw implements draw by agreement, as well as \"single-party\" draw:\n// - Threefold repetition (§9.2)\n// - Fifty-move rule (§9.3)\n// - Insufficient material (§9.4)\n// Note: stalemate happens as a consequence of a Move, and thus is handled in that function.\nfunc Draw(cur realm, gameID string) string {\n\tassertOriginCall(0, cur)\n\tg := getGame(gameID, true)\n\n\tcaller := cur.Previous().Address()\n\tif caller != g.Black \u0026\u0026 caller != g.White {\n\t\tpanic(\"you are not involved in this game\")\n\t}\n\n\t// accepted draw offer (do early to avoid gas for g.Position.IsFinished())\n\tif g.DrawOfferer != nil \u0026\u0026 *g.DrawOfferer != caller {\n\t\tg.State = GameStateDrawnByAgreement\n\t\tg.Winner = WinnerDraw\n\t\tg.Concluder = \u0026caller\n\n\t\tg.saveResult()\n\n\t\treturn g.json()\n\t}\n\n\to := g.Position.IsFinished()\n\tswitch {\n\tcase o\u0026chess.Can50Move != 0:\n\t\tg.State = GameStateDrawn50Move\n\tcase o\u0026chess.Can3Fold != 0:\n\t\tg.State = GameStateDrawn3Fold\n\tcase o\u0026chess.CanInsufficient != 0:\n\t\tg.State = GameStateDrawnInsufficient\n\tdefault:\n\t\tpanic(\"this game can't be automatically drawn\")\n\t}\n\tg.Concluder = \u0026caller\n\tg.Winner = WinnerDraw\n\tg.DrawOfferer = nil\n\n\tg.saveResult()\n\n\treturn g.json()\n}\n\nfunc checkErr(err error) {\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n}\n\n// assertOriginCall is a user-call guard — it accepts EOA maketx-call AND\n// maketx-run ephemeral realms. Despite the name, this is NOT equivalent to\n// runtime.AssertOriginCall() (which is strictly direct MsgCall only).\n//\n// WARNING: do not reuse this helper from any function that reads\n// unsafe.OriginSend() to verify payment. IsUser() lets a maketx-run script\n// consume the origin-send envelope before calling us, bypassing the check.\n// For payment paths, use IsUserCall() (EOA only) — see\n// docs/resources/effective-gno.md#verifying-inbound-coin-payments.\nfunc assertOriginCall(_ int, rlm realm) {\n\tif !rlm.IsCurrent() {\n\t\tpanic(\"unauthorized: rlm is not the caller's live cur\")\n\t}\n\tif !rlm.Previous().IsUser() {\n\t\tpanic(\"invalid non-origin call\")\n\t}\n}\n"},{"name":"chess_test.gno","body":"package chess\n\nimport (\n\t\"chain\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/morgan/chess\"\n\t\"gno.land/p/morgan/chess/glicko2\"\n\t\"gno.land/p/nt/avl/v0\"\n)\n\nfunc cleanup() {\n\tgameStore = avl.Tree{}\n\tgameIDCounter = 0\n\tuser2Games = avl.Tree{}\n\tplayerStore = avl.Tree{}\n\tleaderboard = [CategoryMax]leaderboardType{}\n\tlobby = [tcLobbyMax][]lobbyPlayer{}\n\tlobbyPlayer2Game = avl.Tree{}\n\tplayerRatings = [CategoryMax][]*glicko2.PlayerRating{}\n}\n\nfunc TestNewGame(cur realm, t *testing.T) {\n\tcleanup()\n\n\tg := xNewGame(cur, chain.PackageAddress(\"xx\").String(), 0, 0)\n\tprintln(g)\n}\n\nconst (\n\twhite address = \"g1white\"\n\tblack address = \"g1black\"\n)\n\n/*\nsyntax:\n\n\t[\u003ccommand\u003e ][#[!][\u003cbuf\u003e] \u003cchecker\u003e]\n\ncommand is executed; result of command is stored in buffer.\nthe test is split in lines. other white space is ignored (strings.Fields).\n\n\u003cbuf\u003e: all commands below will generally store a string result value in the\nbuffer \"result\", which is the default and thus may be omitted.\nif the command panics, the panic value is stored in the buffer \"panic\".\n(if it doesn't, buffer panic is set to an empty string).\nif following a command there is no checker on the #panic buffer, the line\n\"#!panic empty\" is implicitly added.\nif \u003cbuf\u003e is preceded by ! (e.g. \"#!panic empty\"), then if the checker fails,\nprocessing is stopped on that line.\n\n\u003ccommand\u003e:\n\n\tnewgame [\u003cwhite\u003e \u003cblack\u003e [\u003cseconds\u003e [\u003cincrement\u003e]]]\n\t\tstores game ID in buffer #id.\n\t\t\u003cwhite\u003e and \u003cblack\u003e are two addresses. if they are not passed, \u003cwhite\u003e\n\t\tassumes value \"white\" and \u003cblack\u003e \"black\"\n\tmove \u003cplayer\u003e \u003clan_move\u003e\n\t\tlan_move is in the same format as Move.String.\n\t\tretrieves game id from #id.\n\tdraw \u003cplayer\u003e\n\tdrawoffer \u003cplayer\u003e\n\tabort \u003cplayer\u003e\n\ttimeout \u003cplayer\u003e\n\t\t(ClaimTimeout)\n\tresign \u003cplayer\u003e\n\tgame [\u003cid\u003e]\n\t\tif not given, id is retrieved from buffer #id.\n\tplayer \u003cplayer\u003e\n\tname \u003cpredicate\u003e\n\t\tsets the name of the test to predicate.\n\tcopy \u003cdst\u003e [\u003csrc\u003e]\n\t\tcopies buffer src to buffer dst.\n\t\tif src not specified, assumed result.\n\t\t(don't specify the #; ie: copy oldresult result)\n\tsleep \u003cseconds\u003e\n\t\tsleep for the given amount of seconds (float).\n\nNOTE: for all values of \u003cplayer\u003e, including \u003cwhite\u003e and \u003cblack\u003e in newgame,\nthe addresses are passed prefixed by \"g1\", and the matching checkers should\nexpect this.\n\n\u003cchecker\u003e:\n\n\tempty\n\t\tthe buffer should be empty.\n\tequal \u003cpredicate\u003e\n\t\tpredicate may start with #, which indicates a buffer.\n\tcontains [\u003cpredicate\u003e...]\n\t\tthe buffer should contain all of the given predicates.\n\tcontainssp \u003cpredicate\u003e\n\t\tthe buffer should contain the given predicate, which contains spaces.\n*/\nvar commandTests = [...]string{\n\t`\tname NewGameNegativeIncrement\n\t\tnewgame white black 10 -5 #panic containssp negative increment invalid\n\t`,\n\t`\tname NewGameDouble\n\t\tnewgame\n\t\tnewgame #panic contains is not yet finished\n\t`,\n\t`\tname NewGameWithSelf\n\t\tnewgame white white #panic contains game with yourself\n\t`,\n\t// ColoursInvert within games played by two players\n\t`\tname ColoursInvert\n\t\tnewgame\n\t\tmove white e2e4\n\t\tmove black e7e5\n\t\tmove white f1c4\n\t\tresign white\n\t\tnewgame\n\t\t# contains \"white\":\"g1black\" \"black\":\"g1white\"\n\t\t#id equal 0000002\n\t`,\n\t// Otherwise, invert from p1's history.\n\t`\tname ColoursInvert3p\n\t\tnewgame p1 p2 #! contains \"white\":\"g1p1\" \"black\":\"g1p2\"\n\t\tmove p1 e2e4\n\t\tabort p1\n\t\tnewgame p1 p3 # contains \"white\":\"g1p3\" \"black\":\"g1p1\"\n\t`,\n\t`\tname ScholarsMate\n\t\tnewgame #id equal 0000001\n\t\tmove white e2e4\n\t\tmove black e7e5\n\t\tmove white f1c4\n\t\tmove black b8c6\n\t\tmove white d1f3\n\t\tmove black d7d6\n\t\tmove white f3f7\n\t\tcopy moveres\n\t\tgame # equal #moveres\n\t\t# contains \"state\":\"checkmated\" \"winner\":\"white\"\n\t\t# containssp r1bqkbnr/ppp2Qpp/2np4/4p3/2B1P3/8/PPPP1PPP/RNB1K1NR b KQkq - 0 4\n\t\tplayer white\n\t\t# contains \"address\":\"g1white\" \"position\":0 \"wins\":1 \"losses\":0 \"draws\":0\n\t\tplayer black\n\t\t# contains \"address\":\"g1black\" \"position\":1 \"wins\":0 \"losses\":1 \"draws\":0\n\t`,\n\t`\tname DrawByAgreement\n\t\tnewgame\n\t\tmove white e2e4\n\t\tmove black e7e5\n\t\tmove white f1c4\n\t\tmove black b8c6\n\t\tcopy moveres\n\t\tgame # equal #moveres\n\t\t# contains \"open\" \"concluder\":null \"draw_offerer\":null\n\t\tdrawoffer white\n\t\t# contains \"open\" \"concluder\":null \"draw_offerer\":\"g1white\"\n\t\tdraw black\n\t\t# contains \"drawn_by_agreement\" \"concluder\":\"g1black\" \"draw_offerer\":\"g1white\"\n\t`,\n\t`\tname AbortFirstMove\n\t\tnewgame\n\t\tabort white # contains \"winner\":\"none\" \"concluder\":\"g1white\"\n\t`,\n\n\t`\tname ThreefoldRepetition\n\t\tnewgame\n\n\t\tmove white g1f3\n\t\tmove black g8f6\n\t\tmove white f3g1\n\t\tmove black f6g8\n\n\t\tmove white g1f3\n\t\tmove black g8f6\n\t\tmove white f3g1\n\t\tmove black f6g8\n\n\t\tdraw black # contains \"winner\":\"draw\" \"concluder\":\"g1black\"\n\t\t# contains \"state\":\"drawn_3_fold\"\n\t`,\n\t`\tname FivefoldRepetition\n\t\tnewgame\n\n\t\tmove white g1f3\n\t\tmove black g8f6\n\t\tmove white f3g1\n\t\tmove black f6g8\n\n\t\tmove white g1f3\n\t\tmove black g8f6\n\t\tmove white f3g1\n\t\tmove black f6g8\n\n\t\tmove white g1f3\n\t\tmove black g8f6\n\t\tmove white f3g1\n\t\tmove black f6g8\n\n\t\tmove white g1f3\n\t\tmove black g8f6\n\t\tmove white f3g1\n\t\tmove black f6g8\n\n\t\t# contains \"winner\":\"draw\" \"concluder\":null \"state\":\"drawn_5_fold\"\n\n\t\tmove white g1f3 #panic contains game is already finished\n\t`,\n\t`\tname TimeoutAborted\n\t\tnewgame white black 3\n\t\tmove white e2e4 #! contains \"state\":\"open\"\n\t\tsleep 31\n\t\tmove black e7e5\n\t\tgame\n\t\t# contains e2e4\n\t\t# contains \"aborted\"\n\t\t# contains \"concluder\":\"g1black\"\n\t`,\n\t`\tname TimeoutAbandoned\n\t\tnewgame white black 1\n\t\tmove white e2e4\n\t\tmove black e7e5\n\t\tsleep 61\n\t\ttimeout black\n\t\t# contains \"state\":\"timeout\" \"winner\":\"black\"\n\t`,\n}\n\nfunc TestCommands(cur realm, t *testing.T) {\n\tfor _, command := range commandTests {\n\t\trunCommandTest(cur, t, command)\n\t}\n}\n\n// testCommandRunner is used to represent the single testCommand types below.\n// the Run function is used to execute the actual command, after parsing.\n//\n// This could have been implemented with simple closures generated within the\n// parser, however it's not because of this issue:\n// https://github.com/gnolang/gno/issues/1135\ntype testCommandRunner interface {\n\tRun(cur realm, t *testing.T, bufs map[string]string)\n}\n\n// testCommandChecker is a wrapper for a runner which is performing a check.\n// This is marked in order not to wrap the calls to them as a panic.\ntype testCommandChecker struct{ testCommandRunner }\n\nfunc (testCommandChecker) Checker() {}\n\ntype testCommandFunc func(cur realm, t *testing.T, bufs map[string]string)\n\nfunc (tc testCommandFunc) Run(cur realm, t *testing.T, bufs map[string]string) {\n\ttc(cross(cur), t, bufs)\n}\n\n// testCommandColorID represents a testCommand, which uses a function of the\n// form func(gameID string) string (hence ID), and that takes as the first\n// parameter a \u003cplayer\u003e which will be the caller.\ntype testCommandColorID struct {\n\tfn   func(realm, string) string\n\taddr address\n}\n\nfunc newTestCommandColorID(fn func(realm, string) string, s string, addr string) testCommandRunner {\n\treturn \u0026testCommandColorID{fn, address(\"g1\" + addr)}\n}\n\nfunc (tc *testCommandColorID) Run(cur realm, t *testing.T, bufs map[string]string) {\n\ttesting.SetRealm(testing.NewUserRealm(tc.addr))\n\tbufs[\"result\"] = tc.fn(cross(cur), bufs[\"id\"])\n}\n\ntype testCommandNewGame struct {\n\tw, b          address\n\tseconds, incr int\n}\n\nfunc (tc *testCommandNewGame) Run(cur realm, t *testing.T, bufs map[string]string) {\n\ttesting.SetRealm(testing.NewUserRealm(tc.w))\n\tres := xNewGame(cross(cur), string(tc.b), tc.seconds, tc.incr)\n\tbufs[\"result\"] = res\n\n\tconst idMagicString = `\"id\":\"`\n\tidx := strings.Index(res, idMagicString)\n\tif idx \u003c 0 {\n\t\tpanic(\"id not found\")\n\t}\n\tid := res[idx+len(idMagicString):]\n\tid = id[:strings.IndexByte(id, '\"')]\n\tbufs[\"id\"] = id\n}\n\ntype testCommandMove struct {\n\taddr      address\n\tfrom, to  string\n\tpromotion chess.Piece\n}\n\nfunc (tc *testCommandMove) Run(cur realm, t *testing.T, bufs map[string]string) {\n\ttesting.SetRealm(testing.NewUserRealm(tc.addr))\n\tbufs[\"result\"] = MakeMove(cross(cur), bufs[\"id\"], tc.from, tc.to, tc.promotion)\n}\n\ntype testCommandGame struct {\n\tidWanted string\n}\n\nfunc (tc *testCommandGame) Run(cur realm, t *testing.T, bufs map[string]string) {\n\tidl := tc.idWanted\n\tif idl == \"\" {\n\t\tidl = bufs[\"id\"]\n\t}\n\tbufs[\"result\"] = GetGame(idl)\n}\n\ntype testCommandPlayer struct {\n\taddr string\n}\n\nfunc (tc *testCommandPlayer) Run(cur realm, t *testing.T, bufs map[string]string) {\n\tbufs[\"result\"] = GetPlayer(cross(cur), tc.addr)\n}\n\ntype testCommandCopy struct {\n\tdst, src string\n}\n\nfunc (tc *testCommandCopy) Run(cur realm, t *testing.T, bufs map[string]string) {\n\tbufs[tc.dst] = bufs[tc.src]\n}\n\ntype testCommandSleep struct {\n\tdur time.Duration\n}\n\nfunc (tc *testCommandSleep) Run(cur realm, t *testing.T, bufs map[string]string) {\n\tos.Sleep(tc.dur)\n}\n\ntype testChecker struct {\n\tfn    func(t *testing.T, bufs map[string]string, tc *testChecker)\n\ttf    func(*testing.T, string, ...interface{})\n\tbufp  string\n\tpreds []string\n}\n\nfunc (*testChecker) Checker() {}\nfunc (tc *testChecker) Run(cur realm, t *testing.T, bufs map[string]string) {\n\ttc.fn(t, bufs, tc)\n}\n\nfunc parseCommandTest(t *testing.T, command string) (funcs []testCommandRunner, testName string) {\n\tlines := strings.Split(command, \"\\n\")\n\tatoi := func(s string) int {\n\t\tn, err := strconv.Atoi(s)\n\t\tcheckErr(err)\n\t\treturn n\n\t}\n\t// used to detect whether to auto-add a panic checker\n\tvar hasPanicChecker bool\n\tpanicChecker := func(lineNum int, testName string) testCommandRunner {\n\t\treturn testCommandChecker{testCommandFunc(\n\t\t\tfunc(cur realm, t *testing.T, bufs map[string]string) {\n\t\t\t\tif bufs[\"panic\"] != \"\" {\n\t\t\t\t\tt.Fatalf(\"%s:%d: buffer \\\"panic\\\" is not empty (%q)\", testName, lineNum, bufs[\"panic\"])\n\t\t\t\t}\n\t\t\t},\n\t\t)}\n\t}\n\n\tfor lineNum, line := range lines {\n\t\tflds := strings.Fields(line)\n\t\tif len(flds) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tcommand, checker := flds, ([]string)(nil)\n\t\tfor idx, fld := range flds {\n\t\t\tif strings.HasPrefix(fld, \"#\") {\n\t\t\t\tcommand, checker = flds[:idx], flds[idx:]\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tvar cmd string\n\t\tif len(command) \u003e 0 {\n\t\t\tcmd = command[0]\n\n\t\t\t// there is a new command; if hasPanicChecker == false,\n\t\t\t// it means the previous command did not have a panic checker.\n\t\t\t// add it.\n\t\t\tif !hasPanicChecker \u0026\u0026 len(funcs) \u003e 0 {\n\t\t\t\t// no lineNum+1 because it was the previous line\n\t\t\t\tfuncs = append(funcs, panicChecker(lineNum, testName))\n\t\t\t}\n\t\t}\n\t\tswitch cmd {\n\t\tcase \"\": // move on\n\t\tcase \"newgame\":\n\t\t\tw, b := white, black\n\t\t\tvar seconds, incr int\n\t\t\tswitch len(command) {\n\t\t\tcase 1:\n\t\t\tcase 5:\n\t\t\t\tincr = atoi(command[4])\n\t\t\t\tfallthrough\n\t\t\tcase 4:\n\t\t\t\tseconds = atoi(command[3])\n\t\t\t\tfallthrough\n\t\t\tcase 3:\n\t\t\t\tw, b = address(\"g1\"+command[1]), address(\"g1\"+command[2])\n\t\t\tdefault:\n\t\t\t\tpanic(\"invalid newgame command \" + line)\n\t\t\t}\n\t\t\tfuncs = append(funcs,\n\t\t\t\t\u0026testCommandNewGame{w, b, seconds, incr},\n\t\t\t)\n\t\tcase \"move\":\n\t\t\tif len(command) != 3 {\n\t\t\t\tpanic(\"invalid move command \" + line)\n\t\t\t}\n\t\t\tif len(command[2]) \u003c 4 || len(command[2]) \u003e 5 {\n\t\t\t\tpanic(\"invalid lan move \" + command[2])\n\t\t\t}\n\t\t\tfrom, to := command[2][:2], command[2][2:4]\n\t\t\tvar promotion chess.Piece\n\t\t\tif len(command[2]) == 5 {\n\t\t\t\tpromotion = chess.PieceFromChar(command[2][4])\n\t\t\t\tif promotion == chess.PieceEmpty {\n\t\t\t\t\tpanic(\"invalid piece for promotion: \" + string(command[2][4]))\n\t\t\t\t}\n\t\t\t}\n\t\t\tfuncs = append(funcs, \u0026testCommandMove{\n\t\t\t\taddr:      address(\"g1\" + command[1]),\n\t\t\t\tfrom:      from,\n\t\t\t\tto:        to,\n\t\t\t\tpromotion: promotion,\n\t\t\t})\n\t\tcase \"abort\":\n\t\t\tfuncs = append(funcs, newTestCommandColorID(Abort, \"abort\", command[1]))\n\t\tcase \"draw\":\n\t\t\tfuncs = append(funcs, newTestCommandColorID(Draw, \"draw\", command[1]))\n\t\tcase \"drawoffer\":\n\t\t\tfuncs = append(funcs, newTestCommandColorID(DrawOffer, \"drawoffer\", command[1]))\n\t\tcase \"timeout\":\n\t\t\tfuncs = append(funcs, newTestCommandColorID(ClaimTimeout, \"timeout\", command[1]))\n\t\tcase \"resign\":\n\t\t\tfuncs = append(funcs, newTestCommandColorID(Resign, \"resign\", command[1]))\n\t\tcase \"game\":\n\t\t\tif len(command) \u003e 2 {\n\t\t\t\tpanic(\"invalid game command \" + line)\n\t\t\t}\n\t\t\ttc := \u0026testCommandGame{}\n\t\t\tif len(command) == 2 {\n\t\t\t\ttc.idWanted = command[1]\n\t\t\t}\n\t\t\tfuncs = append(funcs, tc)\n\t\tcase \"player\":\n\t\t\tif len(command) != 2 {\n\t\t\t\tpanic(\"invalid player command \" + line)\n\t\t\t}\n\t\t\tfuncs = append(funcs, \u0026testCommandPlayer{\"g1\" + command[1]})\n\t\tcase \"name\":\n\t\t\ttestName = strings.Join(command[1:], \" \")\n\t\tcase \"copy\":\n\t\t\tif len(command) \u003e 3 || len(command) \u003c 2 {\n\t\t\t\tpanic(\"invalid copy command \" + line)\n\t\t\t}\n\t\t\ttc := \u0026testCommandCopy{dst: command[1], src: \"result\"}\n\t\t\tif len(command) == 3 {\n\t\t\t\ttc.src = command[2]\n\t\t\t}\n\t\t\tfuncs = append(funcs, tc)\n\t\tcase \"sleep\":\n\t\t\tif len(command) != 2 {\n\t\t\t\tpanic(\"invalid sleep command \" + line)\n\t\t\t}\n\t\t\tfuncs = append(funcs, \u0026testCommandSleep{\n\t\t\t\ttime.Duration(atoi(command[1])) * time.Second,\n\t\t\t})\n\t\tdefault:\n\t\t\tpanic(\"invalid command \" + cmd)\n\t\t}\n\n\t\tif len(checker) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tif len(checker) == 1 {\n\t\t\tpanic(\"no checker specified \" + line)\n\t\t}\n\n\t\tbufp := checker[0]\n\t\tuseFatal := false\n\t\tif len(bufp) \u003e 1 \u0026\u0026 bufp[1] == '!' {\n\t\t\tbufp = bufp[2:]\n\t\t\tuseFatal = true\n\t\t} else {\n\t\t\tbufp = bufp[1:]\n\t\t}\n\t\tif bufp == \"\" {\n\t\t\tbufp = \"result\"\n\t\t}\n\t\tif bufp == \"panic\" \u0026\u0026 !hasPanicChecker {\n\t\t\thasPanicChecker = true\n\t\t}\n\t\ttf := func(ln int, testName string, useFatal bool) func(*testing.T, string, ...interface{}) {\n\t\t\treturn func(t *testing.T, s string, v ...interface{}) {\n\t\t\t\tfn := t.Errorf\n\t\t\t\tif useFatal {\n\t\t\t\t\tfn = t.Fatalf\n\t\t\t\t}\n\t\t\t\tfn(\"%s:%d: \"+s, append([]interface{}{testName, ln}, v...)...)\n\t\t\t}\n\t\t}(lineNum+1, testName, useFatal)\n\n\t\tswitch checker[1] {\n\t\tcase \"empty\":\n\t\t\tif len(checker) != 2 {\n\t\t\t\tpanic(\"invalid empty checker \" + line)\n\t\t\t}\n\t\t\tfuncs = append(funcs, \u0026testChecker{\n\t\t\t\tfn: func(t *testing.T, bufs map[string]string, tc *testChecker) {\n\t\t\t\t\tif bufs[tc.bufp] != \"\" {\n\t\t\t\t\t\ttc.tf(t, \"buffer %q is not empty (%v)\", tc.bufp, bufs[tc.bufp])\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\ttf:   tf,\n\t\t\t\tbufp: bufp,\n\t\t\t})\n\t\tcase \"equal\":\n\t\t\tpred := strings.Join(checker[2:], \" \")\n\t\t\tfuncs = append(funcs, \u0026testChecker{\n\t\t\t\tfn: func(t *testing.T, bufs map[string]string, tc *testChecker) {\n\t\t\t\t\texp := tc.preds[0]\n\t\t\t\t\tif exp[0] == '#' {\n\t\t\t\t\t\texp = bufs[exp[1:]]\n\t\t\t\t\t}\n\t\t\t\t\tif bufs[tc.bufp] != exp {\n\t\t\t\t\t\ttc.tf(t, \"buffer %q: want %v got %v\", tc.bufp, exp, bufs[tc.bufp])\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\ttf:    tf,\n\t\t\t\tbufp:  bufp,\n\t\t\t\tpreds: []string{pred},\n\t\t\t})\n\t\tcase \"contains\":\n\t\t\tpreds := checker[2:]\n\t\t\tif len(preds) == 0 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tfuncs = append(funcs, \u0026testChecker{\n\t\t\t\tfn: func(t *testing.T, bufs map[string]string, tc *testChecker) {\n\t\t\t\t\tfor _, pred := range tc.preds {\n\t\t\t\t\t\tif !strings.Contains(bufs[tc.bufp], pred) {\n\t\t\t\t\t\t\ttc.tf(t, \"buffer %q: %v does not contain %v\", tc.bufp, bufs[tc.bufp], pred)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\ttf:    tf,\n\t\t\t\tbufp:  bufp,\n\t\t\t\tpreds: preds,\n\t\t\t})\n\t\tcase \"containssp\":\n\t\t\tpred := strings.Join(checker[2:], \" \")\n\t\t\tif pred == \"\" {\n\t\t\t\tpanic(\"invalid contanssp checker \" + line)\n\t\t\t}\n\t\t\tfuncs = append(funcs, \u0026testChecker{\n\t\t\t\tfn: func(t *testing.T, bufs map[string]string, tc *testChecker) {\n\t\t\t\t\tif !strings.Contains(bufs[tc.bufp], tc.preds[0]) {\n\t\t\t\t\t\ttc.tf(t, \"buffer %q: %v does not contain %v\", tc.bufp, bufs[tc.bufp], tc.preds[0])\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\ttf:    tf,\n\t\t\t\tbufp:  bufp,\n\t\t\t\tpreds: []string{pred},\n\t\t\t})\n\t\tdefault:\n\t\t\tpanic(\"invalid checker \" + checker[1])\n\t\t}\n\t}\n\tif !hasPanicChecker {\n\t\tfuncs = append(funcs, panicChecker(len(lines), testName))\n\t}\n\treturn\n}\n\nfunc runCommandTest(cur realm, t *testing.T, command string) {\n\tfuncs, testName := parseCommandTest(t, command)\n\n\tt.Run(testName, func(cur realm, t *testing.T) {\n\t\tcleanup()\n\t\tbufs := make(map[string]string, 3)\n\t\tfor _, f := range funcs {\n\t\t\tif _, ok := f.(interface{ Checker() }); ok {\n\t\t\t\tf.Run(cross(cur), t, bufs)\n\t\t\t} else {\n\t\t\t\tcatchPanic(cur, f, t, bufs)\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc catchPanic(cur realm, tc testCommandRunner, t *testing.T, bufs map[string]string) {\n\t// XXX: should prefer testing.Recover, but see: https://github.com/gnolang/gno/issues/1650\n\te := revive(func() { tc.Run(cross(cur), t, bufs) })\n\tif e == nil {\n\t\tbufs[\"panic\"] = \"\"\n\t\treturn\n\t}\n\tbufs[\"result\"] = \"\"\n\tbufs[\"panic\"] = fmt.Sprint(e)\n}\n"},{"name":"discovery.gno","body":"// this file concerns mostly with \"discovery\"; ie. finding information\n// about a user's chess playing and previous games\n\npackage chess\n\nimport (\n\t\"bytes\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/morgan/chess/glicko2\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/r/sys/users\"\n)\n\ntype Category byte\n\nconst (\n\t// blanks are reserved for future use (bullet and classic)\n\t_ Category = iota\n\tBlitz\n\tRapid\n\t_\n\tCorrespondence\n\tCategoryMax\n)\n\nvar categoryString = [CategoryMax]string{\n\tBlitz:          \"blitz\",\n\tRapid:          \"rapid\",\n\tCorrespondence: \"correspondence\",\n}\n\nvar categoryList = [...]Category{Blitz, Rapid, Correspondence}\n\nfunc (c Category) String() string {\n\tif c \u003e CategoryMax || categoryString[c] == \"\" {\n\t\tpanic(\"invalid category\")\n\t}\n\treturn categoryString[c]\n}\n\nfunc CategoryFromString(s string) Category {\n\tfor i, cs := range categoryString {\n\t\tif s == cs {\n\t\t\treturn Category(i)\n\t\t}\n\t}\n\tpanic(\"invalid category\")\n}\n\nfunc (tc *TimeControl) Category() Category {\n\t// https://lichess.org/faq#time-controls\n\tif tc == nil {\n\t\treturn Correspondence\n\t}\n\n\ttotalTime := tc.Seconds + tc.Increment*40\n\tswitch {\n\tcase tc.Seconds \u003c= 0 || tc.Increment \u003c 0:\n\t\t// should not happen\n\t\treturn Correspondence\n\tcase totalTime \u003c 60*8:\n\t\treturn Blitz\n\tdefault:\n\t\treturn Rapid\n\t}\n}\n\n// realm state\nvar (\n\tplayerStore   avl.Tree // address -\u003e *Player\n\tleaderboard   [CategoryMax]leaderboardType\n\tplayerRatings [CategoryMax][]*glicko2.PlayerRating\n)\n\nfunc GetPlayer(cur realm, player string) string {\n\taddr := parsePlayer(0, cur, player)\n\tv := playerStore.Get(addr.String())\n\tif v == nil {\n\t\tpanic(\"player not found\")\n\t}\n\tb, err := v.(*Player).MarshalJSON()\n\tcheckErr(err)\n\treturn string(b)\n}\n\n// Player contains game-related player information.\ntype Player struct {\n\tAddress      address\n\tCategoryInfo [CategoryMax]CategoryInfo\n}\n\ntype CategoryInfo struct {\n\tWins, Losses, Draws int\n\t*glicko2.PlayerRating\n}\n\n// Score for determining leaderboards.\nfunc (p Player) Score(cat Category) float64 {\n\treturn p.CategoryInfo[cat].Rating\n}\n\n// Leaderboard position, 0 indexed.\n// Dynamically calculated to avoid having to shift positions when LB changes.\nfunc (p Player) LeaderboardPosition(cat Category) int {\n\tpos, ok := leaderboard[cat].find(p.Score(cat), p.Address)\n\tif !ok {\n\t\treturn -1\n\t}\n\treturn pos\n}\n\nfunc (g *Game) saveResult() {\n\tw, b := getPlayer(g.White), getPlayer(g.Black)\n\n\tcat := g.Time.Category()\n\n\t// Get numeric result for glicko2.\n\tvar result float64\n\tswitch g.Winner {\n\tcase WinnerWhite:\n\t\tw.CategoryInfo[cat].Wins++\n\t\tb.CategoryInfo[cat].Losses++\n\t\tresult = 1\n\tcase WinnerBlack:\n\t\tw.CategoryInfo[cat].Losses++\n\t\tb.CategoryInfo[cat].Wins++\n\t\tresult = 0\n\tcase WinnerDraw:\n\t\tw.CategoryInfo[cat].Draws++\n\t\tb.CategoryInfo[cat].Draws++\n\t\tresult = 0.5\n\tdefault:\n\t\treturn // TODO: maybe panic\n\t}\n\n\t// Call glicko 2 rating calculator.\n\towr, obr := w.CategoryInfo[cat].Rating, b.CategoryInfo[cat].Rating\n\tglicko2.UpdateRatings(playerRatings[cat], []glicko2.RatingScore{{\n\t\tWhite: g.White,\n\t\tBlack: g.Black,\n\t\tScore: result,\n\t}})\n\n\t// Save in playerStore.\n\tplayerStore.Set(w.Address.String(), w)\n\tplayerStore.Set(b.Address.String(), b)\n\tleaderboard[cat], _ = leaderboard[cat].push(g.White, owr, w.CategoryInfo[cat].Rating)\n\tleaderboard[cat], _ = leaderboard[cat].push(g.Black, obr, b.CategoryInfo[cat].Rating)\n}\n\nfunc getPlayer(addr address) *Player {\n\tpraw := playerStore.Get(addr.String())\n\tif praw != nil {\n\t\treturn praw.(*Player)\n\t}\n\tp := new(Player)\n\tp.Address = addr\n\tfor _, cat := range categoryList {\n\t\tpr := glicko2.NewPlayerRating(addr)\n\t\tp.CategoryInfo[cat] = CategoryInfo{\n\t\t\tPlayerRating: pr,\n\t\t}\n\t\tplayerRatings[cat] = append(playerRatings[cat], pr)\n\t}\n\tplayerStore.Set(addr.String(), p)\n\treturn p\n}\n\ntype lbEntry struct {\n\taddr  address\n\tscore float64\n}\n\ntype leaderboardType []lbEntry\n\n// find performs binary search on leaderboard to find the first\n// position where score appears, or anything lesser than it.\n// Additionally, if addr is given, it finds the position where the given address appears.\n// The second return parameter returns whether the address was found.\n//\n// The index will be 0 if the score is higher than any other on the leaderboard,\n// and len(leaderboards) if it is lower than any other.\nfunc (lb leaderboardType) find(score float64, addr address) (int, bool) {\n\ti := sort.Search(len(lb), func(i int) bool {\n\t\treturn lb[i].score \u003c= score\n\t})\n\n\tif addr == \"\" || i == len(lb) {\n\t\treturn i, false\n\t}\n\n\tfor j := 0; i+j \u003c len(lb) \u0026\u0026 lb[i+j].score == score; j++ {\n\t\tif lb[i+j].addr == addr {\n\t\t\treturn i + j, true\n\t\t}\n\t}\n\n\treturn i, false\n}\n\n// push adds or modifies the player's position in the leaderboard.\n// the new leaderboard, and the new position of the player in the leaderboard is returned (0-indexed)\nfunc (lb leaderboardType) push(player address, oldScore, newScore float64) (leaderboardType, int) {\n\t// determine where the player is, currently\n\toldPos, found := lb.find(oldScore, player)\n\tif found \u0026\u0026 (oldScore == newScore) {\n\t\treturn lb, oldPos\n\t}\n\n\t// determine where to place the player next.\n\tnewPos, _ := lb.find(newScore, \"\")\n\n\tvar n leaderboardType\n\tswitch {\n\tcase !found:\n\t\tn = append(leaderboardType{}, lb[:newPos]...)\n\t\tn = append(n, lbEntry{player, newScore})\n\t\tn = append(n, lb[newPos:]...)\n\n\tcase oldPos == newPos:\n\t\tn = lb\n\t\tn[newPos] = lbEntry{player, newScore}\n\tcase oldPos \u003e newPos:\n\t\tn = append(leaderboardType{}, lb[:newPos]...)\n\t\tn = append(n, lbEntry{player, newScore})\n\t\tn = append(n, lb[newPos:oldPos]...)\n\t\tn = append(n, lb[oldPos+1:]...)\n\tdefault: // oldPos \u003c newPos\n\t\tn = append(leaderboardType{}, lb[:oldPos]...)\n\t\tn = append(n, lb[oldPos+1:newPos]...)\n\t\tn = append(n, lbEntry{player, newScore})\n\t\tn = append(n, lb[newPos:]...)\n\t}\n\treturn n, newPos\n}\n\n// Leaderboard returns a list of all users, ordered by their position in the leaderboard.\n// category is one of blitz, rapid or correspondence.\nfunc Leaderboard(category string) string {\n\tcat := CategoryFromString(category)\n\tvar buf bytes.Buffer\n\tbuf.WriteByte('[')\n\tfor idx, entry := range leaderboard[cat] {\n\t\tp := playerStore.Get(entry.addr.String())\n\t\td, err := p.(*Player).MarshalJSON()\n\t\tcheckErr(err)\n\t\tbuf.Write(d)\n\t\tif idx != len(leaderboard[cat])-1 {\n\t\t\tbuf.WriteByte(',')\n\t\t}\n\t}\n\tbuf.WriteByte(']')\n\treturn buf.String()\n}\n\n// ListGames provides game listing functionality, with filter-based search functionality.\n//\n// available filters:\n//\n//\tplayer:\u003cplayer\u003e white:\u003cplayer\u003e black:\u003cplayer\u003e finished:bool\n//\tlimit:int id\u003ccmp\u003eint sort:asc/desc\n//\t\u003ccmp\u003e: '\u003c' or '\u003e'\n//\t\u003cplayer\u003e: either a bech32 address, \"@user\" (r/demo/users), or \"caller\"\nfunc ListGames(cur realm, filters string) string {\n\tft := parseFilters(0, cur, filters)\n\tresults := make([]*Game, 0, ft.limit)\n\tcb := func(g *Game) (stop bool) {\n\t\tif !ft.valid(g) {\n\t\t\treturn false\n\t\t}\n\t\tresults = append(results, g)\n\t\treturn len(results) \u003e= ft.limit\n\t}\n\n\t// iterate over user2games array if we have one;\n\t// if we don't, iterate over games.\n\tif ft.u2gAddr != \"\" {\n\t\tv := user2Games.Get(ft.u2gAddr.String())\n\t\tif v == nil {\n\t\t\treturn \"[]\"\n\t\t}\n\t\tgames := v.([]*Game)\n\t\tif ft.reverse {\n\t\t\tfor i := len(games) - 1; i \u003e= 0; i-- {\n\t\t\t\tif cb(games[i]) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tfor _, game := range games {\n\t\t\t\tif cb(game) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfn := gameStore.Iterate\n\t\tif ft.reverse {\n\t\t\tfn = gameStore.ReverseIterate\n\t\t}\n\t\tfn(ft.minID, ft.maxID, func(_ string, v interface{}) bool {\n\t\t\treturn cb(v.(*Game))\n\t\t})\n\t}\n\n\t// fast path: no results\n\tif len(results) == 0 {\n\t\treturn \"[]\"\n\t}\n\n\t// encode json\n\tvar buf bytes.Buffer\n\tbuf.WriteByte('[')\n\tfor idx, g := range results {\n\t\tbuf.WriteString(g.json())\n\t\tif idx != len(results)-1 {\n\t\t\tbuf.WriteByte(',')\n\t\t}\n\t}\n\tbuf.WriteByte(']')\n\n\treturn buf.String()\n}\n\ntype listGamesFilters struct {\n\tfilters []func(*Game) bool\n\tu2gAddr address\n\tmaxID   string\n\tminID   string\n\tlimit   int\n\treverse bool\n}\n\nfunc (l *listGamesFilters) valid(game *Game) bool {\n\tfor _, filt := range l.filters {\n\t\tif !filt(game) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// parseFilters parses a filter spec string into a listGamesFilters.\n// callerRlm must be the caller's own live cur; it's threaded into\n// parsePlayer for the \"caller\" / \"@user\" / addr resolution sub-cases.\n// IsCurrent guard mirrors parsePlayer's (Class-2 defense).\nfunc parseFilters(_ int, callerRlm realm, filters string) (r listGamesFilters) {\n\tif !callerRlm.IsCurrent() {\n\t\tpanic(\"unauthorized: rlm is not the caller's live cur\")\n\t}\n\t// default to desc order\n\tr.reverse = true\n\n\tparts := strings.Fields(filters)\n\tfor _, part := range parts {\n\t\tidx := strings.IndexAny(part, \":\u003c\u003e\")\n\t\tif idx \u003c 0 {\n\t\t\tpanic(\"invalid filter: \" + part)\n\t\t}\n\t\tfilt, pred := part[:idx+1], part[idx+1:]\n\t\tswitch filt {\n\t\tcase \"player:\":\n\t\t\ta := parsePlayer(0, callerRlm, pred)\n\t\t\tr.filters = append(r.filters, func(g *Game) bool { return g.White == a || g.Black == a })\n\t\t\tif r.u2gAddr == \"\" {\n\t\t\t\tr.u2gAddr = a\n\t\t\t}\n\t\tcase \"white:\":\n\t\t\ta := parsePlayer(0, callerRlm, pred)\n\t\t\tr.filters = append(r.filters, func(g *Game) bool { return g.White == a })\n\t\t\tif r.u2gAddr == \"\" {\n\t\t\t\tr.u2gAddr = a\n\t\t\t}\n\t\tcase \"black:\":\n\t\t\ta := parsePlayer(0, callerRlm, pred)\n\t\t\tr.filters = append(r.filters, func(g *Game) bool { return g.Black == a })\n\t\t\tif r.u2gAddr == \"\" {\n\t\t\t\tr.u2gAddr = a\n\t\t\t}\n\t\tcase \"finished:\":\n\t\t\tb := parseBool(pred)\n\t\t\tr.filters = append(r.filters, func(g *Game) bool { return g.State.IsFinished() == b })\n\t\tcase \"id\u003c\":\n\t\t\tr.maxID = pred\n\t\tcase \"id\u003e\":\n\t\t\tr.minID = pred\n\t\tcase \"limit:\":\n\t\t\tn, err := strconv.Atoi(pred)\n\t\t\tcheckErr(err)\n\t\t\tr.limit = n\n\t\tcase \"sort:\":\n\t\t\tr.reverse = pred == \"desc\"\n\t\tdefault:\n\t\t\tpanic(\"invalid filter: \" + filt)\n\t\t}\n\t}\n\treturn\n}\n\nfunc parseBool(s string) bool {\n\tswitch s {\n\tcase \"true\", \"True\", \"TRUE\", \"1\":\n\t\treturn true\n\tcase \"false\", \"False\", \"FALSE\", \"0\":\n\t\treturn false\n\t}\n\tpanic(\"invalid bool \" + s)\n}\n\n// parsePlayer resolves a player spec — a bech32 address, \"@user\" (r/sys/users\n// alias), or \"caller\" — to an address. callerRlm must be the caller's\n// own live cur; \"caller\" yields callerRlm.Previous().Address() (the\n// realm that called the exported API).\n//\n// SECURITY: the IsCurrent gate closes Class-2 designation forgery —\n// without it, a hostile caller could pass a stashed realm value whose\n// .Previous().Address() resolves to a victim's address and have\n// parsePlayer return it. See docs/resources/gno-security.md.\nfunc parsePlayer(_ int, callerRlm realm, s string) address {\n\tif !callerRlm.IsCurrent() {\n\t\tpanic(\"unauthorized: rlm is not the caller's live cur\")\n\t}\n\tswitch {\n\tcase s == \"\":\n\t\tpanic(\"invalid address/user\")\n\tcase s == \"caller\":\n\t\treturn callerRlm.Previous().Address()\n\tcase s[0] == '@':\n\t\tu, _ := users.ResolveName(s[1:])\n\t\tif u == nil {\n\t\t\tpanic(\"user not found: \" + s[1:])\n\t\t}\n\t\treturn u.Addr()\n\tcase s[0] == 'g':\n\t\treturn address(s)\n\tdefault:\n\t\tpanic(\"invalid address/user: \" + s)\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/morgan/chess\"\ngno = \"0.9\"\n"},{"name":"json.gno","body":"package chess\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"gno.land/p/morgan/chess\"\n\t\"gno.land/p/morgan/chess/glicko2\"\n\t\"gno.land/r/sys/users\"\n)\n\n// This file contains a bunch of JSON marshalers.\n// These should disappear eventually!\n// https://github.com/gnolang/gno/issues/1655\n\nfunc (g Game) MarshalJSON() (retBytes []byte, err error) {\n\tvar b bytes.Buffer\n\tb.WriteByte('{')\n\n\tnilAddr := func(na *address) string {\n\t\tif na == nil {\n\t\t\treturn `null`\n\t\t}\n\t\treturn `\"` + na.String() + `\"`\n\t}\n\tmjson := func(s string, val interface{ MarshalJSON() ([]byte, error) }, comma bool) {\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\tvar res []byte\n\t\tres, err = val.MarshalJSON()\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\tb.WriteString(`\"` + s + `\":`)\n\t\tb.Write(res)\n\t\tif comma {\n\t\t\tb.WriteByte(',')\n\t\t}\n\t}\n\n\tb.WriteString(`\"id\":\"` + g.ID + `\",`)\n\tb.WriteString(`\"white\":\"` + g.White.String() + `\",`)\n\tb.WriteString(`\"black\":\"` + g.Black.String() + `\",`)\n\n\tmjson(\"position\", jsonPosition{g.Position}, true)\n\tmjson(\"state\", g.State, true)\n\tmjson(\"winner\", g.Winner, true)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tb.WriteString(`\"creator\":\"` + g.Creator.String() + `\",`)\n\tb.WriteString(`\"created_at\":\"` + g.CreatedAt.Format(time.RFC3339) + `\",`)\n\tb.WriteString(`\"draw_offerer\":` + nilAddr(g.DrawOfferer) + \",\")\n\tb.WriteString(`\"concluder\":` + nilAddr(g.Concluder) + \",\")\n\n\tmjson(\"time\", g.Time, false)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tb.WriteByte('}')\n\treturn b.Bytes(), nil\n}\n\ntype jsonPosition struct {\n\tchess.Position\n}\n\nfunc (p jsonPosition) MarshalJSON() ([]byte, error) {\n\tvar b bytes.Buffer\n\tb.WriteByte('{')\n\n\tbfen := p.EncodeFEN()\n\tb.WriteString(`\"fen\":\"` + bfen + `\",`)\n\n\tb.WriteString(`\"moves\":[`)\n\n\tfor idx, m := range p.Moves {\n\t\tb.WriteString(`\"` + m.String() + `\"`)\n\t\tif idx != len(p.Moves)-1 {\n\t\t\tb.WriteByte(',')\n\t\t}\n\t}\n\n\tb.WriteByte(']')\n\tb.WriteByte('}')\n\treturn b.Bytes(), nil\n}\n\nfunc (w Winner) MarshalJSON() ([]byte, error) {\n\tif n := int(w); n \u003c len(winnerString) {\n\t\treturn []byte(`\"` + winnerString[n] + `\"`), nil\n\t}\n\treturn nil, errors.New(\"invalid winner value\")\n}\n\nfunc (g GameState) MarshalJSON() ([]byte, error) {\n\tif int(g) \u003e= len(gameStatesSnake) {\n\t\treturn nil, errors.New(\"invalid game state\")\n\t}\n\treturn []byte(`\"` + gameStatesSnake[g] + `\"`), nil\n}\n\nfunc (tc *TimeControl) MarshalJSON() ([]byte, error) {\n\tif tc == nil {\n\t\treturn []byte(\"null\"), nil\n\t}\n\tvar buf bytes.Buffer\n\n\tbuf.WriteByte('{')\n\tbuf.WriteString(`\"seconds\":` + strconv.Itoa(tc.Seconds) + `,`)\n\tbuf.WriteString(`\"increment\":` + strconv.Itoa(tc.Increment) + `,`)\n\tbuf.WriteString(`\"started_at\":\"` + tc.StartedAt.Format(time.RFC3339) + `\",`)\n\n\tbuf.WriteString(`\"move_timestamps\":[`)\n\tfor idx, mt := range tc.MoveTimestamps {\n\t\tbuf.WriteString(`\"` + mt.Format(time.RFC3339) + `\"`)\n\t\tif idx != len(tc.MoveTimestamps)-1 {\n\t\t\tbuf.WriteByte(',')\n\t\t}\n\t}\n\tbuf.WriteString(\"],\")\n\n\tbuf.WriteString(`\"white_time\":` + strconv.FormatInt(tc.WhiteTime.Milliseconds(), 10) + \",\")\n\tbuf.WriteString(`\"black_time\":` + strconv.FormatInt(tc.BlackTime.Milliseconds(), 10))\n\tbuf.WriteByte('}')\n\n\treturn buf.Bytes(), nil\n}\n\nfunc (p Player) MarshalJSON() ([]byte, error) {\n\tu := users.ResolveAddress(p.Address)\n\n\tvar buf bytes.Buffer\n\tbuf.WriteByte('{')\n\n\tbuf.WriteString(`\"address\":\"` + p.Address.String() + `\",`)\n\tif u == nil {\n\t\tbuf.WriteString(`\"username\":\"\",`)\n\t} else {\n\t\tbuf.WriteString(`\"username\":\"` + u.Name() + `\",`)\n\t}\n\n\tfor idx, cat := range categoryList {\n\t\tstat := p.CategoryInfo[cat]\n\t\tbuf.WriteString(`\"` + cat.String() + `\":{`)\n\t\tbuf.WriteString(`\"wins\":` + strconv.Itoa(stat.Wins) + \",\")\n\t\tbuf.WriteString(`\"losses\":` + strconv.Itoa(stat.Losses) + \",\")\n\t\tbuf.WriteString(`\"draws\":` + strconv.Itoa(stat.Draws) + \",\")\n\t\tbuf.WriteString(`\"rating\":`)\n\t\tif res, err := (jsonPlayerRating{stat.PlayerRating}).MarshalJSON(); err != nil {\n\t\t\treturn nil, err\n\t\t} else {\n\t\t\tbuf.Write(res)\n\t\t}\n\t\tbuf.WriteByte(',')\n\t\tbuf.WriteString(`\"position\":` + strconv.Itoa(p.LeaderboardPosition(cat)))\n\t\tbuf.WriteByte('}')\n\t\tif idx != len(categoryList)-1 {\n\t\t\tbuf.WriteByte(',')\n\t\t}\n\t}\n\n\tbuf.WriteByte('}')\n\treturn buf.Bytes(), nil\n}\n\ntype jsonPlayerRating struct{ *glicko2.PlayerRating }\n\nfunc (p jsonPlayerRating) MarshalJSON() ([]byte, error) {\n\tvar buf bytes.Buffer\n\tbuf.WriteByte('{')\n\tbuf.WriteString(`\"rating\":` + strconv.FormatFloat(p.Rating, 'f', 5, 64) + `,`)\n\tbuf.WriteString(`\"deviation\":` + strconv.FormatFloat(p.RatingDeviation, 'f', 5, 64) + `,`)\n\tbuf.WriteString(`\"volatility\":` + strconv.FormatFloat(p.RatingVolatility, 'f', 5, 64))\n\tbuf.WriteByte('}')\n\treturn buf.Bytes(), nil\n}\n"},{"name":"lobby.gno","body":"package chess\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/nt/avl/v0\"\n)\n\ntype lobbyPlayer struct {\n\tjoinedAt time.Time\n\tseenAt   time.Time\n\tplayer   *Player\n}\n\nfunc (l lobbyPlayer) r(cat Category) float64 { return l.player.CategoryInfo[cat].Rating }\n\n// returns whether the rating r is within l's current \"acceptable\" range.\nfunc (l lobbyPlayer) acceptRating(cat Category, r float64) bool {\n\tdelta := int(time.Since(l.joinedAt) / time.Second)\n\tif delta \u003e= 10 {\n\t\treturn true\n\t}\n\n\thalfsize := bracketSize[delta] / 2\n\trat := l.r(cat)\n\treturn r \u003e= (rat-halfsize) \u0026\u0026 r \u003c= (rat+halfsize)\n}\n\ntype tcLobby byte\n\n// time control lobby divisions. N+M -\u003e tcLobbyNpM\nconst (\n\ttcLobby5p0 tcLobby = iota\n\ttcLobby10p5\n\ttcLobbyMax\n)\n\nfunc (tc tcLobby) Category() Category {\n\tswitch tc {\n\tcase tcLobby5p0:\n\t\treturn Blitz\n\tdefault:\n\t\treturn Rapid\n\t}\n}\n\nfunc (tc tcLobby) Time() (secs, incr int) {\n\tswitch tc {\n\tcase tcLobby5p0:\n\t\treturn 60 * 5, 0\n\tcase tcLobby10p5:\n\t\treturn 60 * 10, 5\n\tdefault:\n\t\tpanic(\"invalid tc value\")\n\t}\n}\n\nvar (\n\tlobby            [tcLobbyMax][]lobbyPlayer\n\tlobbyPlayer2Game avl.Tree // player addr -\u003e *Game. set after a user is matched; reset when joining again.\n)\n\nfunc LobbyJoin(cur realm, seconds, increment int) {\n\tassertOriginCall(0, cur)\n\tvar tc tcLobby\n\tswitch {\n\tcase seconds == (60*5) \u0026\u0026 increment == 0:\n\t\ttc = tcLobby5p0\n\tcase seconds == (60*10) \u0026\u0026 increment == 5:\n\t\ttc = tcLobby10p5\n\tdefault:\n\t\tpanic(\"can only use time controls 5+0 or 10+5\")\n\t}\n\n\t// Ensure that user's previous games are finished (or timed out).\n\tcaller := cur.Previous().Address()\n\n\tgames := getUserGames(caller)\n\t// XXX: temporary to avoid ever prohibiting a user from joining the lobby.\n\t// when possible, change back to assertGamesFinished(games)\n\tfor _, g := range games {\n\t\tif !g.State.IsFinished() {\n\t\t\tif err := resign(0, cur, g); err != nil {\n\t\t\t\tpanic(\"internal error (could not resign game \" + g.ID + \"): \" + err.Error())\n\t\t\t}\n\t\t}\n\t}\n\tassertUserNotInLobby(caller)\n\n\t// remove caller from lobbyPlayer2Game, so LobbyGameFound\n\t// returns the right value.\n\tlobbyPlayer2Game.Remove(caller.String())\n\n\tnow := time.Now()\n\tlobby[tc] = append(lobby[tc], lobbyPlayer{joinedAt: now, seenAt: now, player: getPlayer(caller)})\n\trefreshLobby(0, cur, tc)\n}\n\nfunc assertUserNotInLobby(caller address) {\n\tfor _, sublob := range lobby {\n\t\tfor _, pl := range sublob {\n\t\t\tif pl.player.Address == caller {\n\t\t\t\tpanic(\"you are already in the lobby\")\n\t\t\t}\n\t\t}\n\t}\n}\n\n// refreshLobby serves to run through the lobby, kick timed out users, and see if any users\n// can be matched with the current user.\nfunc refreshLobby(_ int, rlm realm, tc tcLobby) {\n\tif !rlm.IsCurrent() {\n\t\tpanic(\"unauthorized: rlm is not the caller's live cur\")\n\t}\n\tcallerAddr := rlm.Previous().Address()\n\tnow := time.Now()\n\tfor idx, player := range lobby[tc] {\n\t\tif player.player.Address == callerAddr {\n\t\t\t// mark player as seen now\n\t\t\tlobby[tc][idx].seenAt = now\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// lobby housekeeping: kick any player that hasn't contacted us for the\n\t// past 30 seconds.\n\t// do this BEFORE matching the caller, as we want to give them someone who\n\t// is seemingly active in the lobby.\n\tfor i := 0; i \u003c len(lobby[tc]); i++ {\n\t\tif now.Sub(lobby[tc][i].seenAt) \u003e= time.Second*30 {\n\t\t\tnewLobby := append([]lobbyPlayer{}, lobby[tc][:i]...)\n\t\t\tlobby[tc] = append(newLobby, lobby[tc][i+1:]...)\n\t\t\ti--\n\t\t}\n\t}\n\n\t// determine sub lobby\n\tsublob := lobby[tc]\n\n\tcallerPos := -1\n\tvar caller lobbyPlayer\n\tfor idx, player := range sublob {\n\t\tif player.player.Address == callerAddr {\n\t\t\tcallerPos = idx\n\t\t\tcaller = player\n\t\t\tbreak\n\t\t}\n\t}\n\t// caller is not involved in lobby, or lobby only contains the player\n\tif callerPos \u003c 0 || len(sublob) \u003c 2 {\n\t\treturn\n\t}\n\n\tcat := tc.Category()\n\tcallerRating := caller.r(cat)\n\tcallerForce := now.Sub(caller.joinedAt) \u003e= time.Second*10\n\n\tfor i, player := range sublob {\n\t\tif i == callerPos {\n\t\t\tcontinue\n\t\t}\n\t\t// force if either the caller or the player have been waiting for more than 10s.\n\t\tforce := callerForce || (now.Sub(player.joinedAt) \u003e= time.Second*10)\n\t\t// find player whose rating falls in each other's range.\n\t\tif force || (caller.acceptRating(cat, player.r(cat)) \u0026\u0026\n\t\t\tplayer.acceptRating(cat, callerRating)) {\n\t\t\tlobbyMatch(tc, callerPos, i)\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc lobbyMatch(tc tcLobby, p1, p2 int) {\n\t// Get the two players, create a new game with them.\n\tsecs, incr := tc.Time()\n\ta1, a2 := lobby[tc][p1].player.Address, lobby[tc][p2].player.Address\n\n\tgame := newGame(a1, a2, secs, incr)\n\n\t// remove p1 and p2 from lobby\n\tif p1 \u003e p2 {\n\t\tp1, p2 = p2, p1\n\t}\n\tnl := append([]lobbyPlayer{}, lobby[tc][:p1]...)\n\tnl = append(nl, lobby[tc][p1+1:p2]...)\n\tnl = append(nl, lobby[tc][p2+1:]...)\n\tlobby[tc] = nl\n\n\t// add to lobbyPlayer2Game\n\tlobbyPlayer2Game.Set(a1.String(), game)\n\tlobbyPlayer2Game.Set(a2.String(), game)\n}\n\n/*\ngenerated by python code:\n\n\tfor i in range(0,10):\n\t\tprint((i+1)**(3.694692926)+49, ',')\n\nrationale: give brackets in an exponential range between\n50 and 5000, dividing it into 10 steps.\n\"magic constant\" obtained solving for in c in the equation:\n\n\t5000=(x+1)^c+49 (x = steps, 10 in our case)\n\nwhich comes out to be ln(delta)/ln(steps), delta = 5000-49, steps = 10.\n*/\nvar bracketSize = [...]float64{\n\t50.0,\n\t61.9483191543645,\n\t106.91839582826664,\n\t216.65896892328266,\n\t431.3662312611604,\n\t798.94587409321,\n\t1374.4939512498888,\n\t2219.9018387103433,\n\t3403.5405753197747,\n\t5000,\n}\n\nfunc LobbyGameFound(cur realm) string {\n\trefreshLobby(0, cur, tcLobby5p0)\n\trefreshLobby(0, cur, tcLobby10p5)\n\n\tval := lobbyPlayer2Game.Get(cur.Previous().Address().String())\n\tif val == nil {\n\t\treturn \"null\"\n\t}\n\treturn val.(*Game).json()\n}\n\nfunc LobbyQuit(cur realm) {\n\tcaller := cur.Previous().Address()\n\tfor tc, sublob := range lobby {\n\t\tfor i, pl := range sublob {\n\t\t\tif pl.player.Address == caller {\n\t\t\t\tnewLobby := append([]lobbyPlayer{}, sublob[:i]...)\n\t\t\t\tnewLobby = append(newLobby, sublob[i+1:]...)\n\t\t\t\tlobby[tc] = newLobby\n\t\t\t\tlobbyPlayer2Game.Remove(caller.String())\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\n\tpanic(\"you are not in the lobby\")\n}\n"},{"name":"lobby_test.gno","body":"package chess\n\nimport (\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/morgan/chess/glicko2\"\n)\n\nfunc TestLobbyJoin(cur realm, t *testing.T) {\n\tcleanup()\n\ttesting.SetRealm(testing.NewUserRealm(white))\n\tLobbyJoin(cross(cur), 10*60, 5)\n\tos.Sleep(time.Second * 5)\n\ttesting.SetRealm(testing.NewUserRealm(black))\n\tLobbyJoin(cross(cur), 10*60, 5)\n\tres := LobbyGameFound(cross(cur))\n\tif res == \"null\" {\n\t\tt.Errorf(\"LobbyGameFound is null\")\n\t}\n}\n\nfunc sublobbyToIDs(pl []lobbyPlayer) []string {\n\ts := make([]string, len(pl))\n\tfor idx, p := range pl {\n\t\ts[idx] = string(p.player.Address)\n\t}\n\treturn s\n}\n\nfunc TestLobbyGameFound(cur realm, t *testing.T) {\n\tcheck := func(checker ...func(t *testing.T)) func(t *testing.T) {\n\t\treturn func(t *testing.T) {\n\t\t\tfor _, ck := range checker {\n\t\t\t\tck(t)\n\t\t\t}\n\t\t}\n\t}\n\tids := func(ids ...address) func(t *testing.T) {\n\t\treturn func(t *testing.T) {\n\t\t\tif len(ids) != len(lobby[0]) {\n\t\t\t\tt.Errorf(\"lobby doesn't match expected ids: lobby: %v, newIDs: %v\", sublobbyToIDs(lobby[0]), ids)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfor idx, i := range ids {\n\t\t\t\tif pa := lobby[0][idx].player.Address; pa != i {\n\t\t\t\t\tt.Errorf(\"check pos %d: player id doesnt match (got %q want %q)\", idx, pa, i)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tnumGames := func(n int) func(t *testing.T) {\n\t\treturn func(t *testing.T) {\n\t\t\tl := gameStore.Size()\n\t\t\tif l != n {\n\t\t\t\tt.Errorf(\"invalid gameStore size; want %d got %d\", n, l)\n\t\t\t}\n\t\t}\n\t}\n\n\ttype pl struct {\n\t\tid     address\n\t\trating float64\n\t\t// use negative values here to indicate how many seconds in the past;\n\t\t// ie: joinedAt: -1, means player joined 1 second ago.\n\t\tjoinedAt int\n\t\tseenAt   int\n\t}\n\ttt := []struct {\n\t\tname   string\n\t\tpre    []pl\n\t\tcaller address\n\t\tcheck  func(t *testing.T)\n\t}{\n\t\t{\n\t\t\t\"equalRating\",\n\t\t\t[]pl{{\"1\", 1200, -1, -1}, {\"2\", 1200, 0, 0}},\n\t\t\t\"1\",\n\t\t\tcheck(ids(), numGames(1)),\n\t\t},\n\t\t{\n\t\t\t\"minimumApart\", // delta \u003c= 25\n\t\t\t[]pl{{\"1\", 1200, 0, 0}, {\"2\", 1225, 0, 0}},\n\t\t\t\"2\",\n\t\t\tcheck(ids(), numGames(1)),\n\t\t},\n\t\t{\n\t\t\t\"tooFarApart\", // delta \u003e 25\n\t\t\t[]pl{{\"1\", 1200, 0, 0}, {\"2\", 1230, 0, 0}},\n\t\t\t\"2\",\n\t\t\tcheck(ids(\"1\", \"2\"), numGames(0)),\n\t\t},\n\t\t{\n\t\t\t\"oldHighPriority\",\n\t\t\t// kicked hasn't been seen in too long, so should not be considered.\n\t\t\t// 1 is active and has been looking for 30s, so it gets priority, even if 2-3 is\n\t\t\t// a closer match.\n\t\t\t[]pl{{\"kicked\", 1800, -60, -50}, {\"1\", 1900, -30, -10}, {\"2\", 1400, 0, 0}, {\"3\", 1420, 0, 0}},\n\t\t\t\"3\",\n\t\t\tcheck(ids(\"2\"), numGames(1)),\n\t\t},\n\t\t{\n\t\t\t\"oldHighPriority2\",\n\t\t\t[]pl{{\"comeback\", 1800, -60, -50}, {\"1\", 1900, -30, -10}, {\"2\", 1400, 0, 0}, {\"3\", 1420, 0, 0}},\n\t\t\t// same as last one, except the player who was kicked last time, because\n\t\t\t// he's the caller, has their seenAt set back to the current time, so they're matched with 1.\n\t\t\t\"comeback\",\n\t\t\tcheck(ids(\"2\", \"3\"), numGames(1)),\n\t\t},\n\t\t{\n\t\t\t\"alone\",\n\t\t\t[]pl{{\"1\", 1200, 0, 0}},\n\t\t\t\"1\",\n\t\t\tcheck(ids(\"1\"), numGames(0)),\n\t\t},\n\t\t{\n\t\t\t\"brackFail\",\n\t\t\t[]pl{{\"1\", 1200, -4, -4}, {\"2\", 1450, -5, -5}},\n\t\t\t\"1\",\n\t\t\tcheck(ids(\"1\", \"2\"), numGames(0)),\n\t\t},\n\t\t{\n\t\t\t\"brackFail2\",\n\t\t\t[]pl{{\"1\", 1200, -5, -5}, {\"2\", 1450, -4, -4}},\n\t\t\t\"1\",\n\t\t\tcheck(ids(\"1\", \"2\"), numGames(0)),\n\t\t},\n\t\t{\n\t\t\t\"brackSuccess\",\n\t\t\t[]pl{{\"1\", 1200, -5, -5}, {\"2\", 1450, -5, -5}},\n\t\t\t\"1\",\n\t\t\tcheck(ids(), numGames(1)),\n\t\t},\n\t}\n\n\tfor _, tc := range tt {\n\t\tt.Run(tc.name, func(cur realm, t *testing.T) {\n\t\t\tcleanup()\n\t\t\tnow := time.Now()\n\t\t\tfor _, p := range tc.pre {\n\t\t\t\tlobby[0] = append(lobby[0], lobbyPlayer{\n\t\t\t\t\tjoinedAt: now.Add(time.Duration(p.joinedAt) * time.Second),\n\t\t\t\t\tseenAt:   now.Add(time.Duration(p.seenAt) * time.Second),\n\t\t\t\t\tplayer: \u0026Player{\n\t\t\t\t\t\tAddress: p.id,\n\t\t\t\t\t\tCategoryInfo: [CategoryMax]CategoryInfo{\n\t\t\t\t\t\t\tBlitz: {PlayerRating: \u0026glicko2.PlayerRating{Rating: p.rating}},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\n\t\t\ttesting.SetRealm(testing.NewUserRealm(tc.caller))\n\t\t\tLobbyGameFound(cross(cur))\n\n\t\t\tif tc.check != nil {\n\t\t\t\ttc.check(t)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLobbyJoin_HasOpenGames(cur realm, t *testing.T) {\n\tcleanup()\n\tg := \u0026Game{\n\t\tID:    \"123\",\n\t\tWhite: white,\n\t\tBlack: black,\n\t\tState: GameStateOpen,\n\t}\n\tgameStore.Set(g.ID, g)\n\taddToUser2Games(white, g)\n\taddToUser2Games(black, g)\n\n\ttesting.SetRealm(testing.NewUserRealm(white))\n\tLobbyJoin(cross(cur), 10*60, 5)\n\tif g.State != GameStateAborted {\n\t\tt.Errorf(\"state wrong: want %d got %d\", GameStateAborted, g.State)\n\t}\n\tif g.Winner != WinnerNone {\n\t\tt.Errorf(\"winner wrong: want %q got %q\", \"none\", g.Winner)\n\t}\n}\n"},{"name":"time.gno","body":"package chess\n\nimport (\n\t\"time\"\n)\n\n// TimeControl keeps track of time control information for the game.\ntype TimeControl struct {\n\tSeconds   int\n\tIncrement int\n\n\tStartedAt      time.Time\n\tMoveTimestamps []time.Time\n\tWhiteTime      time.Duration\n\tBlackTime      time.Duration\n}\n\nfunc NewTimeControl(seconds, incr int) *TimeControl {\n\tif seconds \u003c= 0 {\n\t\treturn nil\n\t}\n\treturn \u0026TimeControl{\n\t\tSeconds:   seconds,\n\t\tIncrement: incr,\n\n\t\tStartedAt: time.Now(),\n\t\tWhiteTime: time.Duration(seconds) * time.Second,\n\t\tBlackTime: time.Duration(seconds) * time.Second,\n\t}\n}\n\n// AddMove records that at the current time, a new move was added.\nfunc (tc *TimeControl) AddMove() (valid bool) {\n\tnd, v := tc.timedOut()\n\tif v {\n\t\treturn false\n\t}\n\tif len(tc.MoveTimestamps)\u00261 == 0 {\n\t\ttc.WhiteTime = nd\n\t} else {\n\t\ttc.BlackTime = nd\n\t}\n\ttc.MoveTimestamps = append(tc.MoveTimestamps, time.Now())\n\treturn true\n}\n\nfunc (tc *TimeControl) TimedOut() bool {\n\t_, v := tc.timedOut()\n\treturn v\n}\n\nfunc (tc *TimeControl) timedOut() (time.Duration, bool) {\n\tmts := tc.MoveTimestamps\n\n\t// First move for each player: they both have up to 30 seconds.\n\tswitch len(mts) {\n\tcase 0:\n\t\tdelta := time.Since(tc.StartedAt)\n\t\treturn tc.WhiteTime, delta \u003e time.Second*30\n\tcase 1:\n\t\tdelta := time.Since(mts[0])\n\t\treturn tc.BlackTime, delta \u003e time.Second*30\n\t}\n\n\t// Determine color. Determine time since last move. Try subtracting from\n\t// color's time. If \u003e= 0, good. If \u003c 0, timeout.\n\tdelta := time.Since(mts[len(mts)-1])\n\n\tif len(mts)\u00261 == 0 { // white\n\t\tnt := tc.WhiteTime - delta\n\t\treturn nt + tc.incr(), nt \u003c 0\n\t}\n\n\tnt := tc.BlackTime - delta\n\treturn nt + tc.incr(), nt \u003c 0\n}\n\nfunc (tc *TimeControl) incr() time.Duration {\n\t// there is always at least a one second increment, to account for\n\t// block time and the delay between user making a move and tx happening\n\treturn time.Second + time.Duration(tc.Increment)*time.Second\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"nAE3+hBoz4EXV2czH8I1J3p8cW8K0lkN7kQDlhXnZgtN/r3QTd95tWMdw86HQIsmYsAtxP5dAMrOt/Sa831FvQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"guestbook","path":"gno.land/r/morgan/guestbook","files":[{"name":"admin.gno","body":"package guestbook\n\nimport (\n\t\"gno.land/p/nt/ownable/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n)\n\nvar owner *ownable.Ownable\n\nfunc init(cur realm) {\n\tif !cur.Previous().IsUserCall() {\n\t\tpanic(\"realm must be initialized by EOA\")\n\t}\n\towner = ownable.NewWithAddress(cur.Previous().Address())\n}\n\n// AdminDelete removes the guestbook message with the given ID.\n// The user will still be marked as having submitted a message, so they\n// won't be able to re-submit a new message.\nfunc AdminDelete(cur realm, signatureID string) {\n\towner.AssertOwnedBy(cur.Previous().Address())\n\tid, err := seqid.FromString(signatureID)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tidb := id.Binary()\n\tif !guestbook.Has(idb) {\n\t\tpanic(\"signature does not exist\")\n\t}\n\tguestbook.Remove(idb)\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/morgan/guestbook\"\ngno = \"0.9\"\n"},{"name":"guestbook.gno","body":"// Realm guestbook contains an implementation of a simple guestbook.\n// Come and sign yourself up!\npackage guestbook\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n)\n\n// Signature is a single entry in the guestbook.\ntype Signature struct {\n\tMessage string\n\tAuthor  address\n\tTime    time.Time\n}\n\nconst (\n\tmaxMessageLength = 140\n\tmaxPerPage       = 25\n)\n\nvar (\n\tsignatureID seqid.ID\n\tguestbook   avl.Tree // id -\u003e Signature\n\thasSigned   avl.Tree // address -\u003e struct{}\n)\n\nfunc init(cur realm) {\n\tSign(cur, \"You reached the end of the guestbook!\")\n}\n\nconst (\n\terrNotAUser                  = \"this guestbook can only be signed by users\"\n\terrAlreadySigned             = \"you already signed the guestbook!\"\n\terrInvalidCharacterInMessage = \"invalid character in message\"\n)\n\n// Sign signs the guestbook, with the specified message.\nfunc Sign(cur realm, message string) {\n\tprev := cur.Previous()\n\tswitch {\n\tcase !prev.IsUser():\n\t\tpanic(errNotAUser)\n\tcase hasSigned.Has(prev.Address().String()):\n\t\tpanic(errAlreadySigned)\n\t}\n\tmessage = validateMessage(message)\n\n\tguestbook.Set(signatureID.Next().Binary(), Signature{\n\t\tMessage: message,\n\t\tAuthor:  prev.Address(),\n\t\t// NOTE: time.Now() will yield the \"block time\", which is deterministic.\n\t\tTime: time.Now(),\n\t})\n\thasSigned.Set(prev.Address().String(), struct{}{})\n}\n\nfunc validateMessage(msg string) string {\n\tif len(msg) \u003e maxMessageLength {\n\t\tpanic(\"Keep it brief! (max \" + strconv.Itoa(maxMessageLength) + \" bytes!)\")\n\t}\n\tout := \"\"\n\tfor _, ch := range msg {\n\t\tswitch {\n\t\tcase unicode.IsLetter(ch),\n\t\t\tunicode.IsNumber(ch),\n\t\t\tunicode.IsSpace(ch),\n\t\t\tunicode.IsPunct(ch):\n\t\t\tout += string(ch)\n\t\tdefault:\n\t\t\tpanic(errInvalidCharacterInMessage)\n\t\t}\n\t}\n\treturn out\n}\n\nfunc Render(maxID string) string {\n\tvar bld strings.Builder\n\n\tbld.WriteString(\"# Guestbook 📝\\n\\n[Come sign the guestbook!](./guestbook$help\u0026func=Sign)\\n\\n---\\n\\n\")\n\n\tvar maxIDBinary string\n\tif maxID != \"\" {\n\t\tmid, err := seqid.FromString(maxID)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\t// AVL iteration is exclusive, so we need to decrease the ID value to get the \"true\" maximum.\n\t\tmid--\n\t\tmaxIDBinary = mid.Binary()\n\t}\n\n\tvar lastID seqid.ID\n\tvar printed int\n\tguestbook.ReverseIterate(\"\", maxIDBinary, func(key string, val any) bool {\n\t\tsig := val.(Signature)\n\t\tmessage := strings.ReplaceAll(sig.Message, \"\\n\", \"\\n\u003e \")\n\t\tbld.WriteString(\"\u003e \" + message + \"\\n\u003e\\n\")\n\t\tidValue, ok := seqid.FromBinary(key)\n\t\tif !ok {\n\t\t\tpanic(\"invalid seqid id\")\n\t\t}\n\n\t\tbld.WriteString(\"\u003e _Written by \" + sig.Author.String() + \" at \" + sig.Time.Format(time.DateTime) + \"_ (#\" + idValue.String() + \")\\n\\n---\\n\\n\")\n\t\tlastID = idValue\n\n\t\tprinted++\n\t\t// stop after exceeding limit\n\t\treturn printed \u003e= maxPerPage\n\t})\n\n\tif printed == 0 {\n\t\tbld.WriteString(\"No messages!\")\n\t} else if printed \u003e= maxPerPage {\n\t\tbld.WriteString(\"\u003cp style='text-align:right'\u003e\u003ca href='./guestbook:\" + lastID.String() + \"'\u003eNext page\u003c/a\u003e\u003c/p\u003e\")\n\t}\n\n\treturn bld.String()\n}\n"},{"name":"guestbook_test.gno","body":"package guestbook\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/ownable/v0\"\n)\n\nfunc TestSign(cur realm, t *testing.T) {\n\tguestbook = avl.Tree{}\n\thasSigned = avl.Tree{}\n\n\ttesting.SetRealm(testing.NewUserRealm(\"g1user\"))\n\tSign(cross(cur), \"Hello!\")\n\n\ttesting.SetRealm(testing.NewUserRealm(\"g1user2\"))\n\tSign(cross(cur), \"Hello2!\")\n\n\tres := Render(\"\")\n\tt.Log(res)\n\tif !strings.Contains(res, \"\u003e Hello!\\n\u003e\\n\u003e _Written by g1user \") {\n\t\tt.Error(\"does not contain first user's message\")\n\t}\n\tif !strings.Contains(res, \"\u003e Hello2!\\n\u003e\\n\u003e _Written by g1user2 \") {\n\t\tt.Error(\"does not contain second user's message\")\n\t}\n\tif guestbook.Size() != 2 {\n\t\tt.Error(\"invalid guestbook size\")\n\t}\n}\n\nfunc TestSign_FromRealm(cur realm, t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/sys/users/init\"))\n\n\trec := revive(func() { Sign(cross(cur), \"Hey!\") })\n\tif rec == nil {\n\t\tt.Fatal(\"expected panic\")\n\t}\n\trecString, ok := rec.(string)\n\tif !ok {\n\t\tt.Fatal(\"not a string\", rec)\n\t} else if recString != errNotAUser {\n\t\tt.Fatal(\"invalid error\", recString)\n\t}\n}\n\nfunc TestSign_Double(cur realm, t *testing.T) {\n\t// Should not allow signing twice.\n\tguestbook = avl.Tree{}\n\thasSigned = avl.Tree{}\n\n\ttesting.SetRealm(testing.NewUserRealm(\"g1user\"))\n\tSign(cross(cur), \"Hello!\")\n\n\trec := revive(func() { Sign(cross(cur), \"Hello again!\") })\n\tif rec == nil {\n\t\tt.Fatal(\"expected panic\")\n\t}\n\trecString, ok := rec.(string)\n\tif !ok {\n\t\tt.Error(\"type assertion failed\", rec)\n\t} else if recString != errAlreadySigned {\n\t\tt.Error(\"invalid error message\", recString)\n\t}\n}\n\nfunc TestSign_InvalidMessage(cur realm, t *testing.T) {\n\t// Should not allow control characters in message.\n\tguestbook = avl.Tree{}\n\thasSigned = avl.Tree{}\n\n\ttesting.SetRealm(testing.NewUserRealm(\"g1user\"))\n\n\trec := revive(func() { Sign(cross(cur), \"\\x00Hello!\") })\n\tif rec == nil {\n\t\tt.Fatal(\"expected panic\")\n\t}\n\trecString, ok := rec.(string)\n\tif !ok {\n\t\tt.Error(\"type assertion failed\", rec)\n\t} else if recString != errInvalidCharacterInMessage {\n\t\tt.Error(\"invalid error message\", recString)\n\t}\n}\n\nfunc TestAdminDelete(cur realm, t *testing.T) {\n\tconst (\n\t\tuserAddr  address = \"g1user\"\n\t\tadminAddr address = \"g1admin\"\n\t)\n\n\tguestbook = avl.Tree{}\n\thasSigned = avl.Tree{}\n\towner = ownable.NewWithAddress(adminAddr)\n\tsignatureID = 0\n\n\ttesting.SetRealm(testing.NewUserRealm(userAddr))\n\n\tconst bad = \"Very Bad Message! Nyeh heh heh!\"\n\tSign(cross(cur), bad)\n\n\tif rnd := Render(\"\"); !strings.Contains(rnd, bad) {\n\t\tt.Fatal(\"render does not contain bad message\", rnd)\n\t}\n\n\ttesting.SetRealm(testing.NewUserRealm(adminAddr))\n\tAdminDelete(cross(cur), signatureID.String())\n\n\tif rnd := Render(\"\"); strings.Contains(rnd, bad) {\n\t\tt.Error(\"render contains bad message\", rnd)\n\t}\n\tif guestbook.Size() != 0 {\n\t\tt.Error(\"invalid guestbook size\")\n\t}\n\tif hasSigned.Size() != 1 {\n\t\tt.Error(\"invalid hasSigned size\")\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"soJcLn+QNE28lQphfl00/CWBbefYMBR47gtwGEk1eUscZcazPG2HaNGmyc/4vm8GvsBDubNRo9OP8y4bRgaSvQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"home","path":"gno.land/r/morgan/home","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/morgan/home\"\ngno = \"0.9\"\n"},{"name":"home.gno","body":"package home\n\nimport \"gno.land/r/leon/hor\"\n\nconst staticHome = `# morgan's (gn)home\n\n- [📝 sign my guestbook](/r/morgan/guestbook)\n`\n\nfunc init(cur realm) { hor.Register(cross(cur), \"Morgan's Home Realm\", \"\") }\n\nfunc Render(path string) string {\n\treturn staticHome\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"ak598e2sMX0cavju8XZURpC41OUlcdPJN6b5CRVjbaZWtdkiXePM/N6kvDdil/pZmo2xH3LLd5fwe0Hg5KITTA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","package":{"name":"config","path":"gno.land/r/moul/config","files":[{"name":"config.gno","body":"package config\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/moul/authz/v0\"\n)\n\nvar Authorizer *authz.Authorizer\n\nfunc init(cur realm) {\n\tif !cur.Previous().IsUserCall() {\n\t\tpanic(\"r/moul/config must be initialized by an EOA\")\n\t}\n\tAuthorizer = authz.NewWithMembers(cur.Previous().Address())\n}\n\n// AddManager adds a new address to the list of authorized managers.\n// This only works if the current authority is a MemberAuthority.\n// The caller must be authorized by the current authority.\nfunc AddManager(cur realm, addr address) error {\n\tmemberAuth, ok := Authorizer.Authority().(*authz.MemberAuthority)\n\tif !ok {\n\t\treturn errors.New(\"current authority is not a MemberAuthority, cannot add manager directly\")\n\t}\n\treturn memberAuth.AddMember(0, cur, addr)\n}\n\n// RemoveManager removes an address from the list of authorized managers.\n// This only works if the current authority is a MemberAuthority.\n// The caller must be authorized by the current authority.\nfunc RemoveManager(cur realm, addr address) error {\n\tmemberAuth, ok := Authorizer.Authority().(*authz.MemberAuthority)\n\tif !ok {\n\t\treturn errors.New(\"current authority is not a MemberAuthority, cannot remove manager directly\")\n\t}\n\treturn memberAuth.RemoveMember(0, cur, addr)\n}\n\n// SECURITY XXX: TransferManagement accepts any Authority impl as the\n// new authority — `newAuthority` is an open interface. Two risks:\n// (a) If the caller passes a foreign Authority impl, the impl's\n//\n//\tAuthorize method runs with whatever this realm's authority\n//\tgrants. Hostile impls can always-approve (silent privilege\n//\tescalation) or always-deny (DoS / lockout).\n//\n// (b) Once the new authority is installed, there is no way back if it\n//\n//\tturns out hostile — the new authority controls all future\n//\tTransfer calls. An attacker who becomes the authority (legit\n//\tor via a malicious newAuthority) can install a permanent\n//\tDroppedAuthority to lock this realm forever.\n//\n// Fix: restrict newAuthority to canonical authz impls (MemberAuthority,\n// ContractAuthority, etc. from p/moul/authz). Optionally wrap Transfer\n// with a quorum/cooldown check or a one-shot guard so a single\n// compromised authority can't permanently brick the config realm.\n//\n// TransferManagement transfers the authority to manage keys to a new authority.\n// The caller must be authorized by the current authority.\nfunc TransferManagement(cur realm, newAuthority authz.Authority) error {\n\tif newAuthority == nil {\n\t\treturn errors.New(\"new authority cannot be nil\")\n\t}\n\treturn Authorizer.Transfer(0, cur, newAuthority)\n}\n\n// ListManagers returns a slice of all managed keys.\nfunc ListManagers(cur realm) []address {\n\tvar keyList []address\n\tmemberAuth, ok := Authorizer.Authority().(*authz.MemberAuthority)\n\tif !ok {\n\t\treturn keyList\n\t}\n\ttree := memberAuth.Tree()\n\tif !ok || tree == nil {\n\t\treturn keyList // Return empty list if tree is not as expected or nil\n\t}\n\ttree.Iterate(\"\", \"\", func(key string, _ any) bool {\n\t\tkeyList = append(keyList, address(key))\n\t\treturn false\n\t})\n\treturn keyList\n}\n\nfunc HasManager(cur realm, addr address) bool {\n\tmemberAuth, ok := Authorizer.Authority().(*authz.MemberAuthority)\n\tif !ok {\n\t\treturn false // Return false if not a MemberAuthority or doesn't exist\n\t}\n\t// Use the MemberAuthority's specific RemoveMember method,\n\t// which internally performs the authorization check.\n\treturn memberAuth.Has(addr)\n}\n"},{"name":"config_test.gno","body":"package config\n\nimport (\n\t\"chain\"\n\t\"chain/runtime/unsafe\"\n\t\"errors\"\n\t\"testing\"\n\n\t\"gno.land/p/moul/authz/v0\"\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nvar (\n\toriginAddr     = testutils.TestAddress(\"origin\")\n\tmanager1Addr   = testutils.TestAddress(\"manager1\")\n\tmanager2Addr   = testutils.TestAddress(\"manager2\")\n\tnonManagerAddr = testutils.TestAddress(\"nonManager\")\n)\n\n// Helper to reset the Authorizer for each test, simulating initialization.\nfunc setupTest(cur realm, t *testing.T) {\n\tt.Helper()\n\n\t// Set the initial caller context\n\ttesting.SetRealm(testing.NewUserRealm(originAddr))\n\t// Initialize the Authorizer with the originAddr as the sole member,\n\t// simulating the state after NewWithOrigin() in a real deployment.\n\tAuthorizer = authz.NewWithAuthority(authz.NewMemberAuthority(originAddr))\n\t// Ensure the origin address is the initial manager\n\tuassert.True(t, HasManager(cur, originAddr), \"origin should be the initial manager\")\n}\n\nfunc TestAddManager(cur realm, t *testing.T) {\n\tsetupTest(cur, t)\n\n\t// Origin adds manager1 - Should succeed\n\ttesting.SetRealm(testing.NewUserRealm(originAddr))\n\terr := AddManager(cross(cur), manager1Addr)\n\tuassert.NoError(t, err, \"origin adding manager1 should succeed\")\n\tuassert.True(t, HasManager(cur, manager1Addr), \"manager1 should now be a manager\")\n\n\t// Non-manager tries to add manager2 - Should fail\n\ttesting.SetRealm(testing.NewUserRealm(nonManagerAddr))\n\terr = AddManager(cross(cur), manager2Addr)\n\tuassert.Error(t, err, \"non-manager adding manager2 should fail\")\n\tuassert.False(t, HasManager(cur, manager2Addr), \"manager2 should not have been added\")\n\n\t// Manager1 adds manager2 - Should succeed\n\ttesting.SetRealm(testing.NewUserRealm(manager1Addr))\n\terr = AddManager(cross(cur), manager2Addr)\n\tuassert.NoError(t, err, \"manager1 adding manager2 should succeed\")\n\tuassert.True(t, HasManager(cur, manager2Addr), \"manager2 should now be a manager\")\n\n\t// Transfer authority away from MemberAuthority\n\ttesting.SetRealm(testing.NewUserRealm(originAddr)) // Origin transfers\n\terr = TransferManagement(cross(cur), authz.NewAutoAcceptAuthority())\n\tuassert.NoError(t, err, \"transferring authority should succeed\")\n\n\t// Try adding after transfer - Should fail (wrong authority type)\n\ttesting.SetRealm(testing.NewUserRealm(manager1Addr))\n\terr = AddManager(cross(cur), nonManagerAddr) // Try adding someone new\n\tuassert.ErrorContains(t, err, \"current authority is not a MemberAuthority\", \"adding manager should fail after transfer\")\n}\n\nfunc TestRemoveManager(cur realm, t *testing.T) {\n\tsetupTest(cur, t)\n\n\t// Add manager1 first\n\ttesting.SetRealm(testing.NewUserRealm(originAddr))\n\terr := AddManager(cross(cur), manager1Addr)\n\tuassert.NoError(t, err, \"setup: failed to add manager1\")\n\tuassert.True(t, HasManager(cur, manager1Addr), \"setup: manager1 should be added\")\n\n\t// Non-manager tries to remove manager1 - Should fail\n\ttesting.SetRealm(testing.NewUserRealm(nonManagerAddr))\n\terr = RemoveManager(cross(cur), manager1Addr)\n\tuassert.Error(t, err, \"non-manager removing manager1 should fail\")\n\tuassert.True(t, HasManager(cur, manager1Addr), \"manager1 should still be a manager\")\n\n\t// Origin removes manager1 - Should succeed\n\ttesting.SetRealm(testing.NewUserRealm(originAddr))\n\terr = RemoveManager(cross(cur), manager1Addr)\n\tuassert.NoError(t, err, \"origin removing manager1 should succeed\")\n\tuassert.False(t, HasManager(cur, manager1Addr), \"manager1 should now be removed\")\n\n\t// Add manager1 again for next test case\n\ttesting.SetRealm(testing.NewUserRealm(originAddr))\n\terr = AddManager(cross(cur), manager1Addr)\n\tuassert.NoError(t, err, \"setup: failed to re-add manager1\")\n\n\t// Transfer authority\n\ttesting.SetRealm(testing.NewUserRealm(originAddr))\n\terr = TransferManagement(cross(cur), authz.NewAutoAcceptAuthority())\n\tuassert.NoError(t, err, \"transferring authority should succeed\")\n\n\t// Try removing after transfer - Should fail (wrong authority type)\n\ttesting.SetRealm(testing.NewUserRealm(originAddr)) // Use origin, doesn't matter which user now\n\terr = RemoveManager(cross(cur), manager1Addr)\n\tuassert.ErrorContains(t, err, \"current authority is not a MemberAuthority\", \"removing manager should fail after transfer\")\n}\n\nfunc TestListManagers(cur realm, t *testing.T) {\n\tsetupTest(cur, t)\n\tinitialList := ListManagers(cross(cur))\n\tassertAddrSliceEqual(t, []address{originAddr}, initialList)\n\t// Add manager1 and manager2\n\ttesting.SetRealm(testing.NewUserRealm(originAddr))\n\terr := AddManager(cross(cur), manager1Addr)\n\tuassert.NoError(t, err)\n\terr = AddManager(cross(cur), manager2Addr)\n\tuassert.NoError(t, err)\n\n\t// List should contain origin, manager1, manager2\n\tlist1 := ListManagers(cross(cur))\n\texpected1 := []address{manager2Addr, manager1Addr, originAddr}\n\tassertAddrSliceEqual(t, expected1, list1)\n\n\t// Remove manager1\n\ttesting.SetRealm(testing.NewUserRealm(originAddr)) // Can be origin or manager2\n\terr = RemoveManager(cross(cur), manager1Addr)\n\tuassert.NoError(t, err)\n\n\t// List should contain origin, manager2\n\tlist2 := ListManagers(cross(cur))\n\texpected2 := []address{manager2Addr, originAddr}\n\tassertAddrSliceEqual(t, expected2, list2)\n\n\t// Transfer authority\n\ttesting.SetRealm(testing.NewUserRealm(originAddr))\n\terr = TransferManagement(cross(cur), authz.NewAutoAcceptAuthority())\n\tuassert.NoError(t, err)\n\n\t// List should be empty after transfer\n\tlist3 := ListManagers(cross(cur))\n\tuassert.True(t, len(list3) == 0, \"manager list should be empty after transfer\")\n}\n\nfunc TestHasManager(cur realm, t *testing.T) {\n\tsetupTest(cur, t)\n\n\t// Initially, only origin is manager\n\tuassert.True(t, HasManager(cross(cur), originAddr), \"origin should initially be a manager\")\n\tuassert.False(t, HasManager(cross(cur), manager1Addr), \"manager1 should not initially be a manager\")\n\tuassert.False(t, HasManager(cross(cur), nonManagerAddr), \"nonManager should not initially be a manager\")\n\n\t// Add manager1\n\ttesting.SetRealm(testing.NewUserRealm(originAddr))\n\terr := AddManager(cross(cur), manager1Addr)\n\tuassert.NoError(t, err)\n\n\t// Check again\n\tuassert.True(t, HasManager(cross(cur), originAddr), \"origin should still be a manager\")\n\tuassert.True(t, HasManager(cross(cur), manager1Addr), \"manager1 should now be a manager\")\n\tuassert.False(t, HasManager(cross(cur), nonManagerAddr), \"nonManager should still not be a manager\")\n\n\t// Transfer authority\n\ttesting.SetRealm(testing.NewUserRealm(originAddr))\n\terr = TransferManagement(cross(cur), authz.NewAutoAcceptAuthority())\n\tuassert.NoError(t, err)\n\n\t// After transfer, HasManager should always return false for MemberAuthority checks\n\tuassert.False(t, HasManager(cross(cur), originAddr), \"HasManager should be false after transfer\")\n\tuassert.False(t, HasManager(cross(cur), manager1Addr), \"HasManager should be false after transfer\")\n\tuassert.False(t, HasManager(cross(cur), nonManagerAddr), \"HasManager should be false after transfer\")\n}\n\nfunc TestTransferManagement(cur realm, t *testing.T) {\n\tsetupTest(cur, t)\n\n\t// Add manager1\n\ttesting.SetRealm(testing.NewUserRealm(originAddr))\n\terr := AddManager(cross(cur), manager1Addr)\n\tuassert.NoError(t, err)\n\n\t// Create a new authority (MemberAuthority with manager2)\n\tnewAuthority := authz.NewMemberAuthority(manager2Addr)\n\n\t// Non-manager tries to transfer - Should fail\n\ttesting.SetRealm(testing.NewUserRealm(nonManagerAddr))\n\terr = TransferManagement(cross(cur), newAuthority)\n\tuassert.Error(t, err, \"non-manager transfer should fail\")\n\t_, isMemberAuth := Authorizer.Authority().(*authz.MemberAuthority)\n\tuassert.True(t, isMemberAuth, \"authority should still be MemberAuthority\") // Verify it didn't change\n\n\t// Manager1 tries to transfer - Should succeed\n\ttesting.SetRealm(testing.NewUserRealm(manager1Addr))\n\terr = TransferManagement(cross(cur), newAuthority)\n\tuassert.NoError(t, err, \"manager1 transfer should succeed\")\n\n\t// Verify current authority is the new one\n\tcurrentAuth := Authorizer.Authority()\n\tuassert.True(t, currentAuth == newAuthority, \"current authority should be the new one\")\n\n\t// Verify origin is no longer a manager under the *new* authority\n\ttesting.SetRealm(testing.NewUserRealm(manager2Addr)) // Need new manager to check\n\tuassert.False(t, HasManager(cross(cur), originAddr), \"origin should not be manager under new authority\")\n\tuassert.False(t, HasManager(cross(cur), manager1Addr), \"manager1 should not be manager under new authority\")\n\tuassert.True(t, HasManager(cross(cur), manager2Addr), \"manager2 should be manager under new authority\")\n\n\t// Try adding a manager using the old origin - Should fail\n\ttesting.SetRealm(testing.NewUserRealm(originAddr))\n\terr = AddManager(cross(cur), nonManagerAddr)\n\tuassert.Error(t, err, \"origin should not be able to add manager after transfer\")\n\n\t// Try adding a manager using the new manager (manager2) - Should succeed\n\ttesting.SetRealm(testing.NewUserRealm(manager2Addr))\n\terr = AddManager(cross(cur), nonManagerAddr)\n\tuassert.NoError(t, err, \"new manager (manager2) should be able to add managers\")\n\tuassert.True(t, HasManager(cross(cur), nonManagerAddr), \"nonManager should be added by manager2\")\n\n\t// Try transferring to nil - Should fail\n\ttesting.SetRealm(testing.NewUserRealm(manager2Addr))\n\terr = TransferManagement(cross(cur), nil)\n\tuassert.ErrorContains(t, err, \"new authority cannot be nil\", \"transferring to nil should fail\")\n}\n\nfunc TestTransferToContractAuthority(cur realm, t *testing.T) {\n\tsetupTest(cur, t) // Origin is the initial manager\n\n\tcontractPath := \"gno.land/r/testcontract\"\n\tcontractRealm := testing.NewCodeRealm(contractPath) // Simulate contract realm\n\n\t// Define a simple contract authority handler\n\thandlerExecuted := false // Track if the handler itself gets called\n\tcontractAuth := authz.NewContractAuthority(contractPath, func(title string, action authz.PrivilegedAction) error {\n\t\t// Simulate contract checking the caller *before* executing\n\t\tcaller := unsafe.CurrentRealm().Address()\n\t\texpectedContractAddr := chain.PackageAddress(contractPath)\n\t\tif caller != expectedContractAddr {\n\t\t\t// Fail before marking executed or running action\n\t\t\t// Note: In a real scenario, this handler might just ignore the call\n\t\t\t// if the caller isn't right, rather than returning an error,\n\t\t\t// depending on the desired contract logic. Returning an error\n\t\t\t// here helps the test verify the handler wasn't improperly called.\n\t\t\treturn errors.New(\"handler: caller is not the contract\")\n\t\t}\n\n\t\t// Only mark executed and run action if caller is correct\n\t\thandlerExecuted = true\n\t\treturn action()\n\t})\n\n\t// Origin transfers management to the contract authority\n\ttesting.SetRealm(testing.NewUserRealm(originAddr))\n\terr := TransferManagement(cross(cur), contractAuth)\n\tuassert.NoError(t, err, \"transfer to contract authority failed\")\n\tuassert.True(t, Authorizer.Authority() == contractAuth, \"authority should now be the contract authority\")\n\n\t// Now, actions like AddManager/RemoveManager should fail because the current\n\t// authority is no longer a MemberAuthority. The contract would need its own\n\t// logic executed via Authorizer.DoByCurrent() to manage members if desired.\n\n\t// Try adding a manager (will check authority type) - Should fail\n\ttesting.SetRealm(testing.NewUserRealm(originAddr)) // Caller doesn't matter for this check\n\terr = AddManager(cross(cur), manager1Addr)\n\tuassert.ErrorContains(t, err, \"current authority is not a MemberAuthority\", \"AddManager should fail with ContractAuthority\")\n\n\t// Simulate an action authorized *by the contract* using Authorizer.Do\n\tvar contractActionExecuted bool\n\thandlerExecuted = false         // Reset tracker\n\ttesting.SetRealm(contractRealm) // Call must originate from the contract now\n\terr = Authorizer.DoByCurrent(0, cur, \"some_contract_action\", func() error {\n\t\tcontractActionExecuted = true\n\t\t// Imagine contract logic here\n\t\treturn nil\n\t})\n\tuassert.NoError(t, err, \"contract action via Authorizer.Do failed\")\n\tuassert.True(t, handlerExecuted, \"handler should have been executed by contract call\") // Verify handler ran\n\tuassert.True(t, contractActionExecuted, \"contract action should have been executed\")\n\n\t// Simulate an action from a user - Should fail before handler is called\n\tvar userActionExecuted bool\n\thandlerExecuted = false // Reset tracker\n\ttesting.SetRealm(testing.NewUserRealm(nonManagerAddr))\n\terr = Authorizer.DoByCurrent(0, cur, \"some_user_action\", func() error {\n\t\tuserActionExecuted = true\n\t\treturn nil\n\t})\n\t// A plain ContractAuthority defaults its proposer to the contract\n\t// itself, so a user caller is rejected UPSTREAM (at the proposer) and\n\t// the handler is never reached — the handler's own CurrentRealm check\n\t// above is now belt-and-braces, not the guard being relied on.\n\tuassert.Error(t, err, \"user action via Authorizer.Do should fail when contract is authority\")\n\tuassert.ErrorContains(t, err, \"unauthorized\", \"rejection should come from the contract-identity proposer\")\n\tuassert.False(t, handlerExecuted, \"handler should NOT have been executed by user call\")\n\tuassert.False(t, userActionExecuted, \"user action should not have been executed\")\n}\n\n// Helper to check if a slice contains a specific address\nfunc containsAddr(list []address, addr address) bool {\n\tfor _, item := range list {\n\t\tif item == addr {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc assertAddrSliceEqual(t *testing.T, expected, actual []address) {\n\tt.Helper()\n\tif len(expected) != len(actual) {\n\t\tt.Fatalf(\"expected slice length %d, got %d. Expected: %v, Got: %v\", len(expected), len(actual), expected, actual)\n\t}\n\n\tfor i := range expected {\n\t\tif expected[i] != actual[i] {\n\t\t\tt.Fatalf(\"slices differ at index %d. Expected: %v, Got: %v\", i, expected, actual)\n\t\t}\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/moul/config\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"62RKSIuN5x+24r7Uui3QQuyfVJDQ1fNUGqQ+R5f0Da1hxe3sMLLfqz4xgva+uQaHRkSOYN0tdDwW8/E45DCRAg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","package":{"name":"microposts","path":"gno.land/r/moul/microposts","files":[{"name":"README.md","body":"# fork of `leon/fosdem25/microposts`\n\nremoving optional lines to make the code more concise for slides.\n\nOriginal work here: https://gno.land/r/leon/fosdem25/microposts\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/moul/microposts\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"},{"name":"microposts_test.gno","body":"package microposts\n\n// empty file just to make sure that `gno test` tries to parse the implementation.\n"},{"name":"post.gno","body":"package microposts\n\nimport (\n\t\"time\"\n)\n\ntype Post struct {\n\ttext      string\n\tauthor    address\n\tcreatedAt time.Time\n}\n\nfunc (p Post) String() string {\n\tout := p.text + \"\\n\"\n\tout += \"_\" + p.createdAt.Format(\"02 Jan 2006, 15:04\") + \", by \" + p.author.String() + \"_\"\n\treturn out\n}\n"},{"name":"realm.gno","body":"package microposts\n\nimport (\n\t\"strconv\"\n\t\"time\"\n)\n\nvar posts []*Post\n\nfunc CreatePost(cur realm, text string) {\n\tposts = append(posts, \u0026Post{\n\t\ttext:      text,\n\t\tauthor:    cur.Previous().Address(), // provided by env\n\t\tcreatedAt: time.Now(),\n\t})\n}\n\nfunc Render(_ string) string {\n\tout := \"# Posts\\n\"\n\tfor i := len(posts) - 1; i \u003e= 0; i-- {\n\t\tout += \"### Post \" + strconv.Itoa(i) + \"\\n\" + posts[i].String()\n\t}\n\treturn out\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"v3yMJJIui+6uDJ41qpBjlbrCgxYs5UaBSrDWSdpHbpw10bgSozQMC9+bUD6NRNTqhMvnavM4PJgrK3C6YkOZEw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"config","path":"gno.land/r/n2p5/config","files":[{"name":"config.gno","body":"package config\n\nimport (\n\t\"gno.land/p/n2p5/mgroup\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nconst (\n\toriginalOwner = \"g1j39fhg29uehm7twwnhvnpz3ggrm6tprhq65t0t\" // n2p5\n)\n\nvar (\n\tadminGroup  = mgroup.New(originalOwner)\n\tdescription = \"\"\n)\n\n// AddBackupOwner adds a backup owner to the Owner Group.\n// A backup owner can claim ownership of the contract.\nfunc AddBackupOwner(cur realm, addr address) {\n\tif err := adminGroup.AddBackupOwner(0, cur, addr); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// RemoveBackupOwner removes a backup owner from the Owner Group.\n// The primary owner cannot be removed.\nfunc RemoveBackupOwner(cur realm, addr address) {\n\tif err := adminGroup.RemoveBackupOwner(0, cur, addr); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// ClaimOwnership allows an authorized user in the ownerGroup\n// to claim ownership of the contract.\nfunc ClaimOwnership(cur realm) {\n\tif err := adminGroup.ClaimOwnership(0, cur); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// AddAdmin adds an admin to the Admin Group.\nfunc AddAdmin(cur realm, addr address) {\n\tif err := adminGroup.AddMember(0, cur, addr); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// RemoveAdmin removes an admin from the Admin Group.\n// The primary owner cannot be removed.\nfunc RemoveAdmin(cur realm, addr address) {\n\tif err := adminGroup.RemoveMember(0, cur, addr); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Owner returns the current owner of the claims contract.\nfunc Owner() address {\n\treturn adminGroup.Owner()\n}\n\n// BackupOwners returns the current backup owners of the claims contract.\nfunc BackupOwners() []string {\n\treturn adminGroup.BackupOwners()\n}\n\n// Admins returns the current admin members of the claims contract.\nfunc Admins() []string {\n\treturn adminGroup.Members()\n}\n\n// IsAdmin checks if an address is in the config adminGroup.\nfunc IsAdmin(addr address) bool {\n\treturn adminGroup.IsMember(addr)\n}\n\n// toMarkdownList formats a slice of strings as a markdown list.\nfunc toMarkdownList(items []string) string {\n\tvar result string\n\tfor _, item := range items {\n\t\tresult += ufmt.Sprintf(\"- %s\\n\", item)\n\t}\n\treturn result\n}\n\nfunc Render(path string) string {\n\towner := adminGroup.Owner().String()\n\tbackupOwners := toMarkdownList(BackupOwners())\n\tadminMembers := toMarkdownList(Admins())\n\treturn ufmt.Sprintf(`\n# Config Dashboard\n\nThis dashboard shows the current configuration owner, backup owners, and admin members.\n- The owner has the exclusive ability to manage the backup owners and admin members.\n- Backup owners can claim ownership of the contract and become the owner.\n- Admin members are used to authorize actions in other realms, such as [my home realm](/r/n2p5/home).\n\n#### Owner\n\n%s\n\n#### Backup Owners\n\n%s\n\n#### Admin Members\n\n%s\n\n`,\n\t\towner,\n\t\tbackupOwners,\n\t\tadminMembers)\n}\n"},{"name":"config_test.gno","body":"package config\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/ownable/v0\"\n\t\"gno.land/p/nt/testutils/v0\"\n)\n\nfunc TestAddBackupOwner(cur realm, t *testing.T) {\n\towner := address(\"g1j39fhg29uehm7twwnhvnpz3ggrm6tprhq65t0t\")\n\tu1 := testutils.TestAddress(\"u1\")\n\tu2 := testutils.TestAddress(\"u2\")\n\n\ttesting.SetOriginCaller(owner)\n\tAddBackupOwner(cross(cur), u1)\n\tb := BackupOwners()\n\tif b[1] != u1.String() {\n\t\tt.Error(\"failed to add u1 to backupowners\")\n\t}\n\ttesting.SetOriginCaller(u1)\n\tr := revive(func() {\n\t\tAddBackupOwner(cross(cur), u2)\n\t})\n\tif r != ownable.ErrUnauthorized {\n\t\tt.Error(\"failed to catch unauthorized access\")\n\t}\n\n\ttesting.SetOriginCaller(owner)\n\tRemoveBackupOwner(cross(cur), u1)\n\tRemoveBackupOwner(cross(cur), u2)\n}\n\nfunc TestRemoveBackupOwner(cur realm, t *testing.T) {\n\towner := address(\"g1j39fhg29uehm7twwnhvnpz3ggrm6tprhq65t0t\")\n\tu1 := testutils.TestAddress(\"u1\")\n\tu2 := testutils.TestAddress(\"u2\")\n\n\ttesting.SetOriginCaller(owner)\n\tAddBackupOwner(cross(cur), u1)\n\n\ttesting.SetOriginCaller(u2)\n\tr := revive(func() {\n\t\tRemoveBackupOwner(cross(cur), u1)\n\t})\n\tif r != ownable.ErrUnauthorized {\n\t\tt.Error(\"failed to catch unauthorized access\")\n\t}\n\n\ttesting.SetOriginCaller(owner)\n\tRemoveBackupOwner(cross(cur), u1)\n\n\tif len(BackupOwners()) != 1 {\n\t\tt.Error(\"BackupOwners should be length == 1 \")\n\t}\n}\n\nfunc TestClaimOwnership(cur realm, t *testing.T) {\n\towner := address(\"g1j39fhg29uehm7twwnhvnpz3ggrm6tprhq65t0t\")\n\tu1 := testutils.TestAddress(\"u1\")\n\n\tif owner != Owner() {\n\t\tt.Errorf(\"expected: %v, got: %v\", owner, Owner())\n\t}\n\n\ttesting.SetOriginCaller(owner)\n\tAddBackupOwner(cross(cur), u1)\n\n\ttesting.SetOriginCaller(u1)\n\tClaimOwnership(cross(cur))\n\n\tif u1 != Owner() {\n\t\tt.Errorf(\"expected: %v, got: %v\", owner, Owner())\n\t}\n\n\ttesting.SetOriginCaller(owner)\n\tClaimOwnership(cross(cur))\n}\n\nfunc TestAddAdmin(cur realm, t *testing.T) {\n\towner := address(\"g1j39fhg29uehm7twwnhvnpz3ggrm6tprhq65t0t\")\n\tu1 := testutils.TestAddress(\"u1\")\n\tu2 := testutils.TestAddress(\"u2\")\n\n\ttesting.SetOriginCaller(owner)\n\tAddAdmin(cross(cur), u1)\n\tadmins := Admins()\n\tif admins[1] != u1.String() {\n\t\tt.Error(\"failed to add u1 to admins group\")\n\t}\n\ttesting.SetOriginCaller(u1)\n\tr := revive(func() {\n\t\tAddAdmin(cross(cur), u2)\n\t})\n\tif r != ownable.ErrUnauthorized {\n\t\tt.Error(\"failed to catch unauthorized access\")\n\t}\n\n\t// cleanup\n\ttesting.SetOriginCaller(owner)\n\tRemoveAdmin(cross(cur), u1)\n}\n\nfunc TestRemoveAdmin(cur realm, t *testing.T) {\n\towner := address(\"g1j39fhg29uehm7twwnhvnpz3ggrm6tprhq65t0t\")\n\tu1 := testutils.TestAddress(\"u1\")\n\tu2 := testutils.TestAddress(\"u2\")\n\n\ttesting.SetOriginCaller(owner)\n\tAddAdmin(cross(cur), u1)\n\n\ttesting.SetOriginCaller(u2)\n\tr := revive(func() {\n\t\tRemoveAdmin(cross(cur), u1)\n\t})\n\tif r != ownable.ErrUnauthorized {\n\t\tt.Error(\"failed to catch unauthorized access\")\n\t}\n\n\ttesting.SetOriginCaller(owner)\n\tRemoveAdmin(cross(cur), u1)\n\n\tif len(Admins()) != 1 {\n\t\tt.Error(\"Admin should be length == 1 \")\n\t}\n}\n\nfunc TestIsAdmin(cur realm, t *testing.T) {\n\towner := address(\"g1j39fhg29uehm7twwnhvnpz3ggrm6tprhq65t0t\")\n\tu1 := testutils.TestAddress(\"u1\")\n\tu2 := testutils.TestAddress(\"u2\")\n\n\ttesting.SetOriginCaller(owner)\n\tAddAdmin(cross(cur), u1)\n\n\tif !IsAdmin(owner) {\n\t\tt.Error(\"owner should be admin\")\n\t}\n\tif !IsAdmin(u1) {\n\t\tt.Error(\"u1 should be admin\")\n\t}\n\tif IsAdmin(u2) {\n\t\tt.Error(\"u2 should not be admin\")\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/n2p5/config\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"Oti25Cv7mI4wPYl44mYi52CarqKKzrOJ0t5UV3JzYx9z5Z8vG8W0dOFBgO/J3wmRL3f2CIam20tA/aYsk4dN4A=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"haystack","path":"gno.land/r/n2p5/haystack","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/n2p5/haystack\"\ngno = \"0.9\"\n"},{"name":"haystack.gno","body":"package haystack\n\nimport (\n\t\"gno.land/p/n2p5/haystack\"\n)\n\nvar storage = haystack.New()\n\nfunc Render(path string) string {\n\treturn `\nPut a Needle in the Haystack.\n`\n}\n\n// Add takes a fixed-length hex-encoded needle bytes and adds it to the haystack key-value store.\n// If storage encounters an error, it will panic.\nfunc Add(_ realm, needleHex string) {\n\terr := storage.Add(needleHex)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Get takes a fixed-length hex-encoded needle hash and returns the hex-encoded needle bytes.\n// If storage encounters an error, it will panic.\nfunc Get(hashHex string) string {\n\tneedleHex, err := storage.Get(hashHex)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn needleHex\n}\n"},{"name":"haystack_test.gno","body":"package haystack\n\nimport (\n\t\"encoding/hex\"\n\t\"testing\"\n\n\t\"gno.land/p/n2p5/haystack\"\n\t\"gno.land/p/n2p5/haystack/needle\"\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\n// cur is a zero-value realm used as a placeholder when forwarding to\n// uassert/urequire dispatch helpers that gained an `rlm realm` param.\n// These tests pass `func()` callbacks (no crossing inside the callback),\n// so rlm is ignored — a nil realm here is safe.\nvar cur realm\n\nfunc TestHaystack(cur realm, t *testing.T) {\n\tt.Parallel()\n\t// needleHex returns a hex-encoded needle and its hash for a given index.\n\tgenNeedleHex := func(i int) (string, string) {\n\t\tb := make([]byte, needle.PayloadLength)\n\t\tb[0] = byte(i)\n\t\tn, _ := needle.New(b)\n\t\treturn hex.EncodeToString(n.Bytes()), hex.EncodeToString(n.Hash())\n\t}\n\n\tu1 := testutils.TestAddress(\"u1\")\n\tu2 := testutils.TestAddress(\"u2\")\n\n\tt.Run(\"Add\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tn1, _ := genNeedleHex(1)\n\t\tn2, _ := genNeedleHex(2)\n\t\tn3, _ := genNeedleHex(3)\n\n\t\ttesting.SetOriginCaller(u1)\n\t\turequire.NotPanics(t, cur, func() { Add(cross(cur), n1) })\n\t\turequire.AbortsWithMessage(t, cur,\n\t\t\thaystack.ErrorDuplicateNeedle.Error(),\n\t\t\tfunc() {\n\t\t\t\tAdd(cross(cur), n1)\n\t\t\t})\n\t\ttesting.SetOriginCaller(u2)\n\t\turequire.NotPanics(t, cur, func() { Add(cross(cur), n2) })\n\t\turequire.NotPanics(t, cur, func() { Add(cross(cur), n3) })\n\t})\n\n\tt.Run(\"Get\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tn1, h1 := genNeedleHex(4)\n\t\t_, h2 := genNeedleHex(5)\n\n\t\ttesting.SetOriginCaller(u1)\n\t\turequire.NotPanics(t, cur, func() { Add(cross(cur), n1) })\n\t\turequire.NotPanics(t, cur, func() {\n\t\t\tresult := Get(h1)\n\t\t\turequire.Equal(t, n1, result)\n\t\t})\n\n\t\ttesting.SetOriginCaller(u2)\n\t\turequire.NotPanics(t, cur, func() {\n\t\t\tresult := Get(h1)\n\t\t\turequire.Equal(t, n1, result)\n\t\t})\n\t\turequire.PanicsWithMessage(t, cur,\n\t\t\thaystack.ErrorNeedleNotFound.Error(),\n\t\t\tfunc() {\n\t\t\t\tGet(h2)\n\t\t\t})\n\t})\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"0Pj99XwE28J/H8nqTOc/sHTra2/t43E5qf5Wjpb78yYFDOH6H9h9AxDhpxy9bcEq1SQ6bHt5PKL7sEXXKY1PfQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"home","path":"gno.land/r/n2p5/home","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/n2p5/home\"\ngno = \"0.9\"\n"},{"name":"home.gno","body":"package home\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/n2p5/chonk\"\n\n\t\"gno.land/r/leon/hor\"\n\t\"gno.land/r/n2p5/config\"\n)\n\nvar (\n\tactive  = chonk.New()\n\tpreview = chonk.New()\n)\n\nfunc init(cur realm) {\n\thor.Register(cross(cur), \"n2p5's Home Realm\", \"\")\n\n}\n\n// Add appends a string to the preview Chonk.\nfunc Add(cur realm, chunk string) {\n\tassertAdmin(0, cur)\n\tpreview.Add(chunk)\n}\n\n// Flush clears the preview Chonk.\nfunc Flush(cur realm) {\n\tassertAdmin(0, cur)\n\tpreview.Flush()\n}\n\n// Promote promotes the preview Chonk to the active Chonk\n// and creates a new preview Chonk.\nfunc Promote(cur realm) {\n\tassertAdmin(0, cur)\n\tactive = preview\n\tpreview = chonk.New()\n}\n\n// Render returns the contents of the scanner for the active or preview Chonk\n// based on the path provided.\nfunc Render(path string) string {\n\tvar result string\n\tscanner := getScanner(path)\n\tfor scanner.Scan() {\n\t\tresult += scanner.Text()\n\t}\n\treturn result\n}\n\n// assertAdmin panics if the caller is not an admin as defined in the config realm.\nfunc assertAdmin(_ int, rlm realm) {\n\tif !rlm.IsCurrent() {\n\t\tpanic(\"unauthorized: rlm is not the caller's live cur\")\n\t}\n\tcaller := rlm.Previous().Address()\n\tif !config.IsAdmin(caller) {\n\t\tpanic(\"forbidden: must be admin\")\n\t}\n}\n\n// getScanner returns the scanner for the active or preview Chonk based\n// on the path provided.\nfunc getScanner(path string) *chonk.Scanner {\n\tif isPreview(path) {\n\t\treturn preview.Scanner()\n\t}\n\treturn active.Scanner()\n}\n\n// isPreview returns true if the path prefix is \"preview\".\nfunc isPreview(path string) bool {\n\treturn strings.HasPrefix(path, \"preview\")\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"6to/2LvYrsJPI2Kt0g7ID2HkXLbyR3QH0pIRDFr9WrNqZlsxbxfQRSYZtXM/E23PEKJo+qf28lNPLzyhCcF80A=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"loci","path":"gno.land/r/n2p5/loci","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/n2p5/loci\"\ngno = \"0.9\"\n"},{"name":"loci.gno","body":"package loci\n\nimport (\n\t\"chain\"\n\t\"encoding/base64\"\n\n\t\"gno.land/p/n2p5/loci\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nvar store *loci.LociStore\n\nfunc init() {\n\tstore = loci.New()\n}\n\n// Set takes a base64 encoded string and stores it in the Loci store.\n// Keyed by the address of the caller. It also emits a \"set\" event with\n// the address of the caller.\nfunc Set(cur realm, value string) {\n\tb, err := base64.StdEncoding.DecodeString(value)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tstore.Set(0, cur, b)\n\tchain.Emit(\"SetValue\", \"ForAddr\", string(cur.Previous().Address()))\n}\n\n// Get retrieves the value stored at the provided address and\n// returns it as a base64 encoded string.\nfunc Get(_ realm, addr address) string {\n\treturn base64.StdEncoding.EncodeToString(store.Get(addr))\n}\n\nfunc Render(cur realm, path string) string {\n\tif path == \"\" {\n\t\treturn `\n# Welcome to Loci\n\nLoci is a simple key-value store keyed by the caller's gno.land address.\nOnly the caller can set the value for their address, but anyone can\nretrieve the value for any address. There are only two functions: Set and Get.\nIf you'd like to set a value, simply base64 encode any message you'd like and\nit will be stored in in Loci. If you'd like to retrieve a value, simply provide\nthe address of the value you'd like to retrieve.\n\nFor convenience, you can also use gnoweb to view the value for a given address,\nif one exists. For instance append :g1j39fhg29uehm7twwnhvnpz3ggrm6tprhq65t0t to\nthis URL to view the value stored at that address.\n`\n\t}\n\treturn renderGet(cross(cur), address(path))\n}\n\nfunc renderGet(cur realm, addr address) string {\n\tvalue := \"```\\n\" + Get(cur, addr) + \"\\n```\"\n\n\treturn ufmt.Sprintf(`\n# Loci Value Viewer\n\n**Address:** %s\n\n%s\n\n`, addr, value)\n}\n"},{"name":"z_0_filetest.gno","body":"// PKGPATH: gno.land/r/test/test\npackage test\n\nimport (\n\t\"gno.land/r/n2p5/loci\"\n)\n\nfunc main(cur realm) {\n\tcaller := cur\n\tprintln(\"caller: \" + string(caller.Address()))\n\n\t// test nothing being set, yet.\n\tr0 := loci.Get(cross(cur), caller.Address())\n\tprintln(\"expect: \" + \"\")\n\tprintln(\"got   : \" + r0)\n\n\t// set the value, which uses the CurrentRealm as the caller.\n\tinput1 := \"aGVsbG8sIHdvcmxkCg==\"\n\tloci.Set(cross(cur), input1)\n\tprintln(\"set   : \" + string(input1))\n\tr1 := loci.Get(cross(cur), caller.Address())\n\tprintln(\"expect: \" + input1)\n\tprintln(\"got   : \" + r1)\n\n\t// change the value, which should override the previous value.\n\tinput2 := \"Z29vZGJ5ZSwgd29ybGQK\"\n\tloci.Set(cross(cur), input2)\n\tprintln(\"set   : \" + string(input2))\n\tr2 := loci.Get(cross(cur), caller.Address())\n\tprintln(\"expect: \" + input2)\n\tprintln(\"got   : \" + r2)\n\n}\n\n// Output:\n// caller: g1z7fga7u94pdmamlvcrtvsfwxgsye0qv3rres7n\n// expect:\n// got   :\n// set   : aGVsbG8sIHdvcmxkCg==\n// expect: aGVsbG8sIHdvcmxkCg==\n// got   : aGVsbG8sIHdvcmxkCg==\n// set   : Z29vZGJ5ZSwgd29ybGQK\n// expect: Z29vZGJ5ZSwgd29ybGQK\n// got   : Z29vZGJ5ZSwgd29ybGQK\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"UPr4MdMbUGWPZ9ibsC/Xs19KLysKfi/Aetau/6n6BSMLqGiHmoNhgo5B/A1JS3rjLo5PmfyxrtQT2zj2OYML5g=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"config","path":"gno.land/r/nemanya/config","files":[{"name":"config.gno","body":"package config\n\nimport \"errors\"\n\nvar (\n\tmain   address\n\tbackup address\n\n\tErrInvalidAddr  = errors.New(\"Invalid address\")\n\tErrUnauthorized = errors.New(\"Unauthorized\")\n)\n\nfunc init() {\n\tmain = \"g1x9qyf6f34v2g52k4q5smn5tctmj3hl2kj7l2ql\"\n}\n\nfunc Address() address {\n\treturn main\n}\n\nfunc Backup() address {\n\treturn backup\n}\n\nfunc SetAddress(cur realm, a address) error {\n\tif !a.IsValid() {\n\t\treturn ErrInvalidAddr\n\t}\n\n\tif err := checkAuthorized(0, cur); err != nil {\n\t\treturn err\n\t}\n\n\tmain = a\n\treturn nil\n}\n\nfunc SetBackup(cur realm, a address) error {\n\tif !a.IsValid() {\n\t\treturn ErrInvalidAddr\n\t}\n\n\tif err := checkAuthorized(0, cur); err != nil {\n\t\treturn err\n\t}\n\n\tbackup = a\n\treturn nil\n}\n\nfunc checkAuthorized(_ int, rlm realm) error {\n\tif !rlm.IsCurrent() {\n\t\treturn ErrUnauthorized\n\t}\n\tcaller := rlm.Previous().Address()\n\tisAuthorized := caller == main || caller == backup\n\n\tif !isAuthorized {\n\t\treturn ErrUnauthorized\n\t}\n\n\treturn nil\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/nemanya/config\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"bcg6VsAvqkduHscVQVvZjkyRVhk7tF9NjD+J7jTrIsIjfYGNV/NNDBFgRMza92OlmTGMXapPpkiuifqycJtUVw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"home","path":"gno.land/r/nemanya/home","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/nemanya/home\"\ngno = \"0.9\"\n"},{"name":"home.gno","body":"package home\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"chain/runtime/unsafe\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/nemanya/config\"\n)\n\ntype SocialLink struct {\n\tURL  string\n\tText string\n}\n\ntype Sponsor struct {\n\tAddress address\n\tAmount  chain.Coins\n}\n\ntype Project struct {\n\tName        string\n\tDescription string\n\tURL         string\n\tImageURL    string\n\tSponsors    map[address]Sponsor\n}\n\nvar (\n\ttextArt        string\n\taboutMe        string\n\tsponsorInfo    string\n\tsocialLinks    map[string]SocialLink\n\tgnoProjects    map[string]Project\n\totherProjects  map[string]Project\n\ttotalDonations chain.Coins\n)\n\nfunc init() {\n\ttextArt = renderTextArt()\n\taboutMe = \"I am a student of IT at Faculty of Sciences in Novi Sad, Serbia. My background is mainly in web and low-level programming, but since Web3 Bootcamp at Petnica this year I've been actively learning about blockchain and adjacent technologies. I am excited about contributing to the gno.land ecosystem and learning from the community.\\n\\n\"\n\tsponsorInfo = \"You can sponsor a project by sending GNOT to this address. Your sponsorship will be displayed on the project page. Thank you for supporting the development of gno.land!\\n\\n\"\n\n\tsocialLinks = map[string]SocialLink{\n\t\t\"GitHub\":   {URL: \"https://github.com/Nemanya8\", Text: \"Explore my repositories and open-source contributions.\"},\n\t\t\"LinkedIn\": {URL: \"https://www.linkedin.com/in/nemanjamatic/\", Text: \"Connect with me professionally.\"},\n\t\t\"Email Me\": {URL: \"mailto:matic.nemanya@gmail.com\", Text: \"Reach out for collaboration or inquiries.\"},\n\t}\n\n\tgnoProjects = make(map[string]Project)\n\totherProjects = make(map[string]Project)\n\n\tgnoProjects[\"Liberty Bridge\"] = Project{\n\t\tName:        \"Liberty Bridge\",\n\t\tDescription: \"Liberty Bridge was my first Web3 project, developed as part of the Web3 Bootcamp at Petnica. This project served as a centralized bridge between Ethereum and gno.land, enabling seamless asset transfers and fostering interoperability between the two ecosystems.\\n\\n The primary objective of Liberty Bridge was to address the challenges of connecting decentralized networks by implementing a user-friendly solution that simplified the process for users. The project incorporated mechanisms to securely transfer assets between the Ethereum and gno.land blockchains, ensuring efficiency and reliability while maintaining a centralized framework for governance and operations.\\n\\n Through this project, I gained hands-on knowledge of blockchain interoperability, Web3 protocols, and the intricacies of building solutions that bridge different blockchain ecosystems.\\n\\n\",\n\t\tURL:         \"https://gno.land\",\n\t\tImageURL:    \"https://github.com/Milosevic02/LibertyBridge/raw/main/lb_banner.png\",\n\t\tSponsors:    make(map[address]Sponsor),\n\t}\n\n\totherProjects[\"Incognito\"] = Project{\n\t\tName:        \"Incognito\",\n\t\tDescription: \"Incognito is a Web3 platform built for Ethereum-based chains, designed to connect advertisers with users in a privacy-first and mutually beneficial way. Its modular architecture makes it easily expandable to other blockchains. Developed during the ETH Sofia Hackathon, it was recognized as a winning project for its innovation and impact.\\n\\n The platform allows advertisers to send personalized ads while sharing a portion of the marketing budget with users. It uses machine learning to match users based on wallet activity, ensuring precise targeting. User emails are stored securely on-chain and never shared, prioritizing privacy and transparency.\\n\\n With all campaign data stored on-chain, Incognito ensures decentralization and accountability. By rewarding users and empowering advertisers, it sets a new standard for fair and transparent blockchain-based advertising.\",\n\t\tURL:         \"https://github.com/Milosevic02/Incognito-ETHSofia\",\n\t\tImageURL:    \"\",\n\t\tSponsors:    make(map[address]Sponsor),\n\t}\n}\n\nfunc Render(_ string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"# Hi, I'm\\n\")\n\tsb.WriteString(textArt)\n\tsb.WriteString(\"---\\n\")\n\tsb.WriteString(\"## About me\\n\")\n\tsb.WriteString(aboutMe)\n\tsb.WriteString(sponsorInfo)\n\tsb.WriteString(ufmt.Sprintf(\"# Total Sponsor Donations: %s\\n\", totalDonations.String()))\n\tsb.WriteString(\"---\\n\")\n\tsb.WriteString(renderProjects(gnoProjects, \"Gno Projects\"))\n\tsb.WriteString(\"---\\n\")\n\tsb.WriteString(renderProjects(otherProjects, \"Other Projects\"))\n\tsb.WriteString(\"---\\n\")\n\tsb.WriteString(renderSocialLinks())\n\n\treturn sb.String()\n}\n\nfunc renderTextArt() string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"```\\n\")\n\tsb.WriteString(\"      ___           ___           ___           ___           ___           ___           ___     \\n\")\n\tsb.WriteString(\"     /\\\\__\\\\         /\\\\  \\\\         /\\\\__\\\\         /\\\\  \\\\         /\\\\__\\\\         |\\\\__\\\\         /\\\\  \\\\    \\n\")\n\tsb.WriteString(\"    /::|  |       /::\\\\  \\\\       /::|  |       /::\\\\  \\\\       /::|  |        |:|  |       /::\\\\  \\\\   \\n\")\n\tsb.WriteString(\"   /:|:|  |      /:/\\\\:\\\\  \\\\     /:|:|  |      /:/\\\\:\\\\  \\\\     /:|:|  |        |:|  |      /:/\\\\:\\\\  \\\\  \\n\")\n\tsb.WriteString(\"  /:/|:|  |__   /::\\\\~\\\\:\\\\  \\\\   /:/|:|__|__   /::\\\\~\\\\:\\\\  \\\\   /:/|:|  |__      |:|__|__   /::\\\\~\\\\:\\\\  \\\\ \\n\")\n\tsb.WriteString(\" /:/ |:| /\\\\__\\\\ /:/\\\\:\\\\ \\\\:\\\\__\\\\ /:/ |::::\\\\__\\\\ /:/\\\\:\\\\ \\\\:\\\\__\\\\ /:/ |:| /\\\\__\\\\     /::::\\\\__\\\\ /:/\\\\:\\\\ \\\\:\\\\__\\\\\\n\")\n\tsb.WriteString(\" \\\\/__|:|/:/  / \\\\:\\\\~\\\\:\\\\ \\\\/__/ \\\\/__/~~/:/  / \\\\/__\\\\:\\\\/:/  / \\\\/__|:|/:/  /    /:/~~/~    \\\\/__\\\\:\\\\/:/  / \\n\")\n\tsb.WriteString(\"     |:/:/  /   \\\\:\\\\ \\\\:\\\\__\\\\         /:/  /       \\\\::/  /      |:/:/  /    /:/  /           \\\\::/  /  \\n\")\n\tsb.WriteString(\"     |::/  /     \\\\:\\\\ \\\\/__/        /:/  /        /:/  /       |::/  /     \\\\/__/            /:/  /   \\n\")\n\tsb.WriteString(\"     /:/  /       \\\\:\\\\__\\\\         /:/  /        /:/  /        /:/  /                      /:/  /    \\n\")\n\tsb.WriteString(\"     \\\\/__/         \\\\/__/         \\\\/__/         \\\\/__/         \\\\/__/                       \\\\/__/     \\n\")\n\tsb.WriteString(\"\\n```\\n\")\n\treturn sb.String()\n}\n\nfunc renderSocialLinks() string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"## Links\\n\\n\")\n\tsb.WriteString(\"You can find me here:\\n\\n\")\n\tsb.WriteString(ufmt.Sprintf(\"- [GitHub](%s) - %s\\n\", socialLinks[\"GitHub\"].URL, socialLinks[\"GitHub\"].Text))\n\tsb.WriteString(ufmt.Sprintf(\"- [LinkedIn](%s) - %s\\n\", socialLinks[\"LinkedIn\"].URL, socialLinks[\"LinkedIn\"].Text))\n\tsb.WriteString(ufmt.Sprintf(\"- [Email Me](%s) - %s\\n\", socialLinks[\"Email Me\"].URL, socialLinks[\"Email Me\"].Text))\n\tsb.WriteString(\"\\n\")\n\treturn sb.String()\n}\n\nfunc renderProjects(projectsMap map[string]Project, title string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(ufmt.Sprintf(\"## %s\\n\\n\", title))\n\tfor _, project := range projectsMap {\n\t\tif project.ImageURL != \"\" {\n\t\t\tsb.WriteString(ufmt.Sprintf(\"![%s](%s)\\n\\n\", project.Name, project.ImageURL))\n\t\t}\n\t\tsb.WriteString(ufmt.Sprintf(\"### [%s](%s)\\n\\n\", project.Name, project.URL))\n\t\tsb.WriteString(project.Description + \"\\n\\n\")\n\n\t\tif len(project.Sponsors) \u003e 0 {\n\t\t\tsb.WriteString(ufmt.Sprintf(\"#### %s Sponsors\\n\", project.Name))\n\t\t\tfor _, sponsor := range project.Sponsors {\n\t\t\t\tsb.WriteString(ufmt.Sprintf(\"- %s: %s\\n\", sponsor.Address.String(), sponsor.Amount.String()))\n\t\t\t}\n\t\t\tsb.WriteString(\"\\n\")\n\t\t}\n\t}\n\treturn sb.String()\n}\n\nfunc UpdateLink(cur realm, name, newURL string) {\n\tif !isAuthorized(cur.Previous().Address()) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\n\tif _, exists := socialLinks[name]; !exists {\n\t\tpanic(\"Link with the given name does not exist\")\n\t}\n\n\tsocialLinks[name] = SocialLink{\n\t\tURL:  newURL,\n\t\tText: socialLinks[name].Text,\n\t}\n}\n\nfunc UpdateAboutMe(cur realm, text string) {\n\tif !isAuthorized(cur.Previous().Address()) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\n\taboutMe = text\n}\n\nfunc AddGnoProject(cur realm, name, description, url, imageURL string) {\n\tif !isAuthorized(cur.Previous().Address()) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\tproject := Project{\n\t\tName:        name,\n\t\tDescription: description,\n\t\tURL:         url,\n\t\tImageURL:    imageURL,\n\t\tSponsors:    make(map[address]Sponsor),\n\t}\n\tgnoProjects[name] = project\n}\n\nfunc DeleteGnoProject(cur realm, projectName string) {\n\tif !isAuthorized(cur.Previous().Address()) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\n\tif _, exists := gnoProjects[projectName]; !exists {\n\t\tpanic(\"Project not found\")\n\t}\n\n\tdelete(gnoProjects, projectName)\n}\n\nfunc AddOtherProject(cur realm, name, description, url, imageURL string) {\n\tif !isAuthorized(cur.Previous().Address()) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\tproject := Project{\n\t\tName:        name,\n\t\tDescription: description,\n\t\tURL:         url,\n\t\tImageURL:    imageURL,\n\t\tSponsors:    make(map[address]Sponsor),\n\t}\n\totherProjects[name] = project\n}\n\nfunc RemoveOtherProject(cur realm, projectName string) {\n\tif !isAuthorized(cur.Previous().Address()) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\n\tif _, exists := otherProjects[projectName]; !exists {\n\t\tpanic(\"Project not found\")\n\t}\n\n\tdelete(otherProjects, projectName)\n}\n\nfunc isAuthorized(addr address) bool {\n\treturn addr == config.Address() || addr == config.Backup()\n}\n\nfunc SponsorGnoProject(cur realm, projectName string) {\n\tif !cur.Previous().IsUserCall() {\n\t\tpanic(\"only user-call (maketx call) accepted\")\n\t}\n\taddress_XXX := unsafe.OriginCaller()\n\tamount := unsafe.OriginSend()\n\n\tif amount.AmountOf(\"ugnot\") == 0 {\n\t\tpanic(\"Donation must include GNOT\")\n\t}\n\n\tproject, exists := gnoProjects[projectName]\n\tif !exists {\n\t\tpanic(\"Gno project not found\")\n\t}\n\n\tproject.Sponsors[address_XXX] = Sponsor{\n\t\tAddress: address_XXX,\n\t\tAmount:  project.Sponsors[address_XXX].Amount.Add(amount),\n\t}\n\n\ttotalDonations = totalDonations.Add(amount)\n\n\tgnoProjects[projectName] = project\n}\n\nfunc SponsorOtherProject(cur realm, projectName string) {\n\tif !cur.Previous().IsUserCall() {\n\t\tpanic(\"only user-call (maketx call) accepted\")\n\t}\n\taddress_XXX := unsafe.OriginCaller()\n\tamount := unsafe.OriginSend()\n\n\tif amount.AmountOf(\"ugnot\") == 0 {\n\t\tpanic(\"Donation must include GNOT\")\n\t}\n\n\tproject, exists := otherProjects[projectName]\n\tif !exists {\n\t\tpanic(\"Other project not found\")\n\t}\n\n\tproject.Sponsors[address_XXX] = Sponsor{\n\t\tAddress: address_XXX,\n\t\tAmount:  project.Sponsors[address_XXX].Amount.Add(amount),\n\t}\n\n\ttotalDonations = totalDonations.Add(amount)\n\n\totherProjects[projectName] = project\n}\n\nfunc Withdraw(cur realm) string {\n\trealmAddress := cur.Previous().Address()\n\tif !isAuthorized(realmAddress) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\n\tbanker_ := banker.NewBanker(banker.BankerTypeRealmSend, cur)\n\tcoins := banker_.GetCoins(realmAddress)\n\n\tif len(coins) == 0 {\n\t\treturn \"No coins available to withdraw\"\n\t}\n\n\tbanker_.SendCoins(realmAddress, config.Address(), coins)\n\n\treturn \"Successfully withdrew all coins to config address\"\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"4zys7LnbNxlXWvKl/1DlThMcBDBHmcz0YDMpvlOcBdtGpAKpO55iCynkYh4/pZfh/twYxGNHk85h/t6xd5U9Bw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"config","path":"gno.land/r/sacha/config","files":[{"name":"config.gno","body":"package config\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/ownable/v0\"\n)\n\nvar (\n\tOwnableMain   = ownable.NewWithAddress(\"g1jpg2hmsh8j4f60u4vkseqh5azeupns8vhrstfm\")\n\tOwnableBackup = ownable.NewWithAddress(\"g1ez7rzp7dms7he05yy9nwdczl97qw233z892hsm\")\n\n\tErrUnauthorized = errors.New(\"You're not Sacha\")\n)\n\nfunc IsAuthorized(addr address) bool {\n\treturn addr == OwnableMain.Owner() || addr == OwnableBackup.Owner()\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/sacha/config\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"hwQHW/+EGhT/3X3mcZwjrRidQf2X3x8h8Jf2aqJmwal5ZQwutWk9um4LPdkPFtA0bACYy4RK+CpJsXO4mUR1Ag=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"coinflip","path":"gno.land/r/sacha/coinflip","files":[{"name":"coinflip.gno","body":"package coinflip\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"chain/runtime\"\n\t\"chain/runtime/unsafe\"\n\t\"math/rand\"\n\n\t\"gno.land/p/demo/entropy\"\n\t\"gno.land/p/moul/fifo/v0\"\n\t\"gno.land/r/sacha/config\"\n)\n\nconst denom = \"ugnot\"\nconst minBetAmount = 1\n\nvar (\n\tbets       = []Bet{}\n\tlatestBets = fifo.New(8)\n)\n\nfunc Heads(cur realm) {\n\truntime.AssertOriginCall()\n\n\tflipResult := flip(1)\n\tbetLogic(cross(cur), flipResult)\n}\n\nfunc Tails(cur realm) {\n\truntime.AssertOriginCall()\n\n\tflipResult := flip(0)\n\tbetLogic(cross(cur), flipResult)\n}\n\nfunc GetResults(cur realm) []Bet {\n\treturn bets\n}\n\nfunc GetRealmBalance(cur realm) int64 {\n\tbanker_ := banker.NewReadonlyBanker()\n\tcoins := banker_.GetCoins(cur.Address())\n\tbalance := coins.AmountOf(denom)\n\n\treturn balance\n}\n\nfunc Withdrawal(cur realm, amount int64) {\n\tif !config.IsAuthorized(cur.Previous().Address()) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\n\truntime.AssertOriginCall()\n\n\tbanker_ := banker.NewBanker(banker.BankerTypeRealmSend, cur)\n\n\tif GetRealmBalance(cur) \u003c amount {\n\t\tpanic(\"insufficient balance\")\n\t}\n\n\tbanker_.SendCoins(cur.Address(), unsafe.OriginCaller(), chain.NewCoins(chain.NewCoin(denom, amount)))\n}\n\nfunc flip(choice int) bool {\n\tseed1 := uint64(entropy.New().Seed())\n\tseed2 := uint64(entropy.New().Seed())\n\n\tr := rand.New(rand.NewPCG(seed1, seed2))\n\tresult := r.IntN(2)\n\n\treturn result == choice\n}\n\nfunc getBet(cur realm) chain.Coin {\n\tbet := unsafe.OriginSend()\n\tif len(bet) != 1 || bet[0].Denom != denom {\n\t\tpanic(\"bet is invalid\")\n\t}\n\n\tamount := bet[0].Amount\n\n\tif amount \u003c minBetAmount {\n\t\tpanic(\"Bet too low\")\n\t}\n\tif amount*2 \u003e GetRealmBalance(cross(cur)) {\n\t\tpanic(\"Bet too high\")\n\t}\n\n\treturn bet[0]\n}\n\nfunc sendPrize(cur realm, caller address, amount int64) {\n\tbanker_ := banker.NewBanker(banker.BankerTypeRealmSend, cur)\n\twonCoins := chain.NewCoins(chain.NewCoin(denom, amount))\n\n\tbanker_.SendCoins(cur.Address(), caller, wonCoins)\n}\n\nfunc betLogic(cur realm, flipResult bool) {\n\tcaller := unsafe.OriginCaller()\n\tbet := getBet(cross(cur))\n\n\tif flipResult == true {\n\t\tsendPrize(cross(cur), caller, bet.Amount*2)\n\t\tchain.Emit(\"BetResult\", \"Result\", \"win\")\n\t} else {\n\t\tchain.Emit(\"BetResult\", \"Result\", \"lose\")\n\t}\n\n\tbets = append(bets, Bet{Address: caller, Result: flipResult, Amount: bet})\n\tlatestBets.Append(Bet{Address: caller, Result: flipResult, Amount: bet})\n}\n"},{"name":"coinflip_test.gno","body":"package coinflip\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n)\n\nvar coinflipAddr = chain.PackageAddress(\"gno.land/r/sacha/coinflip\")\n\nfunc TestGetRealmBalance(cur realm, t *testing.T) {\n\tvalue := GetRealmBalance(cross(cur))\n\n\tif value != 0 {\n\t\tt.Fatalf(\"Expected 0, got %v\", value)\n\t}\n}\n\nfunc TestFlip(cur realm, t *testing.T) {\n\tvalue := flip(1)\n\n\tif value != true {\n\t\tt.Fatalf(\"Expected true, got %v\", value)\n\t}\n\n\tvalue = flip(0)\n\n\tif value != false {\n\t\tt.Fatalf(\"Expected false, got %v\", value)\n\t}\n}\n\nfunc TestWithdrawal(cur realm, t *testing.T) {\n\tbanker_ := banker.NewReadonlyBanker()\n\tsachaAddr := address(\"g1jpg2hmsh8j4f60u4vkseqh5azeupns8vhrstfm\")\n\n\ttesting.IssueCoins(coinflipAddr, chain.Coins{{\"ugnot\", 100000000}})\n\n\ttesting.SetRealm(testing.NewUserRealm(sachaAddr))\n\n\tWithdrawal(cross(cur), 1000)\n\n\tmainbal := banker_.GetCoins(coinflipAddr)\n\tsachabal := banker_.GetCoins(sachaAddr)\n\n\tif !(mainbal[0].Amount == 99999000 \u0026\u0026 sachabal[0].Amount == 1000) {\n\t\tt.Fatalf(\"Expected main balance to be 99999100 and sacha balance to be 900, got %v and %v\", mainbal, sachabal)\n\t}\n}\n\nfunc TestSendPrize(cur realm, t *testing.T) {\n\tbanker_ := banker.NewReadonlyBanker()\n\taliceAddr := testutils.TestAddress(\"alice\")\n\n\ttesting.IssueCoins(coinflipAddr, chain.Coins{{\"ugnot\", 100000000}})\n\n\ttesting.SetRealm(testing.NewUserRealm(aliceAddr))\n\n\tsendPrize(cross(cur), aliceAddr, 1000)\n\n\tmainbal := banker_.GetCoins(coinflipAddr)\n\talicebal := banker_.GetCoins(aliceAddr)\n\tif !(mainbal[0].Amount == 99999000 \u0026\u0026 alicebal[0].Amount == 1000) {\n\t\tt.Fatalf(\"Expected main balance to be 99999100 and alice balance to be 900, got %v and %v\", mainbal, alicebal)\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/sacha/coinflip\"\ngno = \"0.9\"\n"},{"name":"render.gno","body":"package coinflip\n\nimport (\n\t\"chain\"\n\t\"strconv\"\n\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/p/moul/txlink/v0\"\n\t\"gno.land/r/sys/users\"\n)\n\nvar (\n\tgithubUsername    string\n\tpocInnovationName string\n)\n\nfunc init() {\n\tgithubUsername = \"dujardin09\"\n\tpocInnovationName = \"PoCInnovation\"\n}\n\nfunc renderResult(address address, result bool, amount chain.Coin) string {\n\taddressFmt := \"**\" + address.String()[:30] + \"**: \"\n\tresultFmt := \"lost\"\n\tamountFmt := \" **\" + amount.String() + \"** \"\n\temoji := \"📉\"\n\n\tdata := users.ResolveAddress(address)\n\tif data != nil {\n\t\taddressFmt = \"**\" + data.RenderLink(\"\") + \"**: \"\n\t}\n\n\tif result == true {\n\t\tresultFmt = \"won\"\n\t\temoji = \"📈\"\n\t}\n\n\treturn addressFmt + resultFmt + amountFmt + emoji\n}\n\nfunc renderLatestResults() string {\n\tsize := latestBets.Size()\n\n\tif size == 0 {\n\t\treturn \"No results yet\"\n\t}\n\n\tentries := latestBets.Entries()\n\n\tvar out string\n\tfor i := size - 1; i \u003e= 0; i-- {\n\t\tbet := entries[i].(Bet)\n\t\tout += md.BulletItem(renderResult(bet.Address, bet.Result, bet.Amount))\n\t}\n\treturn out\n}\n\nfunc renderGame() string {\n\tout := \"# \"\n\tout += md.Link(\"Heads\", txlink.Call(\"Heads\")) + \" or \"\n\tout += md.Link(\"Tails\", txlink.Call(\"Tails\")) + \" ?\\n\"\n\n\tout += md.H3(\"Latests Results:\\n\\n\")\n\tout += renderLatestResults()\n\n\treturn out\n}\n\nfunc renderRight(_ int, rlm realm) string {\n\tout := md.Image(\"Coin Flip Illustration\", \"https://ipfs.io/ipfs/QmVZxCKz3J5iMHrACNLfFWpfpfcKT2aSmZMfvZsCQnMWbf\")\n\tout += \"\\n\"\n\tout += renderBalance(0, rlm)\n\treturn out\n}\n\nfunc renderFooter() string {\n\tout := md.HorizontalRule()\n\tout += md.BulletList([]string{\n\t\tmd.Link(\"Home\", \"home\"),\n\t\tmd.Link(\"GitHub: @\"+githubUsername, \"https://github.com/\"+githubUsername),\n\t\tmd.Link(\"PoC-Innovation\", \"https://github.com/\"+pocInnovationName),\n\t})\n\treturn out\n}\n\nfunc renderBalance(_ int, rlm realm) string {\n\tbalance := strconv.Itoa(int(GetRealmBalance(cross(rlm))))\n\tout := md.H2(\"Balance: \" + balance + denom + \"\\n\")\n\n\treturn out\n}\n\nfunc Render(cur realm, _ string) string {\n\tout := md.Columns([]string{\n\t\trenderGame(),\n\t\trenderRight(0, cur),\n\t}, false)\n\n\tout += renderFooter()\n\n\treturn out\n}\n"},{"name":"types.gno","body":"package coinflip\n\nimport (\n\t\"chain\"\n)\n\ntype Bet struct {\n\tAddress address\n\tResult  bool\n\tAmount  chain.Coin\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"aEDOi0rtJG7eFA5EEGrFVsu4HKoEGMp4GfAEITAXqwMYxJWtxOxQLKrKJWGKxrsRcCeJ6SxDxdwHcClmrIXT3g=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"home","path":"gno.land/r/sacha/home","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/sacha/home\"\ngno = \"0.9\"\n"},{"name":"home.gno","body":"package home\n\nimport (\n\t\"gno.land/p/moul/md/v0\"\n\t\"gno.land/r/leon/hor\"\n)\n\nvar (\n\tbanner            string\n\ttitle             string\n\tstory             string\n\tgame              map[string]string\n\tart               string\n\tgithubUsername    string\n\tpocInnovationName string\n)\n\nfunc init(cur realm) {\n\thor.Register(cross(cur), \"Sacha's Home Realm\", \"A home away from home.\")\n\n\tbanner = \"https://ipfs.io/ipfs/QmWKvxG4GifnCbCp5yPMpNmkjr7jvR9rHTmwA86Daap9sY\"\n\tstory = \"In the quiet layers of gno.land, a gnome brushed against a line of forgotten code. No one saw him. No one knows what he changed. But sometimes, in the logs...\\n\\n\\\"gnonk.\\\"\\n\\nJust once. Then silence.\"\n\ttitle = \"Sacha's Space 🏗️\"\n\tgame = make(map[string]string)\n\tgame[\"coinflip\"] = \"🪙 Flip a coin!\"\n\tart = `⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣾⣿⣿⣷⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⣿⣿⣿⣦⡀⠒⢶⣄⠀⠀⠀⠀⠀⠀⠀\n⠀⢰⣶⣷⣶⣶⣤⣄⠀⣠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣾⣿⡆⠀⠀⠀⠀⠀⠀\n⠀⢿⣿⣿⣿⣿⡟⢁⣄⠙⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⠀⠀⠀⠀⠀⠀\n⠀⠘⣿⣿⣿⣿⣧⡈⠻⢷⣦⣄⡉⠛⠿⢿⣿⣿⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠈⠻⣿⣿⣿⣿⣶⣄⡈⠙⠻⢷⣶⣤⣄⣈⡉⠛⠛⠛⠃⢠⣀⣀⡀⠀⠀⠀\n⠀⠀⠀⠀⠈⠙⠻⢿⣿⣿⣿⣿⣶⣦⣤⣍⣉⠙⠛⠛⠛⠿⠃⢸⣿⣿⣿⣷⡀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠻⠿⣿⣿⣿⣿⣿⣿⣿⣷⣶⣶⣾⣿⣿⣿⣿⣿⣧⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠙⠛⠻⠏⠀⠉⠻⢿⣿⣿⣿⣿⠿⠋⠀`\n\tgithubUsername = \"dujardin09\"\n\tpocInnovationName = \"PoCInnovation\"\n}\n\nfunc renderStory() string {\n\tout := md.H2(\"📖 Story\")\n\tout += md.Paragraph(story)\n\treturn out\n}\n\nfunc renderGame() string {\n\tout := md.H2(\"🎰 Game\")\n\tfor name, title := range game {\n\t\tout += md.H3(md.Link(title, name))\n\t}\n\treturn out\n}\n\nfunc renderArt() string {\n\tout := md.H2(\"🎨 Art\")\n\tout += md.CodeBlock(art)\n\treturn out\n}\n\nfunc renderBody() string {\n\treturn md.Columns([]string{\n\t\trenderStory(),\n\t\trenderGame(),\n\t\trenderArt(),\n\t}, false)\n}\n\nfunc renderTitle() string {\n\treturn md.H1(title)\n}\n\nfunc renderBanner() string {\n\tout := md.Paragraph(md.Image(\"banner\", banner))\n\treturn out\n}\n\nfunc renderFooter() string {\n\tout := md.HorizontalRule()\n\tout += md.BulletList([]string{\n\t\tmd.Link(\"Home\", \"home\"),\n\t\tmd.Link(\"GitHub: @\"+githubUsername, \"https://github.com/\"+githubUsername),\n\t\tmd.Link(\"PoC-Innovation\", \"https://github.com/\"+pocInnovationName),\n\t})\n\treturn out\n}\n\nfunc Render(_ string) string {\n\tout := renderBanner()\n\tout += renderTitle()\n\tout += renderBody()\n\tout += renderFooter()\n\n\treturn out\n}\n"},{"name":"update.gno","body":"package home\n\nimport (\n\t\"gno.land/r/sacha/config\"\n)\n\nfunc UpdateTitle(cur realm, newTitle string) {\n\tif !config.IsAuthorized(cur.Previous().Address()) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\n\ttitle = newTitle\n}\n\nfunc UpdateBanner(cur realm, newBanner string) {\n\tif !config.IsAuthorized(cur.Previous().Address()) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\n\tbanner = newBanner\n}\n\nfunc UpdateStory(cur realm, newStory string) {\n\tif !config.IsAuthorized(cur.Previous().Address()) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\n\tstory = newStory\n}\n\nfunc UpdateArt(cur realm, newArt string) {\n\tif !config.IsAuthorized(cur.Previous().Address()) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\n\tart = newArt\n}\n\nfunc UpdateFooter(cur realm, newGitHubUsername string, newPocInnovationName string) {\n\tif !config.IsAuthorized(cur.Previous().Address()) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\n\tgithubUsername = newGitHubUsername\n\tpocInnovationName = newPocInnovationName\n}\n\nfunc RemoveGame(cur realm, name string) {\n\tif !config.IsAuthorized(cur.Previous().Address()) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\n\tif _, exists := game[name]; exists {\n\t\tdelete(game, name)\n\t} else {\n\t\tpanic(\"Game not found\")\n\t}\n}\n\nfunc AddGame(cur realm, name string, title string) {\n\tif !config.IsAuthorized(cur.Previous().Address()) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\n\tgame[name] = title\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"pqA2kzXnoRbVveDi9FqwYKgpjvgJA9iUzyC1iSrbsyY/8is/MCPMH4gE7AP61qYkLiRF6GqJw5xvuhdAwI5ZuQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1kfd9f5zlvcvy6aammcmqswa7cyjpu2nyt9qfen","package":{"name":"daodemo","path":"gno.land/r/samcrew/daodemo","files":[{"name":"daodemo.gno","body":"package daodemo\n\n// This is the most basic example of a DAO using DAOKIT.\n// It is a simple DAO that has a single admin role and a single public-relationships role.\n// It is used to demonstrate the basic functionality of DAOKIT.\n\nimport (\n\t\"gno.land/p/samcrew/basedao\"\n\t\"gno.land/p/samcrew/daocond\"\n\t\"gno.land/r/demo/profile\"\n)\n\n// DAO is the canonical public surface — a concrete *basedao.DAOWrapper,\n// NOT the daokit.DAO interface. Holding the concrete wrapper prevents\n// any /r/ from down-casting to the address-typed interface (which would\n// re-introduce a Class-2 designation-forgery surface). All mutating\n// methods on *DAOWrapper take (_ int, rlm realm, ...) and validate\n// IsCurrent before deriving the caller.\nvar DAO *basedao.DAOWrapper\n\n// TODO: add a little bit more complexity to show daocond initialization\nfunc init(cur realm) {\n\tinitialRoles := []basedao.RoleInfo{\n\t\t{Name: \"admin\", Description: \"Admin is the superuser\"},\n\t\t{Name: \"public-relationships\", Description: \"Responsible of communication with the public\"},\n\t\t{Name: \"finance-officer\", Description: \"Responsible of funds management\"},\n\t}\n\n\tinitialMembers := []basedao.Member{\n\t\t{Address: \"g126gx6p6d3da4ymef35ury6874j6kys044r7zlg\", Roles: []string{\"admin\", \"public-relationships\"}},\n\t\t{Address: \"g1ld6uaykyugld4rnm63rcy7vju4zx23lufml3jv\", Roles: []string{\"public-relationships\"}},\n\t\t{Address: \"g1r69l0vhp7tqle3a0rk8m8fulr8sjvj4h7n0tth\", Roles: []string{\"finance-officer\"}},\n\t\t{Address: \"g16jv3rpz7mkt0gqulxas56se2js7v5vmc6n6e0r\", Roles: []string{}},\n\t}\n\n\t// create the member store now to be able to use it in the condition\n\tmemberStore := basedao.NewMembersStore(initialRoles, initialMembers)\n\n\tmembersMajority := daocond.MembersThreshold(0.6, memberStore.IsMember, memberStore.MembersCount)\n\tpublicRelationships := daocond.RoleCount(1, \"public-relationships\", memberStore.HasRole)\n\tfinanceOfficer := daocond.RoleCount(1, \"finance-officer\", memberStore.HasRole)\n\n\t// and \u0026 or use va_args so you can pass as many conditions as you want\n\tadminCond := daocond.And(membersMajority, publicRelationships, financeOfficer)\n\n\tDAO = basedao.New(\u0026basedao.Config{\n\t\tName:             \"Demo DAOKIT DAO\",\n\t\tDescription:      \"This is a demo DAO built with [DAOKIT](/p/samcrew/daokit)\",\n\t\tMembers:          memberStore,\n\t\tInitialCondition: adminCond,\n\t\tGetProfileString: profile.GetStringField,\n\t\tSetProfileString: profile.SetStringField,\n\t}, cur)\n}\n\nfunc Vote(cur realm, proposalID uint64, vote daocond.Vote) {\n\tDAO.Vote(0, cur, proposalID, vote)\n}\n\nfunc Execute(cur realm, proposalID uint64) {\n\tDAO.Execute(0, cur, proposalID)\n}\n\nfunc Render(path string) string {\n\treturn DAO.Render(path)\n}\n"},{"name":"daodemo_test.gno","body":"package daodemo\n\nimport \"testing\"\n\nfunc TestInit(t *testing.T) {\n\tmembersCount := DAO.Private().Members.MembersCount()\n\tif membersCount != 4 {\n\t\tt.Fatalf(\"Expected 4 members, got %d\", membersCount)\n\t}\n\n\troles := DAO.Private().Members.GetRoles()\n\texpectedRoles := []string{\"admin\", \"public-relationships\", \"finance-officer\"}\n\tif len(roles) != len(expectedRoles) {\n\t\tt.Fatalf(\"Expected %d roles, got %d\", len(expectedRoles), len(roles))\n\t}\n\tfor _, role := range roles {\n\t\terr := true\n\t\tfor _, expectedRole := range expectedRoles {\n\t\t\tif role == expectedRole {\n\t\t\t\terr = false\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif err {\n\t\t\tt.Fatalf(\"Expected roles %v, got %v\", expectedRoles, roles)\n\t\t}\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/samcrew/daodemo\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1kfd9f5zlvcvy6aammcmqswa7cyjpu2nyt9qfen\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"+AHCF5YaCEqrZBfpSXuiGia2/nwzglKM+bhlGJhqrm95F5xCdyX3kBAhSO/LIK04tC7l6F00JSyQXLCXWKESpA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"registry","path":"gno.land/r/stefann/registry","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/stefann/registry\"\ngno = \"0.9\"\n"},{"name":"registry.gno","body":"package registry\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/ownable/v0\"\n)\n\nvar (\n\tmainAddr   address\n\tbackupAddr address\n\towner      *ownable.Ownable\n)\n\nfunc init() {\n\tmainAddr = \"g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8\"\n\tbackupAddr = \"g13awn2575t8s2vf3svlprc4dg0e9z5wchejdxk8\"\n\n\towner = ownable.NewWithAddress(mainAddr)\n}\n\nfunc MainAddr() address {\n\treturn mainAddr\n}\n\nfunc BackupAddr() address {\n\treturn backupAddr\n}\n\nfunc SetMainAddr(cur realm, addr address) error {\n\tif !addr.IsValid() {\n\t\treturn errors.New(\"config: invalid address\")\n\t}\n\n\towner.AssertOwnedBy(cur.Previous().Address())\n\n\tmainAddr = addr\n\treturn nil\n}\n\nfunc SetBackupAddr(cur realm, addr address) error {\n\tif !addr.IsValid() {\n\t\treturn errors.New(\"config: invalid address\")\n\t}\n\n\towner.AssertOwnedBy(cur.Previous().Address())\n\n\tbackupAddr = addr\n\treturn nil\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"v5yjevQy5OJ3lK0RXtvV6OXyroRm9CwH7w/9yljf2XMEq3oZMeo2EJIYhSMxV/sawccWD4RD0FiesnrCfNZbQQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"home","path":"gno.land/r/sunspirit/home","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/sunspirit/home\"\ngno = \"0.9\"\n"},{"name":"home.gno","body":"package home\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/sunspirit/md/v0\"\n)\n\nfunc Render(path string) string {\n\tvar sb strings.Builder\n\n\tsb.WriteString(md.H1(\"Sunspirit's Home\") + md.LineBreak(1))\n\n\tsb.WriteString(md.Paragraph(ufmt.Sprintf(\n\t\t\"Welcome to Sunspirit’s home! This is where I’ll bring %s to Gno.land, crafted with my experience and creativity.\",\n\t\tmd.Italic(md.Bold(\"simple, useful dapps\")),\n\t)) + md.LineBreak(1))\n\n\tsb.WriteString(md.Paragraph(ufmt.Sprintf(\n\t\t\"📚 I’ve created a Markdown rendering library at %s. Feel free to use it for your own projects!\",\n\t\tmd.Link(\"gno.land/p/sunspirit/md/v0\", \"/p/sunspirit/md/v0\"),\n\t)) + md.LineBreak(1))\n\n\tsb.WriteString(md.Paragraph(\"💬 I’d love to hear your feedback to help improve this library!\") + md.LineBreak(1))\n\n\tsb.WriteString(md.Paragraph(ufmt.Sprintf(\n\t\t\"🌐 You can check out a demo of this package in action at %s.\",\n\t\tmd.Link(\"gno.land/r/sunspirit/md\", \"/r/sunspirit/md\"),\n\t)) + md.LineBreak(1))\n\tsb.WriteString(md.HorizontalRule())\n\n\treturn sb.String()\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"KZcPVfVIdIDdxcvIMpyC2EJHzfawHX6WWxWziR4O44dDVTkQlqUSB2u7i4GJses6OFP1bgkF68Nb3Vjl/XfwFA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"md","path":"gno.land/r/sunspirit/md","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/sunspirit/md\"\ngno = \"0.9\"\n"},{"name":"md.gno","body":"package md\n\nimport (\n\t\"gno.land/p/sunspirit/md/v0\"\n\t\"gno.land/p/sunspirit/table/v0\"\n)\n\nfunc Render(path string) string {\n\ttitle := \"A simple, flexible, and easy-to-use library for creating markdown documents in gno.land\"\n\n\tmdBuilder := md.NewBuilder().\n\t\tAdd(md.H1(md.Italic(md.Bold(title)))).\n\n\t\t// Bold Text section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"1. Bold Text\")),\n\t\t\tmd.Paragraph(\"To make text bold, use the `md.Bold()` function:\"),\n\t\t\tmd.Bold(\"This is bold text\"),\n\t\t).\n\n\t\t// Italic Text section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"2. Italic Text\")),\n\t\t\tmd.Paragraph(\"To make text italic, use the `md.Italic()` function:\"),\n\t\t\tmd.Italic(\"This is italic text\"),\n\t\t).\n\n\t\t// Strikethrough Text section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"3. Strikethrough Text\")),\n\t\t\tmd.Paragraph(\"To add strikethrough, use the `md.Strikethrough()` function:\"),\n\t\t\tmd.Strikethrough(\"This text is strikethrough\"),\n\t\t).\n\n\t\t// Headers section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"4. Headers (H1 to H6)\")),\n\t\t\tmd.Paragraph(\"You can create headers (H1 to H6) using the `md.H1()` to `md.H6()` functions:\"),\n\t\t\tmd.H1(\"This is a level 1 header\"),\n\t\t\tmd.H2(\"This is a level 2 header\"),\n\t\t\tmd.H3(\"This is a level 3 header\"),\n\t\t\tmd.H4(\"This is a level 4 header\"),\n\t\t\tmd.H5(\"This is a level 5 header\"),\n\t\t\tmd.H6(\"This is a level 6 header\"),\n\t\t).\n\n\t\t// Bullet List section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"5. Bullet List\")),\n\t\t\tmd.Paragraph(\"To create bullet lists, use the `md.BulletList()` function:\"),\n\t\t\tmd.BulletList([]string{\"Item 1\", \"Item 2\", \"Item 3\"}),\n\t\t).\n\n\t\t// Ordered List section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"6. Ordered List\")),\n\t\t\tmd.Paragraph(\"To create ordered lists, use the `md.OrderedList()` function:\"),\n\t\t\tmd.OrderedList([]string{\"First\", \"Second\", \"Third\"}),\n\t\t).\n\n\t\t// Todo List section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"7. Todo List\")),\n\t\t\tmd.Paragraph(\"You can create a todo list using the `md.TodoList()` function, which supports checkboxes:\"),\n\t\t\tmd.TodoList([]string{\"Task 1\", \"Task 2\"}, []bool{true, false}),\n\t\t).\n\n\t\t// Blockquote section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"8. Blockquote\")),\n\t\t\tmd.Paragraph(\"To create blockquotes, use the `md.Blockquote()` function:\"),\n\t\t\tmd.Blockquote(\"This is a blockquote.\\nIt can span multiple lines.\"),\n\t\t).\n\n\t\t// Inline Code section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"9. Inline Code\")),\n\t\t\tmd.Paragraph(\"To insert inline code, use the `md.InlineCode()` function:\"),\n\t\t\tmd.InlineCode(\"fmt.Println() // inline code\"),\n\t\t).\n\n\t\t// Code Block section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"10. Code Block\")),\n\t\t\tmd.Paragraph(\"For multi-line code blocks, use the `md.CodeBlock()` function:\"),\n\t\t\tmd.CodeBlock(\"package main\\n\\nfunc main() {\\n\\t// Your code here\\n}\"),\n\t\t).\n\n\t\t// Horizontal Rule section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"11. Horizontal Rule\")),\n\t\t\tmd.Paragraph(\"To add a horizontal rule (separator), use the `md.HorizontalRule()` function:\"),\n\t\t\tmd.LineBreak(1),\n\t\t\tmd.HorizontalRule(),\n\t\t).\n\n\t\t// Language-specific Code Block section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"12. Language-specific Code Block\")),\n\t\t\tmd.Paragraph(\"To create language-specific code blocks, use the `md.LanguageCodeBlock()` function:\"),\n\t\t\tmd.LanguageCodeBlock(\"go\", \"package main\\n\\nfunc main() {}\"),\n\t\t).\n\n\t\t// Hyperlink section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"13. Hyperlink\")),\n\t\t\tmd.Paragraph(\"To create a hyperlink, use the `md.Link()` function:\"),\n\t\t\tmd.Link(\"Gnoland official docs\", \"https://docs.gno.land\"),\n\t\t).\n\n\t\t// Image section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"14. Image\")),\n\t\t\tmd.Paragraph(\"To insert an image, use the `md.Image()` function:\"),\n\t\t\tmd.LineBreak(1),\n\t\t\tmd.Image(\"Gnoland Logo\", \"https://gnolang.github.io/blog/2024-05-21_the-gnome/src/banner.png\"),\n\t\t).\n\n\t\t// Footnote section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"15. Footnote\")),\n\t\t\tmd.Paragraph(\"To create footnotes, use the `md.Footnote()` function:\"),\n\t\t\tmd.LineBreak(1),\n\t\t\tmd.Footnote(\"1\", \"This is a footnote.\"),\n\t\t).\n\n\t\t// Table section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"16. Table\")),\n\t\t\tmd.Paragraph(\"To create a table, use the `md.Table()` function. Here's an example of a table:\"),\n\t\t)\n\n\t// Create a table using the table package\n\ttb, _ := table.New([]string{\"Feature\", \"Description\"}, [][]string{\n\t\t{\"Bold\", \"Make text bold using \" + md.Bold(\"double asterisks\")},\n\t\t{\"Italic\", \"Make text italic using \" + md.Italic(\"single asterisks\")},\n\t\t{\"Strikethrough\", \"Cross out text using \" + md.Strikethrough(\"double tildes\")},\n\t})\n\tmdBuilder.Add(md.Table(tb))\n\n\t// Escaping Markdown section\n\tmdBuilder.Add(\n\t\tmd.H3(md.Bold(\"17. Escaping Markdown\")),\n\t\tmd.Paragraph(\"Sometimes, you need to escape special Markdown characters (like *, _, and `). Use the `md.EscapeMarkdown()` function for this:\"),\n\t)\n\n\t// Example of escaping markdown\n\ttext := \"- Escape special chars like *, _, and ` in markdown\"\n\tmdBuilder.Add(\n\t\tmd.H4(\"Text Without Escape:\"),\n\t\ttext,\n\t\tmd.LineBreak(1),\n\t\tmd.H4(\"Text With Escape:\"),\n\t\tmd.EscapeMarkdown(text),\n\t)\n\n\treturn mdBuilder.Render(md.LineBreak(1))\n}\n"},{"name":"md_test.gno","body":"package md\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestRender(t *testing.T) {\n\toutput := Render(\"\")\n\tif !strings.Contains(output, \"A simple, flexible, and easy-to-use library for creating markdown documents in gno.land\") {\n\t\tt.Errorf(\"invalid output\")\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"WoYtFrvRk+QX9LDZKh5fsk/4PHqVvxwCefcMxnJxzW5k9CI/nnsD3UC1RlDVj6/0tuYNu9jlJv9HtBc3IZ9mag=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"storage","path":"gno.land/r/tests/vm/benchmark/storage","files":[{"name":"boards.gno","body":"package storage\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/p/nt/avl/v0\"\n)\n\nvar boards avl.Tree\n\ntype Board interface {\n\tAddPost(title, content string)\n\tGetPost(id int) (Post, bool)\n\tSize() int\n}\n\n// posts are persisted in an avl tree\ntype TreeBoard struct {\n\tid    int\n\tposts *avl.Tree\n}\n\nfunc (b *TreeBoard) AddPost(title, content string) {\n\tn := b.posts.Size()\n\tp := Post{n, title, content}\n\tb.posts.Set(strconv.Itoa(n), p)\n}\n\nfunc (b *TreeBoard) GetPost(id int) (Post, bool) {\n\tp := b.posts.Get(strconv.Itoa(id))\n\tif p != nil {\n\t\treturn p.(Post), true\n\t} else {\n\t\treturn Post{}, false\n\t}\n}\n\nfunc (b *TreeBoard) Size() int {\n\treturn b.posts.Size()\n}\n\n// posts are persisted in a map\ntype MapBoard struct {\n\tid    int\n\tposts map[int]Post\n}\n\nfunc (b *MapBoard) AddPost(title, content string) {\n\tn := len(b.posts)\n\tp := Post{n, title, content}\n\tb.posts[n] = p\n}\n\nfunc (b *MapBoard) GetPost(id int) (Post, bool) {\n\tp, ok := b.posts[id]\n\tif ok {\n\t\treturn p, ok\n\t} else {\n\t\treturn Post{}, ok\n\t}\n}\n\nfunc (b *MapBoard) Size() int {\n\treturn len(b.posts)\n}\n\n// posts are persisted in a slice\ntype SliceBoard struct {\n\tid    int\n\tposts []Post\n}\n\nfunc (b *SliceBoard) AddPost(title, content string) {\n\tn := len(b.posts)\n\tp := Post{n, title, content}\n\tb.posts = append(b.posts, p)\n}\n\nfunc (b *SliceBoard) GetPost(id int) (Post, bool) {\n\tif id \u003c len(b.posts) {\n\t\tp := b.posts[id]\n\n\t\treturn p, true\n\t} else {\n\t\treturn Post{}, false\n\t}\n}\n\nfunc (b *SliceBoard) Size() int {\n\treturn len(b.posts)\n}\n\ntype Post struct {\n\tid      int\n\ttitle   string\n\tcontent string\n}\n"},{"name":"forum.gno","body":"package storage\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/p/nt/avl/v0\"\n)\n\nfunc init() {\n\t// we write to three common data structure for persistence\n\t// avl.Tree, map and slice.\n\tposts0 := avl.NewTree()\n\tb0 := \u0026TreeBoard{0, posts0}\n\tboards.Set(strconv.Itoa(0), b0)\n\n\tposts1 := make(map[int]Post)\n\tb1 := \u0026MapBoard{1, posts1}\n\tboards.Set(strconv.Itoa(1), b1)\n\n\tposts2 := []Post{}\n\tb2 := \u0026SliceBoard{2, posts2}\n\tboards.Set(strconv.Itoa(2), b2)\n}\n\n// post to all boards.\nfunc AddPost(title, content string) {\n\tfor i := 0; i \u003c boards.Size(); i++ {\n\t\tboardId := strconv.Itoa(i)\n\t\tb := boards.Get(boardId)\n\t\tif b != nil {\n\t\t\tb.(Board).AddPost(title, content)\n\t\t}\n\t}\n}\n\nfunc GetPost(boardId, postId int) string {\n\tb := boards.Get(strconv.Itoa(boardId))\n\tvar res string\n\n\tif b != nil {\n\t\tp, ok := b.(Board).GetPost(postId)\n\t\tif ok {\n\t\t\tres = p.title + \",\" + p.content\n\t\t}\n\t}\n\treturn res\n}\n\nfunc GetPostSize(boardId int) int {\n\tb := boards.Get(strconv.Itoa(boardId))\n\tvar res int\n\n\tif b != nil {\n\t\tres = b.(Board).Size()\n\t} else {\n\t\tres = -1\n\t}\n\n\treturn res\n}\n\nfunc GetBoardSize() int {\n\treturn boards.Size()\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/tests/vm/benchmark/storage\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"1UV9Hos2wCB6isiwM3KnJwodh191pBSj6yReDnrtfD4/W9cWuu/86kc7Da4egeMFDvnlncDRv05P9vMbNyDcPg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"crossrealm_c","path":"gno.land/r/tests/vm/crossrealm_c","files":[{"name":"crossrealm.gno","body":"package crossrealm_c\n\nimport (\n\t\"chain/runtime/unsafe\"\n\n\t\"gno.land/p/demo/tests/p_crossrealm/v0\"\n\t\"gno.land/r/tests/vm/crossrealm\"\n)\n\n// EntryPoint is a non-crossing helper. Callers thread their own\n// live cur as `rlm`; `passThrough` and the downstream `cross(rlm)`\n// calls forward it into crossrealm's crossing functions.\nfunc EntryPoint(_ int, rlm realm) {\n\tprintln(\"crossrealm_c  EntryPoint: \" + unsafe.PreviousRealm().PkgPath())\n\tprintln(\"crossrealm_c  EntryPoint: \" + unsafe.PreviousRealm().Address())\n\tprintln(\" \")\n\tpassThrough(0, rlm)\n}\n\nfunc passThrough(_ int, rlm realm) {\n\towner := crossrealm.EntryPoint().Owner()\n\tprintln(\"Owner from crossrealm EntryPoint no crossing: \" + owner)\n\tprintln()\n\towner2 := crossrealm.EntryPointWithCrossing(0, rlm).Owner()\n\tprintln(\"Owner from crossrealm EntryPoint with crossing: \" + owner2)\n\tprintln()\n\towner = crossrealm.PrevRealmNoCrossing().Owner()\n\tprintln(\"Owner from crossrealm PrevRealmNoCrossing: \" + owner)\n\tprintln()\n\towner = crossrealm.PrevRealmCrossing(cross(rlm)).Owner()\n\tprintln(\"Owner from crossrealm PrevRealmCrossing: \" + owner)\n}\n\nfunc CurRealmEntryPoint(_ int, rlm realm) {\n\n\tprintln(\"crossrealm CurRealmWithCrossing: \" + crossrealm.CurRealmCrossing(cross(rlm)).Address())\n\tprintln(\"crossrealm CurRealmNoCrossing: \" + crossrealm.CurRealmNoCrossing().Address())\n\n\tprintln(\"p_crossrealm CurRealm: \" + p_crossrealm.CurrentRealm().Address())\n\tprintln(\"crossrealm PkgCurRealmWithCrossing: \" + crossrealm.PkgCurRealmCrossing(cross(rlm)).Address())\n\tprintln(\"crossrealm PkgCurRealmNoCrossing: \" + crossrealm.PkgCurRealmNoCrossing().Address())\n\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/tests/vm/crossrealm_c\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"gKvUl0ZaRFcnr0qKdL4w1aqXM82dgp7Tt4ZITWRw14xiGVams8xacqUWSbX0f6krWLy9o/ftavMxEtz528cOJw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"deep","path":"gno.land/r/tests/vm/deep/very/deep","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/tests/vm/deep/very/deep\"\ngno = \"0.9\"\n"},{"name":"render.gno","body":"package deep\n\n// This can't be called from a MsgCall as it isn't crossing.\nfunc Render(path string) string {\n\tif path == \"\" {\n\t\treturn \"it works!\"\n\t} else {\n\t\treturn \"hi \" + path\n\t}\n}\n\n// For testing with MsgCall.\nfunc RenderCrossing(cur realm, path string) string {\n\tif path == \"\" {\n\t\treturn \"it works!\"\n\t} else {\n\t\treturn \"hi \" + path\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"v9tpHJnC2/5hfPvkf0xqe8hvYAu2lnwd0FKmuqhcmo4pvm2rq7I2EsgATUaXi4BxtKfIiTow/XaUOLbPi3fXHw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"init","path":"gno.land/r/tests/vm/init","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/tests/vm/init\"\ngno = \"0.9\"\n"},{"name":"init.gno","body":"package init\n\nvar (\n\ts string\n)\n\nfunc init() {\n\ts = \"hello\"\n}\n\nfunc main() {\n}\n"},{"name":"init_test.gno","body":"package init\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nvar (\n\th = s\n)\n\nfunc TestInit(t *testing.T) {\n\tuassert.Empty(t, h)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"Cwpus8lP/jmgtjkTeMM/LOfftrNcBFmozUr0vjKq1KVbkAo2ikWvE05t28evaSNnZsRID6g6YO7OyW0lqjeLbA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"map_delete","path":"gno.land/r/tests/vm/map_delete","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/tests/vm/map_delete\"\ngno = \"0.9\"\n"},{"name":"map_delete.gno","body":"package map_delete\n\nvar mapus map[uint64]string = make(map[uint64]string)\n\nfunc init() {\n\tmapus[3] = \"three\"\n\tmapus[5] = \"five\"\n\tmapus[9] = \"nine\"\n}\n\nfunc DeleteMap(_ realm, k uint64) {\n\tdelete(mapus, k)\n}\n\nfunc GetMap(k uint64) bool {\n\t_, exist := mapus[k]\n\treturn exist\n}\n"},{"name":"map_delete_test.gno","body":"package map_delete\n\nimport \"testing\"\n\nfunc TestGetMap(t *testing.T) {\n\tif !(GetMap(3)) {\n\t\tt.Error(\"Expected true, got \", GetMap(3))\n\t}\n}\n\nfunc TestDeleteMap(cur realm, t *testing.T) {\n\tDeleteMap(cross(cur), 3)\n}\n\nfunc TestGetMapAfterDelete(t *testing.T) {\n\tif GetMap(3) {\n\t\tt.Error(\"Expected false, got \", GetMap(3))\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"B83MCb+gU68qLJVBf1jg9fndjf+ilBZYyHOL8vhai5NcNeEG/RP0VxKCzCF7joGLNE26/vjwgZcrGKZgRgyGJA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"skipheight","path":"gno.land/r/tests/vm/skipheight","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/tests/vm/skipheight\"\ngno = \"0.9\"\n"},{"name":"skiptime.gno","body":"package skipheight\n"},{"name":"skiptime_test.gno","body":"package skipheight\n\nimport (\n\t\"chain/runtime\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestSkipHeights(t *testing.T) {\n\toldHeight := runtime.ChainHeight()\n\tshouldEQ(t, oldHeight, 123)\n\n\toldNow := time.Now().Unix()\n\tshouldEQ(t, oldNow, 1234567890)\n\n\t// skip 3 blocks == 15 seconds\n\ttesting.SkipHeights(3)\n\n\tshouldEQ(t, runtime.ChainHeight()-oldHeight, 3)\n\tshouldEQ(t, time.Now().Unix()-oldNow, 15)\n}\n\nfunc shouldEQ(t *testing.T, got, expected int64) {\n\tif got != expected {\n\t\tt.Fatalf(\"expected %d, got %d.\", expected, got)\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"ozacaiuNisosBeor0bf9VYFnHjw8Ibs4O9EBgu9IK8cMlEcNmTa4rWootzoKK6OPzfDPjBXEU/FaAhxP4a28vA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"types","path":"gno.land/r/tests/vm/types","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/tests/vm/types\"\ngno = \"0.9\"\n"},{"name":"types.gno","body":"// package to test types behavior in various conditions (TXs, imports).\npackage types\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"gno.land/p/nt/avl/v0\"\n)\n\nvar (\n\tgInt         int      = -42\n\tgUint        uint     = 42\n\tgString      string   = \"a string\"\n\tgStringSlice []string = []string{\"a\", \"string\", \"slice\"}\n\tgError       error    = errors.New(\"an error\")\n\tgIntSlice    []int    = []int{-42, 0, 42}\n\tgUintSlice   []uint   = []uint{0, 42, 84}\n\tgTree        avl.Tree\n\t// gInterface  = any{-42, \"a string\", uint(42)}\n)\n\nfunc init() {\n\tgTree.Set(\"a\", \"content of A\")\n\tgTree.Set(\"b\", \"content of B\")\n}\n\nfunc Noop()                     {}\nfunc RetTimeNow() time.Time     { return time.Now() }\nfunc RetString() string         { return gString }\nfunc RetStringPointer() *string { return \u0026gString }\nfunc RetUint() uint             { return gUint }\nfunc RetInt() int               { return gInt }\nfunc RetUintPointer() *uint     { return \u0026gUint }\nfunc RetIntPointer() *int       { return \u0026gInt }\nfunc RetTree() avl.Tree         { return gTree }\nfunc RetIntSlice() []int        { return gIntSlice }\nfunc RetUintSlice() []uint      { return gUintSlice }\nfunc RetStringSlice() []string  { return gStringSlice }\nfunc RetError() error           { return gError }\nfunc Panic()                    { panic(\"PANIC!\") }\n\n// TODO: floats\n// TODO: typed errors\n// TODO: ret interface\n// TODO: recover\n// TODO: take types as input\n\nfunc Render(path string) string {\n\treturn \"package to test data types.\"\n}\n"},{"name":"types_test.gno","body":"package types\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"mESbJPpQMd7nPXb5YBjNe+1kkX3M32xsp41pNJL/LVphtLzW8tZ2fgIsz/TGdZ+T1Mc5Rq7GfGcJj6X7CyCrlA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"registry","path":"gno.land/r/ursulovic/registry","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/ursulovic/registry\"\ngno = \"0.9\"\n"},{"name":"registry.gno","body":"package registry\n\nimport \"errors\"\n\nvar (\n\tmainAddress   address\n\tbackupAddress address\n\n\tErrInvalidAddr  = errors.New(\"Ivan's registry: Invalid address\")\n\tErrUnauthorized = errors.New(\"Ivan's registry: Unauthorized\")\n)\n\nfunc init() {\n\tmainAddress = \"g1d24j8fwnc0w5q427fauyey4gdd30qgu69k6n0x\"\n\tbackupAddress = \"g1mw2xft3eava9kfhqw3fjj3kkf3pkammty0mtv7\"\n}\n\nfunc MainAddress() address {\n\treturn mainAddress\n}\n\nfunc BackupAddress() address {\n\treturn backupAddress\n}\n\nfunc SetMainAddress(cur realm, addr address) error {\n\tassertAuthorized(0, cur)\n\n\tif !addr.IsValid() {\n\t\treturn ErrInvalidAddr\n\t}\n\n\tmainAddress = addr\n\treturn nil\n}\n\nfunc SetBackupAddress(cur realm, addr address) error {\n\tassertAuthorized(0, cur)\n\n\tif !addr.IsValid() {\n\t\treturn ErrInvalidAddr\n\t}\n\n\tbackupAddress = addr\n\treturn nil\n}\n\n// assertAuthorized checks that the caller realm (rlm.Previous()) is the\n// main or backup address. rlm must be the caller's live cur — the\n// IsCurrent gate makes the helper safe-by-construction so a future\n// non-crossing call site can't silently bypass auth by passing a\n// stashed or sibling-frame realm value. See\n// docs/resources/gno-security.md (Class-2 designation forgery).\nfunc assertAuthorized(_ int, rlm realm) {\n\tif !rlm.IsCurrent() {\n\t\tpanic(ErrUnauthorized)\n\t}\n\tcaller := rlm.Previous().Address()\n\tisAuthorized := caller == mainAddress || caller == backupAddress\n\n\tif !isAuthorized {\n\t\tpanic(ErrUnauthorized)\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"oWHldCiDqgfjAgwi+PNFEEeFT+OVHiVDk9XW9vbTLbkvU0umAKTjHBH3XcT/CRb0HVEujyzw7yNuwwK7B9+12w=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"authority","path":"gno.land/p/aib/authority/v0","files":[{"name":"authority.gno","body":"// Package authority is the admin gate shared by the AIB realms: a member set\n// bootstrapped with the realm's deployer and the GovDAO proxy address.\n//\n// The principal is the address of the realm or user that called the realm\n// function holding the live cur, cur.Previous().Address(), never the\n// transaction signer. A direct MsgCall from a member passes; a call routed\n// through another realm, or through MsgRun, presents that realm's address and\n// is refused; a GovDAO proposal callback declared by the realm presents the\n// GovDAO proxy, which is a member from the start.\npackage authority\n\nimport (\n\t\"chain\"\n\n\t\"gno.land/p/moul/addrset/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// GovDAOPath is the GovDAO proxy realm. Its package address is a member from\n// the start: a proposal executor runs the realm's callback with the proxy as\n// the previous realm, so that is the address the gate sees.\nconst GovDAOPath = \"gno.land/r/gov/dao\"\n\n// Authority is a member set.\ntype Authority struct {\n\tmembers addrset.Set\n\t// bootstrapped is false only when the deployer could not be captured at\n\t// init, which happens in the gno test framework (the origin caller is\n\t// empty there). Until then the first AddMember is ungated and completes\n\t// the bootstrap. On chain, init always sees the deployer: MsgAddPackage\n\t// runs it as the creator, and so does MsgEnablePackage on an inert chain.\n\tbootstrapped bool\n}\n\n// New returns an authority whose members are deployer (skipped when empty, see\n// bootstrapped) and the GovDAO proxy. Call it from the realm's\n// init(cur realm) with cur.Previous().Address().\nfunc New(deployer address) *Authority {\n\ta := \u0026Authority{}\n\tif deployer != \"\" {\n\t\ta.members.Add(deployer)\n\t\ta.bootstrapped = true\n\t}\n\ta.members.Add(chain.PackageAddress(GovDAOPath))\n\treturn a\n}\n\n// NewWithMembers returns an authority whose members are exactly the given\n// addresses (empty ones skipped), with no deployer and no test-framework\n// bootstrap: every later AddMember is gated by the members. A realm whose\n// administration belongs to governance realms (ADR pr6375 R14: the IBC realms\n// keep the GovDAO proxy and the ibcgov realm, and drop the deployer) calls it\n// from init with their package addresses.\nfunc NewWithMembers(members ...address) *Authority {\n\ta := \u0026Authority{bootstrapped: true}\n\tfor _, m := range members {\n\t\tif m != \"\" {\n\t\t\ta.members.Add(m)\n\t\t}\n\t}\n\tif a.members.Size() == 0 {\n\t\tpanic(\"authority: no member\")\n\t}\n\treturn a\n}\n\n// Assert panics unless the caller of the realm function holding rlm is a\n// member. rlm must be that realm's live cur: a stored or forwarded realm value\n// is rejected, so a foreign realm cannot replay a captured cur to impersonate\n// a member. op names the operation in the panic message.\nfunc (a *Authority) Assert(_ int, rlm realm, op string) {\n\tif !rlm.IsCurrent() {\n\t\tpanic(op + \": rlm is not the caller's live realm\")\n\t}\n\tif caller := rlm.Previous().Address(); !a.members.Has(caller) {\n\t\tpanic(ufmt.Sprintf(\"%s: caller %s is not a member of the authority\", op, caller))\n\t}\n}\n\n// AddMember adds addr. Members only, except for the test-framework bootstrap\n// described on the bootstrapped field.\nfunc (a *Authority) AddMember(_ int, rlm realm, addr address) {\n\tif addr == \"\" {\n\t\tpanic(\"authority: empty address\")\n\t}\n\tif a.bootstrapped {\n\t\ta.Assert(0, rlm, \"add_authority_member\")\n\t}\n\ta.bootstrapped = true\n\ta.members.Add(addr)\n}\n\n// RemoveMember removes addr. The last member cannot be removed: an empty\n// authority would leave the realm without anyone able to administer it.\nfunc (a *Authority) RemoveMember(_ int, rlm realm, addr address) {\n\ta.Assert(0, rlm, \"remove_authority_member\")\n\tif !a.members.Has(addr) {\n\t\tpanic(\"authority: \" + string(addr) + \" is not a member\")\n\t}\n\tif a.members.Size() == 1 {\n\t\tpanic(\"authority: cannot remove the last member\")\n\t}\n\ta.members.Remove(addr)\n}\n\n// IsMember reports whether addr is a member.\nfunc (a *Authority) IsMember(addr address) bool {\n\treturn a.members.Has(addr)\n}\n\n// Members returns the members in address order.\nfunc (a *Authority) Members() []address {\n\tvar out []address\n\ta.members.Tree().Iterate(\"\", \"\", func(k string, _ any) bool {\n\t\tout = append(out, address(k))\n\t\treturn false\n\t})\n\treturn out\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/aib/authority/v0\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"2Kdux9MP1VQj4VgBlSg3zOIdUJz8f92HGhkcrm0L6Uw8LgwSg7xUC1GivIk0/z1UV181Z1AWGuO8WT90/xdaBg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"jsonmd","path":"gno.land/p/aib/jsonmd/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/aib/jsonmd/v0\"\ngno = \"0.9\"\n"},{"name":"jsonmd.gno","body":"// Package jsonmd makes a JSON document safe to return from Render, whose\n// output gnoweb parses as Markdown: every character of a string literal that\n// Markdown or HTML could act on is written as a \\uXXXX escape, so the page\n// shows the data as inert text while a JSON parser reads exactly the same\n// values. A realm's JSON pages carry user-controlled strings (an undecodable\n// receiver kept on an owed sweep entry, a denom a counterparty chose, a path\n// segment echoed in an error): without this, a string such as\n// \"[claim](https://attacker.example)\" becomes a link on the realm's page.\npackage jsonmd\n\nimport \"gno.land/p/nt/markdown/sanitize/v0\"\n\nconst hexDigits = \"0123456789abcdef\"\n\n// Inert returns the JSON text doc with every Markdown- or HTML-active\n// character inside its string literals escaped (see Quote). Text outside\n// string literals (numbers, true, false, null, and the structure) is kept:\n// it cannot form a link, an image, an autolink, raw HTML or emphasis. doc is\n// expected to be valid JSON; a string left open runs to the end.\nfunc Inert(doc string) string {\n\tout := make([]byte, 0, len(doc)+len(doc)/8)\n\tin := false\n\tfor i := 0; i \u003c len(doc); i++ {\n\t\tc := doc[i]\n\t\tif !in {\n\t\t\tout = append(out, c)\n\t\t\tif c == '\"' {\n\t\t\t\tin = true\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tswitch {\n\t\tcase c == '\"':\n\t\t\tout = append(out, c)\n\t\t\tin = false\n\t\tcase c == '\\\\' \u0026\u0026 i+1 \u003c len(doc):\n\t\t\t// An escape sequence is kept as written: it is already inert\n\t\t\t// (a backslash before a letter or a quote).\n\t\t\tout = append(out, c, doc[i+1])\n\t\t\ti++\n\t\tcase active(doc, i):\n\t\t\tout = appendU(out, rune(c))\n\t\tdefault:\n\t\t\tout = append(out, c)\n\t\t}\n\t}\n\treturn string(out)\n}\n\n// Quote is s as a JSON string literal, inert in Markdown: the quote, the\n// backslash, control characters and every Markdown- or HTML-active ASCII\n// character are \\u escapes, and so is every non-ASCII character (no bidi\n// control or lookalike reaches the page). Invalid UTF-8 becomes U+FFFD. It\n// decodes, as JSON, to s (with invalid UTF-8 replaced).\nfunc Quote(s string) string {\n\tout := make([]byte, 0, len(s)+2)\n\tout = append(out, '\"')\n\tfor i, r := range s {\n\t\tswitch {\n\t\tcase r == '\"' || r == '\\\\' || r \u003c 0x20 || r \u003e= 0x7f || active(s, i):\n\t\t\tout = appendU(out, r)\n\t\tdefault:\n\t\t\tout = append(out, byte(r))\n\t\t}\n\t}\n\treturn string(append(out, '\"'))\n}\n\n// active reports whether s[i], an ASCII byte, could take part in Markdown\n// or HTML structure: links and images ([ ] ( ) !), code spans (`), emphasis\n// and strikethrough (* ~, and an underscore not inside a word), raw HTML,\n// autolinks and entities (\u003c \u003e \u0026), headings (#) and table cells (|), and\n// gnoweb's own inline extensions: TeX math ($, and the backslash, always\n// escaped) and mentions (@). (gnoweb's other extensions, alerts, buttons,\n// columns, forms, frames, icons and foreign blocks, open with \u003c or \u003e or ],\n// all escaped already.)\nfunc active(s string, i int) bool {\n\tswitch s[i] {\n\tcase '`', '*', '[', ']', '(', ')', '~', '\u003c', '\u003e', '#', '!', '\u0026', '|', '$', '@':\n\t\treturn true\n\tcase '_':\n\t\treturn i == 0 || i+1 \u003e= len(s) || !word(s[i-1]) || !word(s[i+1])\n\t}\n\treturn false\n}\n\nfunc word(c byte) bool {\n\treturn c \u003e= '0' \u0026\u0026 c \u003c= '9' || c \u003e= 'a' \u0026\u0026 c \u003c= 'z' || c \u003e= 'A' \u0026\u0026 c \u003c= 'Z'\n}\n\n// appendU appends r as one \\uXXXX escape, or a UTF-16 surrogate pair.\nfunc appendU(out []byte, r rune) []byte {\n\tif r \u003e 0xffff {\n\t\tr -= 0x10000\n\t\tout = appendU(out, 0xd800+(r\u003e\u003e10)\u00260x3ff)\n\t\treturn appendU(out, 0xdc00+r\u00260x3ff)\n\t}\n\treturn append(out, '\\\\', 'u', hexDigits[r\u003e\u003e12\u00260xf], hexDigits[r\u003e\u003e8\u00260xf], hexDigits[r\u003e\u003e4\u00260xf], hexDigits[r\u00260xf])\n}\n\n// Cell is s as a Markdown table cell, inert: sanitize.TableCell, then the\n// gnoweb extension delimiters it leaves active, TeX math ($) and mentions\n// (@), written as character references, which show the character and open\n// nothing.\nfunc Cell(s string) string { return gnowebInert(sanitize.TableCell(s)) }\n\n// Text is s as inline Markdown text, inert: sanitize.InlineText, then the\n// gnoweb extension delimiters as in Cell.\nfunc Text(s string) string { return gnowebInert(sanitize.InlineText(s)) }\n\nfunc gnowebInert(s string) string {\n\tout := make([]byte, 0, len(s))\n\tfor i := 0; i \u003c len(s); i++ {\n\t\tswitch s[i] {\n\t\tcase '$':\n\t\t\tout = append(out, \"\u0026#36;\"...)\n\t\tcase '@':\n\t\t\tout = append(out, \"\u0026#64;\"...)\n\t\tdefault:\n\t\t\tout = append(out, s[i])\n\t\t}\n\t}\n\treturn string(out)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"Dw7OE2nVxEG3+hYS3s1zlBcS62pEaUc0zR9DLEBiIiktdtmZ9Y1vbLwkMCJdJsSOwyL18Lgfu5cAsnNBPs87Fg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"decisions","path":"gno.land/p/aib/decisions/v0","files":[{"name":"VENDORED.md","body":"Originally from gnolang/bridges, branch `ethereum-draft`, commit\na58efe81c9ebb8849ccc328d7abb0e025d63b90b, `solana/realm/p/decisions/v0`\n(`decisions.gno`, `decisions_test.gno`), Copyright (C) 2026 NewTendermint, LLC,\nlicensed under the GNO Network General Public License (see that repository's\nLICENSE.md, including its strong-attribution additional terms). Vendored here as\n`gno.land/p/aib/decisions/v0` (ADR pr6375 open question 8: vendor under our own\nnamespace rather than import the bridge operator's deployment).\n\nLocal changes: package and constant documentation, `UnpauseDelay`,\n`Decision.Copy` and `Ledger.Find`, and tests for them.\n"},{"name":"decisions.gno","body":"// Package decisions is the ledger a governed realm keeps of its decisions: a decision is born when a proposal made\n// with one of the realm's builders executes (or the governor runs it before the yield), gets the next number, and is\n// either applied at once or queued until a due time; anyone applies a queued one after its delay, and a cancel\n// decision can withdraw it in between. The ledger keeps no closure and no proposal text: the parameters a builder\n// wrote are the record. The realm decides what a decision does; this package keeps the numbers, the clocks and the\n// page.\n//\n// Vendored from gnolang/bridges (branch ethereum-draft, commit a58efe81c9eb), solana/realm/p/decisions/v0\n// (Copyright (C) 2026 NewTendermint, LLC, under the GNO Network General Public License; see VENDORED.md). Changes\n// from the original: the package documentation, UnpauseDelay, Decision.Copy and Ledger.Find, so a realm can hand out\n// a decision by value and look one up without exposing the stored pointer.\npackage decisions\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/aib/jsonmd/v0\"\n\n\t\"gno.land/p/nt/avl/v0\"\n)\n\nconst (\n\t// Day is a day of block time, in seconds.\n\tDay = int64(86400)\n\t// Factor divides every delay of the realms (Delay, Deadline, UnpauseDelay): 1 for the mainnet build; a devnet\n\t// build may be deployed with a larger value so its delays pass faster. A compiled constant shown on every\n\t// realm's page. Meters are rates per real day and are not divided.\n\tFactor = int64(168)\n\t// Delay is the wait every loosening decision takes after it is recorded (ADR pr6375 §7.5: 7 days of block\n\t// time), divided by Factor.\n\tDelay = 7 * Day / Factor\n\t// UnpauseDelay is the wait of an unpause decision where one applies (ADR pr6375 §5.4: 24 hours, on Ethereum),\n\t// divided by Factor. On gno.land unpauses are immediate.\n\tUnpauseDelay = Day / Factor\n\t// Deadline is how old a proposal may be when it executes; past it the closure returns an error and GovDAO\n\t// marks the proposal denied for good (14 days), divided by Factor.\n\tDeadline = 14 * Day / Factor\n\t// PageMax is how many decisions of one kind and state the page shows.\n\tPageMax = 100\n\t// DAOPath is the realm whose frame every builder's closure must be called from, once the governor has yielded\n\t// (or from the first block, when none was compiled): the proxy that executes GovDAO's proposals.\n\tDAOPath = \"gno.land/r/gov/dao\"\n)\n\n// ------------------------------------------------------------- governor --\n\n// The governor. A governed realm may compile a governor address; while one is compiled and has not yielded, a\n// decision is the governor's own direct call through the realm's Govern (the same closure, checks, queues and delays a\n// proposal's execution runs, only who decides differs) and GovDAO's proposals are refused. The yield is one way: from\n// that block DAOPath's proxy is the only governor. A compiled governor of \"\" means GovDAO from the first block. This\n// package keeps the rule and the page line; the realms keep the identity checks, since an identity is read from a live\n// cur alone.\n\n// DAOGoverns is whether DAOPath's proxy decides now: no governor was compiled, or the governor yielded at that\n// block height (0 until the yield).\nfunc DAOGoverns(governor address, yielded int64) bool { return governor == \"\" || yielded != 0 }\n\n// GovernorLine is the line a governed realm's page shows of who governs it.\nfunc GovernorLine(governor address, yielded int64) string {\n\tswitch {\n\tcase governor == \"\":\n\t\treturn \"governor: \" + DAOPath + \" from the first block (no governor was compiled)\"\n\tcase yielded == 0:\n\t\treturn \"governor: \" + string(governor) + \" (until the yield) — it alone decides, by Govern from its own key, and \" + DAOPath + \"'s proposals are refused\"\n\t}\n\treturn \"governor: \" + DAOPath + \" since height \" + itoa(yielded) + \" (\" + string(governor) + \" yielded)\"\n}\n\n// GovernorJSON is the same for the agents: `\"governor\":\"…\",\"yielded\":H` (no leading or trailing comma).\nfunc GovernorJSON(governor address, yielded int64) string {\n\treturn \"\\\"governor\\\":\" + strconv.Quote(string(governor)) + \",\\\"yielded\\\":\" + itoa(yielded)\n}\n\n// Decision is one numbered change, as the realm's page shows it.\ntype Decision struct {\n\tID        int64\n\tKind      string // the builder's name, e.g. \"set cap floor\"\n\tText      string // the parameters, as the builder wrote them into the proposal's description\n\tParams    any    // the parameters, typed, the realm's own\n\tMade      int64  // unix time the proposal was made (the builder call)\n\tRecorded  int64  // unix time the proposal executed: the decision's birth\n\tDue       int64  // unix time it may be applied: Recorded for an at-once change, Recorded + Delay for a queued one\n\tApplied   int64  // unix time it was applied; 0 while pending\n\tCancelled int64  // unix time GovDAO cancelled it; 0 if it was not\n\tSpent     bool   // recorded with no effect (an unpause that would have cleared nothing)\n\tNote      string // what happened at execution, in words\n}\n\n// Copy is the decision by value, safe to hand to another realm: Params is left out, since it holds the realm's own\n// typed parameters, which may be pointers into its state.\nfunc (d *Decision) Copy() Decision {\n\tc := *d\n\tc.Params = nil\n\treturn c\n}\n\n// Pending is whether the decision still waits to be applied.\nfunc (d *Decision) Pending() bool { return d.Applied == 0 \u0026\u0026 d.Cancelled == 0 \u0026\u0026 !d.Spent }\n\n// Queued is whether the decision waits on its delay (pending, and not due at once).\nfunc (d *Decision) Queued() bool { return d.Pending() \u0026\u0026 d.Due \u003e d.Recorded }\n\n// State is \"pending\", \"applied\", \"cancelled\" or \"spent\".\nfunc (d *Decision) State() string {\n\tswitch {\n\tcase d.Cancelled != 0:\n\t\treturn \"cancelled\"\n\tcase d.Spent:\n\t\treturn \"spent\"\n\tcase d.Applied != 0:\n\t\treturn \"applied\"\n\t}\n\treturn \"pending\"\n}\n\n// Ledger numbers and keeps a realm's decisions. The zero value is empty.\ntype Ledger struct {\n\tseq int64\n\tall avl.Tree // key(id) → *Decision\n}\n\nfunc key(id int64) string {\n\ts := strconv.FormatInt(id, 10)\n\treturn strings.Repeat(\"0\", 12-len(s)) + s\n}\n\n// Record gives a decision the next number and keeps it. due is when it may be applied (now for at once).\nfunc (l *Ledger) Record(kind, text string, params any, made, now, due int64) *Decision {\n\tl.seq++\n\td := \u0026Decision{ID: l.seq, Kind: kind, Text: text, Params: params, Made: made, Recorded: now, Due: due}\n\tl.all.Set(key(d.ID), d)\n\treturn d\n}\n\n// Get is the decision with this number, or nil.\nfunc (l *Ledger) Get(id int64) *Decision {\n\tv := l.all.Get(key(id))\n\tif v == nil {\n\t\treturn nil\n\t}\n\treturn v.(*Decision)\n}\n\n// Find is the decision with this number, by value (see Copy), and whether it exists.\nfunc (l *Ledger) Find(id int64) (Decision, bool) {\n\td := l.Get(id)\n\tif d == nil {\n\t\treturn Decision{}, false\n\t}\n\treturn d.Copy(), true\n}\n\n// Last is the number of the newest decision (0 if none).\nfunc (l *Ledger) Last() int64 { return l.seq }\n\n// Size is how many decisions were ever recorded.\nfunc (l *Ledger) Size() int { return l.all.Size() }\n\n// Apply marks a decision applied now, with a note.\nfunc (l *Ledger) Apply(d *Decision, now int64, note string) {\n\td.Applied = now\n\tif note != \"\" {\n\t\td.Note = note\n\t}\n}\n\n// Spend marks a decision spent: recorded, no effect.\nfunc (l *Ledger) Spend(d *Decision, note string) {\n\td.Spent = true\n\td.Note = note\n}\n\n// Cancel marks a pending decision cancelled now. It errors on a number that is not pending.\nfunc (l *Ledger) Cancel(id, now int64) (*Decision, error) {\n\td := l.Get(id)\n\tif d == nil {\n\t\treturn nil, errors.New(\"no decision \" + strconv.FormatInt(id, 10))\n\t}\n\tif !d.Pending() {\n\t\treturn nil, errors.New(\"decision \" + strconv.FormatInt(id, 10) + \" is \" + d.State() + \", not pending\")\n\t}\n\td.Cancelled = now\n\treturn d, nil\n}\n\n// Due is a pending decision whose delay has passed, by number; it errors otherwise.\nfunc (l *Ledger) Due(id, now int64) (*Decision, error) {\n\td := l.Get(id)\n\tif d == nil {\n\t\treturn nil, errors.New(\"no decision \" + strconv.FormatInt(id, 10))\n\t}\n\tif !d.Pending() {\n\t\treturn nil, errors.New(\"decision \" + strconv.FormatInt(id, 10) + \" is \" + d.State() + \", not pending\")\n\t}\n\tif now \u003c d.Due {\n\t\treturn nil, errors.New(\"decision \" + strconv.FormatInt(id, 10) + \" is due at \" + strconv.FormatInt(d.Due, 10) + \", not before\")\n\t}\n\treturn d, nil\n}\n\n// Each walks every decision, oldest first, until fn returns true.\nfunc (l *Ledger) Each(fn func(d *Decision) bool) {\n\tl.all.Iterate(\"\", \"\", func(_ string, v any) bool { return fn(v.(*Decision)) })\n}\n\n// Expired is the one error a builder's closure returns: the proposal was made more than Deadline ago, so GovDAO\n// marks it denied for good. Everything else a closure refuses is a panic, which leaves the proposal for a retry.\nfunc Expired(made, now int64) error {\n\tif now-made \u003e Deadline {\n\t\treturn errors.New(\"the proposal is past its 14-day deadline (made at \" + strconv.FormatInt(made, 10) + \", now \" + strconv.FormatInt(now, 10) + \")\")\n\t}\n\treturn nil\n}\n\n// ---------------------------------------------------------------- pages --\n\nfunc itoa(n int64) string { return strconv.FormatInt(n, 10) }\n\n// Clocks says the clock factor and the delays it gives, for every realm's page; the mainnet build's factor is 1.\nfunc Clocks() string {\n\treturn \"clock factor \" + itoa(Factor) + \" (1 on mainnet; a devnet build is compiled with 8): the 7-day delay is \" + itoa(Delay) +\n\t\t\" s and the 14-day deadline \" + itoa(Deadline) + \" s of block time\"\n}\n\n// ClocksJSON is the same for the agents: {\"factor\":…,\"delay\":…,\"deadline\":…}.\nfunc ClocksJSON() string {\n\treturn \"{\\\"factor\\\":\" + itoa(Factor) + \",\\\"delay\\\":\" + itoa(Delay) + \",\\\"deadline\\\":\" + itoa(Deadline) + \"}\"\n}\n\n// Page is the decisions page: for every kind, the pending decisions (up to PageMax) and the last PageMax executed\n// ones, newest first.\nfunc (l *Ledger) Page() string {\n\tif l.all.Size() == 0 {\n\t\treturn \"No decision yet: one exists only when a proposal made with a builder executes.\\n\"\n\t}\n\tkinds := []string{}\n\tpending := map[string][]*Decision{}\n\tdone := map[string][]*Decision{}\n\tl.all.ReverseIterate(\"\", \"\", func(_ string, v any) bool {\n\t\td := v.(*Decision)\n\t\tif _, ok := pending[d.Kind]; !ok {\n\t\t\tkinds = append(kinds, d.Kind)\n\t\t\tpending[d.Kind] = nil\n\t\t\tdone[d.Kind] = nil\n\t\t}\n\t\tif d.Pending() {\n\t\t\tif len(pending[d.Kind]) \u003c PageMax {\n\t\t\t\tpending[d.Kind] = append(pending[d.Kind], d)\n\t\t\t}\n\t\t} else if len(done[d.Kind]) \u003c PageMax {\n\t\t\tdone[d.Kind] = append(done[d.Kind], d)\n\t\t}\n\t\treturn false\n\t})\n\tvar b strings.Builder\n\tfor _, k := range kinds {\n\t\tb.WriteString(\"### \" + jsonmd.Text(k) + \"\\n\\n\")\n\t\tif len(pending[k]) == 0 {\n\t\t\tb.WriteString(\"pending: none\\n\")\n\t\t} else {\n\t\t\tb.WriteString(\"pending:\\n\")\n\t\t\tfor _, d := range pending[k] {\n\t\t\t\tb.WriteString(\"- \" + line(d) + \"\\n\")\n\t\t\t}\n\t\t}\n\t\tif len(done[k]) \u003e 0 {\n\t\t\tb.WriteString(\"\\nexecuted:\\n\")\n\t\t\tfor _, d := range done[k] {\n\t\t\t\tb.WriteString(\"- \" + line(d) + \"\\n\")\n\t\t\t}\n\t\t}\n\t\tb.WriteString(\"\\n\")\n\t}\n\treturn b.String()\n}\n\n// line is a decision's line on the page. Its text and note carry the\n// parameters a builder took (client ids such as a router's \"_eth_\" or\n// \"\u003cbridge\u003e\", denoms, refusal reasons), so both are escaped as inline\n// Markdown text: the page shows them as written.\nfunc line(d *Decision) string {\n\ts := \"#\" + itoa(d.ID) + \" \" + d.State() + \": \" + jsonmd.Text(d.Text) + \" (recorded \" + itoa(d.Recorded)\n\tif d.Queued() {\n\t\ts += \", due \" + itoa(d.Due)\n\t}\n\tswitch {\n\tcase d.Applied != 0:\n\t\ts += \", applied \" + itoa(d.Applied)\n\tcase d.Cancelled != 0:\n\t\ts += \", cancelled \" + itoa(d.Cancelled)\n\t}\n\ts += \")\"\n\tif d.Note != \"\" {\n\t\ts += \" — \" + jsonmd.Text(d.Note)\n\t}\n\treturn s\n}\n\n// JSON is every decision, oldest first, as a JSON array.\nfunc (l *Ledger) JSON() string {\n\tvar b strings.Builder\n\tb.WriteString(\"[\")\n\tfirst := true\n\tl.all.Iterate(\"\", \"\", func(_ string, v any) bool {\n\t\td := v.(*Decision)\n\t\tif !first {\n\t\t\tb.WriteString(\",\")\n\t\t}\n\t\tfirst = false\n\t\tb.WriteString(\"{\\\"id\\\":\" + itoa(d.ID) + \",\\\"kind\\\":\" + strconv.Quote(d.Kind) + \",\\\"text\\\":\" + strconv.Quote(d.Text) +\n\t\t\t\",\\\"state\\\":\" + strconv.Quote(d.State()) + \",\\\"made\\\":\" + itoa(d.Made) + \",\\\"recorded\\\":\" + itoa(d.Recorded) +\n\t\t\t\",\\\"due\\\":\" + itoa(d.Due) + \",\\\"applied\\\":\" + itoa(d.Applied) + \",\\\"cancelled\\\":\" + itoa(d.Cancelled) +\n\t\t\t\",\\\"note\\\":\" + strconv.Quote(d.Note) + \"}\")\n\t\treturn false\n\t})\n\tb.WriteString(\"]\")\n\treturn b.String()\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/aib/decisions/v0\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"FhpB0XpA/+TvSZqjrSf9gn6uZK7vcEHP/+JxA/M2eAwx0zZN6YavdoQLypKgzPrHZihjo2fnASG/QzAzsI759Q=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"abi","path":"gno.land/p/aib/encoding/abi/v0","files":[{"name":"abi.gno","body":"// Package abi is a small Solidity ABI encoder: abi.encode of values built from\n// the constructors below, as Solidity's abi.encode and abi.decode see them\n// (the \"Formal Specification of the Encoding\" of the Solidity ABI spec).\n//\n// Supported types: every static 32-byte type (uintN, intN for non-negative\n// values, address, bool, bytes32 and bytesN), the dynamic string and bytes,\n// tuples (static or dynamic) and dynamic arrays T[]. Fixed-size arrays T[k] are\n// not supported: they encode as a tuple of k elements of T, so Tuple serves.\n//\n//\tparams := abi.Encode(abi.Uint64(1), abi.String(\"client-0\"))\n//\t// == abi.encode(uint8(1), \"client-0\") in Solidity\n//\n// Encode(vs...) is abi.encode(v1, ..., vn): the encoding of the tuple of the\n// arguments, without an outer offset word. A Solidity contract reads it back\n// with abi.decode(data, (T1, ..., Tn)).\npackage abi\n\nimport (\n\t\"encoding/binary\"\n\t\"errors\"\n)\n\n// Word is the ABI word size in bytes.\nconst Word = 32\n\ntype kind uint8\n\nconst (\n\tkindWord  kind = iota + 1 // a static 32-byte value\n\tkindBytes                 // string or bytes: dynamic\n\tkindTuple                 // dynamic iff a component is\n\tkindArray                 // T[]: always dynamic\n)\n\n// Value is one ABI value. Build it with the constructors; the zero Value is\n// invalid and Encode panics on it.\ntype Value struct {\n\tkind  kind\n\tword  [Word]byte\n\tdata  []byte\n\telems []Value\n}\n\n// Uint64 is a uintN (N ≥ 8) or a non-negative intN holding v.\nfunc Uint64(v uint64) Value {\n\tvar w [Word]byte\n\tbinary.BigEndian.PutUint64(w[Word-8:], v)\n\treturn Value{kind: kindWord, word: w}\n}\n\n// Bool is a bool.\nfunc Bool(b bool) Value {\n\tif b {\n\t\treturn Uint64(1)\n\t}\n\treturn Uint64(0)\n}\n\n// Address is an address: 20 bytes, left-padded with zeros.\nfunc Address(a [20]byte) Value {\n\tvar w [Word]byte\n\tcopy(w[Word-20:], a[:])\n\treturn Value{kind: kindWord, word: w}\n}\n\n// Bytes32 is a bytes32.\nfunc Bytes32(b [Word]byte) Value {\n\treturn Value{kind: kindWord, word: b}\n}\n\n// Uint256 is a uint256 given as its 32 big-endian bytes.\nfunc Uint256(w [Word]byte) Value {\n\treturn Value{kind: kindWord, word: w}\n}\n\n// Uint256Of is a uint256 holding v.\nfunc Uint256Of(v uint256) Value {\n\tvar w [Word]byte\n\tfor i := Word - 1; i \u003e= 0; i-- {\n\t\tw[i] = byte(v)\n\t\tv \u003e\u003e= 8\n\t}\n\treturn Uint256(w)\n}\n\n// WordUint256 reads a big-endian 32-byte word as a uint256.\nfunc WordUint256(w [Word]byte) uint256 {\n\tvar v uint256\n\tfor _, c := range w {\n\t\tv = v\u003c\u003c8 | uint256(c)\n\t}\n\treturn v\n}\n\n// Uint256Decimal is a uint256 given in decimal digits: no sign, no leading\n// \"+\", no spaces, at least one digit; leading zeros are accepted. It errs when\n// the number does not fit 256 bits.\nfunc Uint256Decimal(s string) (Value, error) {\n\tw, err := DecimalWord(s)\n\tif err != nil {\n\t\treturn Value{}, err\n\t}\n\treturn Uint256(w), nil\n}\n\n// DecimalWord parses decimal digits into a big-endian 32-byte word.\nfunc DecimalWord(s string) ([Word]byte, error) {\n\tvar w [Word]byte\n\tif s == \"\" {\n\t\treturn w, errors.New(\"abi: empty number\")\n\t}\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 w, errors.New(\"abi: not a decimal number\")\n\t\t}\n\t\tcarry := uint(c - '0')\n\t\tfor j := Word - 1; j \u003e= 0; j-- {\n\t\t\tv := uint(w[j])*10 + carry\n\t\t\tw[j] = byte(v)\n\t\t\tcarry = v \u003e\u003e 8\n\t\t}\n\t\tif carry != 0 {\n\t\t\treturn w, errors.New(\"abi: number overflows uint256\")\n\t\t}\n\t}\n\treturn w, nil\n}\n\n// WordDecimal formats a big-endian 32-byte word in decimal digits.\nfunc WordDecimal(w [Word]byte) string {\n\tn := w\n\tvar digits []byte\n\tfor {\n\t\tzero := true\n\t\tvar rem uint\n\t\tfor i := 0; i \u003c Word; i++ {\n\t\t\tv := rem\u003c\u003c8 | uint(n[i])\n\t\t\tn[i] = byte(v / 10)\n\t\t\trem = v % 10\n\t\t\tif n[i] != 0 {\n\t\t\t\tzero = false\n\t\t\t}\n\t\t}\n\t\tdigits = append(digits, byte('0'+rem))\n\t\tif zero {\n\t\t\tbreak\n\t\t}\n\t}\n\tfor i, j := 0, len(digits)-1; i \u003c j; i, j = i+1, j-1 {\n\t\tdigits[i], digits[j] = digits[j], digits[i]\n\t}\n\treturn string(digits)\n}\n\n// String is a string (UTF-8 bytes, as Solidity keeps them).\nfunc String(s string) Value {\n\treturn Value{kind: kindBytes, data: []byte(s)}\n}\n\n// Bytes is a bytes. The slice is copied.\nfunc Bytes(b []byte) Value {\n\treturn Value{kind: kindBytes, data: append([]byte{}, b...)}\n}\n\n// Tuple is a tuple (a Solidity struct) of the components, in order.\nfunc Tuple(components ...Value) Value {\n\treturn Value{kind: kindTuple, elems: append([]Value{}, components...)}\n}\n\n// Array is a dynamic array T[] of the elements, which must all have the same\n// ABI type; an empty array encodes as its length alone, whatever T is.\nfunc Array(elems ...Value) Value {\n\treturn Value{kind: kindArray, elems: append([]Value{}, elems...)}\n}\n\n// Dynamic reports whether the value's type is dynamic: string, bytes, any\n// T[], and a tuple with a dynamic component.\nfunc (v Value) Dynamic() bool {\n\tswitch v.kind {\n\tcase kindWord:\n\t\treturn false\n\tcase kindBytes, kindArray:\n\t\treturn true\n\tcase kindTuple:\n\t\tfor _, e := range v.elems {\n\t\t\tif e.Dynamic() {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}\n\tpanic(\"abi: invalid value\")\n}\n\n// Encode is abi.encode(vs...).\nfunc Encode(vs ...Value) []byte {\n\treturn encodeSeq(vs)\n}\n\n// uintWord is v as a big-endian ABI word.\nfunc uintWord(v uint64) []byte {\n\tw := make([]byte, Word)\n\tbinary.BigEndian.PutUint64(w[Word-8:], v)\n\treturn w\n}\n\n// encode is enc(v) of the spec.\nfunc encode(v Value) []byte {\n\tswitch v.kind {\n\tcase kindWord:\n\t\tw := v.word\n\t\treturn w[:]\n\tcase kindBytes:\n\t\tpadded := (len(v.data) + Word - 1) / Word * Word\n\t\tout := uintWord(uint64(len(v.data)))\n\t\tbody := make([]byte, padded)\n\t\tcopy(body, v.data)\n\t\treturn append(out, body...)\n\tcase kindTuple:\n\t\treturn encodeSeq(v.elems)\n\tcase kindArray:\n\t\treturn append(uintWord(uint64(len(v.elems))), encodeSeq(v.elems)...)\n\t}\n\tpanic(\"abi: invalid value\")\n}\n\n// encodeSeq encodes a tuple's components: the heads (a static component\n// in place, the offset of a dynamic one, counted from the start of the\n// heads), then the tails of the dynamic components in order.\nfunc encodeSeq(vs []Value) []byte {\n\tencs := make([][]byte, len(vs))\n\theadLen := 0\n\tfor i, v := range vs {\n\t\tencs[i] = encode(v)\n\t\tif v.Dynamic() {\n\t\t\theadLen += Word\n\t\t} else {\n\t\t\theadLen += len(encs[i])\n\t\t}\n\t}\n\tout := make([]byte, 0, headLen)\n\ttailOff := headLen\n\tfor i, v := range vs {\n\t\tif v.Dynamic() {\n\t\t\tout = append(out, uintWord(uint64(tailOff))...)\n\t\t\ttailOff += len(encs[i])\n\t\t} else {\n\t\t\tout = append(out, encs[i]...)\n\t\t}\n\t}\n\tfor i, v := range vs {\n\t\tif v.Dynamic() {\n\t\t\tout = append(out, encs[i]...)\n\t\t}\n\t}\n\treturn out\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/aib/encoding/abi/v0\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"23tNcdH45+xpgEBbFlOc8jWr7dFGmFO3OPy3KrYONcYkYOMAWko3fKwIMHj+NfjY8ukXekFUSAmp6fvv/c+MDA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"ethaddr","path":"gno.land/p/aib/encoding/ethaddr/v0","files":[{"name":"ethaddr.gno","body":"// Package ethaddr reads and writes Ethereum addresses as people and tools\n// write them: \"0x\" and 40 hex digits, all lower case, all upper case, or mixed\n// case that must be the address's EIP-55 checksum.\n//\n// Ported from gnolang/bridges (branch ethereum-draft, commit a58efe81c9eb),\n// ethereum/realm/hlmailbox/ethaddr.gno (ParseEthAddress, EthAddress, eip55),\n// Copyright (C) 2026 NewTendermint, LLC, under the GNO Network General Public\n// License. Changes: an address is 20 bytes here (IBC carries the receiver as a\n// hex string, not Hyperlane's left-padded 32 bytes), so the 64-digit form is\n// not accepted; Reserved is new.\npackage ethaddr\n\nimport (\n\t\"crypto/keccak256\"\n\t\"encoding/hex\"\n\t\"errors\"\n)\n\n// Address is an Ethereum address.\ntype Address [20]byte\n\n// Parse reads \"0x\" and 40 hex digits: all lower case, all upper case, or mixed\n// case equal to the EIP-55 checksum (a mixed-case address with one letter's\n// case wrong is a typo the checksum caught, refused). Nothing else is\n// accepted: no surrounding spaces, no missing \"0x\", no other length. The zero\n// address parses; callers refuse it where they must.\nfunc Parse(s string) (Address, error) {\n\tvar out Address\n\tif len(s) != 42 || s[0] != '0' || s[1] != 'x' {\n\t\treturn out, errors.New(\"not an Ethereum address: want 0x and 40 hex digits\")\n\t}\n\th := s[2:]\n\tb, err := hex.DecodeString(h)\n\tif err != nil || len(b) != 20 {\n\t\treturn out, errors.New(\"not an Ethereum address: want 0x and 40 hex digits\")\n\t}\n\tcopy(out[:], b)\n\tif hasLetter(h, 'a', 'f') \u0026\u0026 hasLetter(h, 'A', 'F') \u0026\u0026 checksum(b) != h {\n\t\treturn out, errors.New(\"not an Ethereum address: mixed case that is not its EIP-55 checksum\")\n\t}\n\treturn out, nil\n}\n\n// String is the address in its EIP-55 checksum case, with \"0x\".\nfunc (a Address) String() string {\n\treturn \"0x\" + checksum(a[:])\n}\n\n// Lower is the address in lower case, with \"0x\": the form denoms and\n// comparisons use.\nfunc (a Address) Lower() string {\n\treturn \"0x\" + hex.EncodeToString(a[:])\n}\n\n// IsZero reports whether a is the zero address.\nfunc (a Address) IsZero() bool {\n\treturn a == Address{}\n}\n\n// Reserved reports whether a is at or below 0xffff: a precompile's address,\n// or one the protocol reserves. Nothing sent there can be moved again.\nfunc (a Address) Reserved() bool {\n\tfor _, c := range a[:18] {\n\t\tif c != 0 {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// checksum is a 20-byte address's 40 hex digits in EIP-55 case: a letter is\n// upper case where the matching nibble of keccak256 of the lower-case hex (as\n// ASCII) is 8 or more.\nfunc checksum(addr []byte) string {\n\tlower := hex.EncodeToString(addr)\n\th := keccak256.Sum256([]byte(lower))\n\tout := []byte(lower)\n\tfor i := 0; i \u003c len(out); i++ {\n\t\tc := out[i]\n\t\tif c \u003c 'a' || c \u003e 'f' {\n\t\t\tcontinue\n\t\t}\n\t\tnib := h[i/2] \u003e\u003e 4\n\t\tif i%2 == 1 {\n\t\t\tnib = h[i/2] \u0026 0x0f\n\t\t}\n\t\tif nib \u003e= 8 {\n\t\t\tout[i] = c - 'a' + 'A'\n\t\t}\n\t}\n\treturn string(out)\n}\n\nfunc hasLetter(s string, lo, hi byte) bool {\n\tfor i := 0; i \u003c len(s); i++ {\n\t\tif s[i] \u003e= lo \u0026\u0026 s[i] \u003c= hi {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/aib/encoding/ethaddr/v0\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"jbbI7VKSVLY3zjCbxiP2FcZp4lzWEYPfw35h/R2sL6wYLIF22Gk+HnCeDp8j8vEAr5A/Y3Kr8fAO1+wgTqSHUg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"proto","path":"gno.land/p/aib/encoding/proto/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/aib/encoding/proto/v0\"\ngno = \"0.9\"\n"},{"name":"proto.gno","body":"package proto\n\nimport (\n\t\"encoding/binary\"\n\t\"time\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\ntype WireType int\n\nconst (\n\tVARINT  WireType = 0 //\tint32, int64, uint32, uint64, sint32, sint64, bool, enum\n\tFIXED64 WireType = 1 // fixed64, sfixed64, double\n\tLEN     WireType = 2 // string, bytes, embedded messages, packed repeated fields\n\tFIXED32 WireType = 5 //\tfixed32, sfixed32, float\n)\n\n// AppendVarint appends a varint field.\nfunc AppendVarint(buf []byte, fieldNum int, v uint64) []byte {\n\tif v == 0 {\n\t\treturn buf\n\t}\n\tbuf = AppendTag(buf, fieldNum, VARINT)\n\treturn binary.AppendUvarint(buf, v)\n}\n\n// MarshalAny returns the wire encoding of a google.protobuf.Any message\n// holding (typeURL, value). Used to match the cdc.MarshalInterface format\n// produced by the Cosmos SDK proto codec when storing or verifying\n// interface-typed messages (e.g. ClientState in the upgrade store).\nfunc MarshalAny(typeURL string, value []byte) []byte {\n\tvar bz []byte\n\tbz = AppendLengthDelimited(bz, 1, []byte(typeURL))\n\tbz = AppendLengthDelimited(bz, 2, value)\n\treturn bz\n}\n\n// AppendPackedVarintInt32 appends a packed repeated int32 varint field.\n// Negative values are sign-extended to 64 bits to match proto3 wire format.\n// Returns buf unchanged when values is empty (proto3 default).\nfunc AppendPackedVarintInt32(buf []byte, fieldNum int, values []int32) []byte {\n\tif len(values) == 0 {\n\t\treturn buf\n\t}\n\tvar packed []byte\n\tfor _, v := range values {\n\t\tpacked = binary.AppendUvarint(packed, uint64(int64(v)))\n\t}\n\tbuf = AppendTag(buf, fieldNum, LEN)\n\tbuf = binary.AppendUvarint(buf, uint64(len(packed)))\n\treturn append(buf, packed...)\n}\n\n// AppendFixed64 appends a fixed 64-bit field.\nfunc AppendFixed64(buf []byte, fieldNum int, v uint64) []byte {\n\tif v == 0 {\n\t\treturn buf\n\t}\n\tbuf = AppendTag(buf, fieldNum, FIXED64)\n\tvar b [8]byte\n\tbinary.LittleEndian.PutUint64(b[:], v)\n\treturn append(buf, b[:]...)\n}\n\n// AppendLengthDelimited appends a length-delimited field. Returns buf\n// unchanged when bz is empty (proto3 default for optional fields).\nfunc AppendLengthDelimited(buf []byte, fieldNum int, bz []byte) []byte {\n\tif len(bz) == 0 {\n\t\treturn buf\n\t}\n\tbuf = AppendTag(buf, fieldNum, LEN)\n\tbuf = binary.AppendUvarint(buf, uint64(len(bz)))\n\treturn append(buf, bz...)\n}\n\n// AppendAlwaysLengthDelimited appends a length-delimited field, including\n// when bz is empty (emits a length-0 value). Required to match the wire\n// format produced by gogoproto for fields marked\n// `[(gogoproto.nullable) = false]`, which always serialize even when their\n// underlying message has no non-zero fields.\nfunc AppendAlwaysLengthDelimited(buf []byte, fieldNum int, bz []byte) []byte {\n\tbuf = AppendTag(buf, fieldNum, LEN)\n\tbuf = binary.AppendUvarint(buf, uint64(len(bz)))\n\treturn append(buf, bz...)\n}\n\n// TimeMarshal returns a proto marshalled time.\nfunc TimeMarshal(t time.Time) []byte {\n\tvar (\n\t\tbuf     []byte\n\t\tseconds = t.Unix()\n\t\tnanos   = int32(t.Nanosecond())\n\t)\n\t// Field 1: seconds (int64 - varint)\n\tbuf = AppendVarint(buf, 1, uint64(seconds))\n\t// Field 2: nanos (int32 - varint)\n\tbuf = AppendVarint(buf, 2, uint64(nanos))\n\treturn buf\n}\n\n// AppendTime appends a google.protobuf.Timestamp field.\nfunc AppendTime(buf []byte, fieldNum int, t time.Time) []byte {\n\treturn AppendLengthDelimited(buf, fieldNum, TimeMarshal(t))\n}\n\n// DurationMarshal returns the inner proto encoding of a\n// google.protobuf.Duration (without the length-delimited field wrapper).\nfunc DurationMarshal(d time.Duration) []byte {\n\tvar buf []byte\n\tseconds := d / time.Second\n\tnanos := int32(d % time.Second)\n\tif nanos \u003c 0 {\n\t\tnanos += int32(time.Second)\n\t}\n\tbuf = AppendVarint(buf, 1, uint64(seconds))\n\tbuf = AppendVarint(buf, 2, uint64(nanos))\n\treturn buf\n}\n\n// AppendDuration appends a google.protobuf.Duration field.\nfunc AppendDuration(buf []byte, fieldNum int, d time.Duration) []byte {\n\treturn AppendLengthDelimited(buf, fieldNum, DurationMarshal(d))\n}\n\n// AppendTag appends a protobuf tag (field number and wire type)\nfunc AppendTag(buf []byte, fieldNum int, wireType WireType) []byte {\n\ttag := (fieldNum \u003c\u003c 3) | int(wireType)\n\treturn binary.AppendUvarint(buf, uint64(tag))\n}\n\n// DecodeVarint reads a varint from the byte slice and returns the value and new position\nfunc DecodeVarint(buf []byte, pos int) (uint64, int, error) {\n\tif pos \u003e len(buf) {\n\t\treturn 0, pos, ufmt.Errorf(\"buffer underflow while reading varint\")\n\t}\n\tv, n := binary.Uvarint(buf[pos:])\n\tif n == 0 {\n\t\treturn 0, pos, ufmt.Errorf(\"buffer underflow while reading varint\")\n\t}\n\tif n \u003c 0 {\n\t\treturn 0, pos + (-n), ufmt.Errorf(\"varint overflows uint64\")\n\t}\n\treturn v, pos + n, nil\n}\n\n// DecodeString reads a string (length-prefixed) from the byte slice\nfunc DecodeString(buf []byte, pos int) (string, int, error) {\n\tlength, newPos, err := DecodeVarint(buf, pos)\n\tif err != nil {\n\t\treturn \"\", newPos, err\n\t}\n\tpos = newPos\n\n\tif length \u003e uint64(len(buf)-pos) {\n\t\treturn \"\", pos, ufmt.Errorf(\"buffer underflow while reading string\")\n\t}\n\n\tend := pos + int(length)\n\treturn string(buf[pos:end]), end, nil\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"sYBCXJlnZ3Dtg8hrHWwojosZBmclcm9wyAD8NjkkdVp+HSsWvV925uTqcQ768eBMyyMUeAceii60YcKl0Grnbw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"encoding","path":"gno.land/p/aib/encoding/v0","files":[{"name":"encoding.gno","body":"package encoding\n\nimport \"encoding/binary\"\n\n// Uint64ToBigEndian - marshals uint64 to a bigendian byte slice so it can be\n// sorted\nfunc Uint64ToBigEndian(i uint64) []byte {\n\tb := make([]byte, 8)\n\tbinary.BigEndian.PutUint64(b, i)\n\treturn b\n}\n\n// BigEndianToUint64 returns an uint64 from big endian encoded bytes. If\n// encoding is empty, zero is returned.\nfunc BigEndianToUint64(bz []byte) uint64 {\n\tif len(bz) == 0 {\n\t\treturn 0\n\t}\n\n\treturn binary.BigEndian.Uint64(bz)\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/aib/encoding/v0\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"HpWgI9HiHuaKQ6/B8mphZ6T8Jwil+k0qDImIuCp4JyYipbNRaf6w/+ZLryfbmqCy2UglyGdIj+vyv2lUfVYFDA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"wire","path":"gno.land/p/aib/encoding/wire/v0","files":[{"name":"VENDORED.md","body":"Originally from github.com/clockworkgr/gno-upgradeable at 8c18598\n(gno.land/p/clockwork/wire/v0: wire.gno, its tests and gnomod.toml); maintained\nhere as gno.land/p/aib/encoding/wire/v0 since.\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/aib/encoding/wire/v0\"\ngno = \"0.9\"\n"},{"name":"wire.gno","body":"// Package wire is a schema-positional binary codec for gno.\n//\n// It exists because gno charges gas per interpreted opcode, and the usual way\n// to pack call arguments — a delimited text format like CSV — pays that cost\n// per byte: it scans every byte looking for separators and quotes, and parses\n// every number with strconv. A format that instead length-prefixes its fields\n// lets the reader slice each one out in a fixed number of opcodes, so decoding\n// costs on the order of the field count, not the payload length. On measured\n// gno, decoding a 60-character string this way is ~30x cheaper than CSV and,\n// unlike CSV, does not grow with the string's length.\n//\n// # The wire has no schema; the call order is the schema\n//\n// A payload is just values laid end to end. Nothing on the wire says what type\n// each one is — the [Reader] must read the same types in the same order the\n// [Writer] wrote them. That shared, out-of-band schema is the whole trick: no\n// field tags, no type bytes, no delimiters, no quoting. To pin that order from\n// a declaration and generate the reader and writer — in Gno or another language\n// — see gno.land/p/clockwork/wire/schema/v0 in github.com/clockworkgr/gno-upgradeable.\n//\n//\t// encode (off chain, or in a calling realm)\n//\tpayload := wire.NewWriter().Addr(to).Int(amount).Str(memo).Bytes()\n//\n//\t// decode (in the handler)\n//\tr := wire.NewReader(payload)\n//\tto, amount, memo := r.Addr(), r.Int(), r.Str()\n//\tif err := r.Err(); err != nil {\n//\t\tpanic(err)\n//\t}\n//\n// # Pass it as []byte\n//\n// Hand the payload to a realm as a []byte argument, not a string. gnokey sends\n// a []byte argument base64-encoded and the VM decodes it in native Go before\n// your code runs, so the base64 envelope costs no interpreted gas. A string\n// argument would arrive raw but forces you back into per-byte parsing.\n//\n// # Safety\n//\n// A [Reader] never panics on a malformed or truncated payload, including a\n// hostile one that claims a huge length. Every read is bounds-checked; the\n// first failure sets a sticky error, every read after it returns a zero value,\n// and [Reader.Err] reports it. Check Err once, after reading the fields you\n// expect. This makes the codec safe to point at untrusted input.\npackage wire\n\nimport \"errors\"\n\nvar (\n\t// ErrShort means the payload ended before a value could be read in full,\n\t// or a length prefix claimed more bytes than remain.\n\tErrShort = errors.New(\"wire: payload too short\")\n\n\t// ErrOverflow means a varint was longer than a 64-bit value permits.\n\tErrOverflow = errors.New(\"wire: varint overflows 64 bits\")\n)\n\n// Writer accumulates a binary payload. Its methods append one value each and\n// return the Writer, so writes chain. A Writer never fails.\ntype Writer struct {\n\tbuf []byte\n}\n\n// NewWriter returns an empty Writer.\nfunc NewWriter() *Writer { return \u0026Writer{} }\n\n// Bool writes a single 0x00/0x01 byte.\nfunc (w *Writer) Bool(v bool) *Writer {\n\tif v {\n\t\tw.buf = append(w.buf, 1)\n\t} else {\n\t\tw.buf = append(w.buf, 0)\n\t}\n\treturn w\n}\n\n// Byte writes one raw byte.\nfunc (w *Writer) Byte(v byte) *Writer {\n\tw.buf = append(w.buf, v)\n\treturn w\n}\n\n// Uint writes a uint64 as 8 big-endian bytes. Fixed width is the cheapest to\n// decode; use Uvarint when values are small and payload size matters more.\nfunc (w *Writer) Uint(v uint64) *Writer {\n\tw.buf = append(w.buf,\n\t\tbyte(v\u003e\u003e56), byte(v\u003e\u003e48), byte(v\u003e\u003e40), byte(v\u003e\u003e32),\n\t\tbyte(v\u003e\u003e24), byte(v\u003e\u003e16), byte(v\u003e\u003e8), byte(v))\n\treturn w\n}\n\n// Uint256 writes a uint256 as 32 big-endian bytes.\nfunc (w *Writer) Uint256(v uint256) *Writer {\n\tvar b [32]byte\n\tfor i := 31; i \u003e= 0; i-- {\n\t\tb[i] = byte(v)\n\t\tv \u003e\u003e= 8\n\t}\n\tw.buf = append(w.buf, b[:]...)\n\treturn w\n}\n\n// Int writes an int64 as 8 big-endian bytes (two's complement).\nfunc (w *Writer) Int(v int64) *Writer { return w.Uint(uint64(v)) }\n\n// Uvarint writes a uint64 as an unsigned LEB128 varint: 1 byte for values\n// below 128, up to 10 for the largest. Compact, slightly dearer to decode.\nfunc (w *Writer) Uvarint(v uint64) *Writer {\n\tfor v \u003e= 0x80 {\n\t\tw.buf = append(w.buf, byte(v)|0x80)\n\t\tv \u003e\u003e= 7\n\t}\n\tw.buf = append(w.buf, byte(v))\n\treturn w\n}\n\n// Varint writes an int64 as a zigzag-encoded Uvarint, so small-magnitude\n// negatives stay compact.\nfunc (w *Writer) Varint(v int64) *Writer {\n\treturn w.Uvarint(uint64((v \u003c\u003c 1) ^ (v \u003e\u003e 63)))\n}\n\n// Str writes a string as an Uvarint length followed by its bytes. Any bytes\n// are allowed; nothing is escaped.\nfunc (w *Writer) Str(s string) *Writer {\n\tw.Uvarint(uint64(len(s)))\n\tw.buf = append(w.buf, s...)\n\treturn w\n}\n\n// Blob writes a byte slice as an Uvarint length followed by its bytes.\nfunc (w *Writer) Blob(b []byte) *Writer {\n\tw.Uvarint(uint64(len(b)))\n\tw.buf = append(w.buf, b...)\n\treturn w\n}\n\n// Addr writes an address as its bech32 string (see Str). This decodes cheaply\n// and hands the reader a value it can use directly; the codec does not\n// validate the address, which is the caller's business.\nfunc (w *Writer) Addr(a address) *Writer { return w.Str(string(a)) }\n\n// Bytes returns the accumulated payload. The slice aliases the Writer's\n// internal buffer: it stays valid as long as you only read it, but Reset (and\n// the writes that follow) will overwrite it, so copy it first if you intend to\n// keep it across a Reset.\nfunc (w *Writer) Bytes() []byte { return w.buf }\n\n// Len is the number of bytes written so far.\nfunc (w *Writer) Len() int { return len(w.buf) }\n\n// Reset empties the Writer for reuse, keeping its capacity.\nfunc (w *Writer) Reset() *Writer {\n\tw.buf = w.buf[:0]\n\treturn w\n}\n\n// Reader reads values from a payload in the order they were written. It never\n// panics: the first read that runs past the end (or overflows) sets a sticky\n// error, after which every read returns a zero value. Read the fields you\n// expect, then check Err once.\ntype Reader struct {\n\tbuf []byte\n\toff int\n\terr error\n}\n\n// NewReader returns a Reader over b. It does not copy b.\nfunc NewReader(b []byte) *Reader { return \u0026Reader{buf: b} }\n\n// Err returns the first error encountered, or nil. Once set it stays set.\nfunc (r *Reader) Err() error { return r.err }\n\n// More reports whether unread bytes remain and no error has occurred. Use it\n// to read trailing optional fields.\nfunc (r *Reader) More() bool { return r.err == nil \u0026\u0026 r.off \u003c len(r.buf) }\n\n// Done reports whether the whole payload was consumed with no error. Use it to\n// reject trailing bytes a stricter caller would treat as malformed.\nfunc (r *Reader) Done() bool { return r.err == nil \u0026\u0026 r.off == len(r.buf) }\n\n// Remaining is the number of unread bytes.\nfunc (r *Reader) Remaining() int {\n\tif r.off \u003e len(r.buf) {\n\t\treturn 0\n\t}\n\treturn len(r.buf) - r.off\n}\n\n// fail records err (keeping the first) and reports false.\nfunc (r *Reader) fail(err error) bool {\n\tif r.err == nil {\n\t\tr.err = err\n\t}\n\treturn false\n}\n\n// take advances over n bytes, returning them, or fails.\nfunc (r *Reader) take(n int) ([]byte, bool) {\n\tif r.err != nil {\n\t\treturn nil, false\n\t}\n\t// Compare against the bytes that remain rather than r.off+n: with n coming\n\t// from a caller-supplied length (via int(Uvarint)), r.off+n can overflow\n\t// int64 and wrap negative, slipping past the check and panicking the slice.\n\t// len(r.buf)-r.off is always in [0, len]; n\u003c0 catches a length so large its\n\t// int conversion went negative.\n\tif n \u003c 0 || n \u003e len(r.buf)-r.off {\n\t\treturn nil, r.fail(ErrShort)\n\t}\n\tb := r.buf[r.off : r.off+n]\n\tr.off += n\n\treturn b, true\n}\n\n// Byte reads one raw byte, or 0 on failure.\nfunc (r *Reader) Byte() byte {\n\tb, ok := r.take(1)\n\tif !ok {\n\t\treturn 0\n\t}\n\treturn b[0]\n}\n\n// Bool reads a boolean. Any non-zero byte is true.\nfunc (r *Reader) Bool() bool { return r.Byte() != 0 }\n\n// Uint reads a fixed 8-byte big-endian uint64, or 0 on failure.\nfunc (r *Reader) Uint() uint64 {\n\tb, ok := r.take(8)\n\tif !ok {\n\t\treturn 0\n\t}\n\treturn uint64(b[0])\u003c\u003c56 | uint64(b[1])\u003c\u003c48 | uint64(b[2])\u003c\u003c40 | uint64(b[3])\u003c\u003c32 |\n\t\tuint64(b[4])\u003c\u003c24 | uint64(b[5])\u003c\u003c16 | uint64(b[6])\u003c\u003c8 | uint64(b[7])\n}\n\n// Uint256 reads a fixed 32-byte big-endian uint256, or 0 on failure.\nfunc (r *Reader) Uint256() uint256 {\n\tb, ok := r.take(32)\n\tif !ok {\n\t\treturn 0\n\t}\n\tvar v uint256\n\tfor _, c := range b {\n\t\tv = v\u003c\u003c8 | uint256(c)\n\t}\n\treturn v\n}\n\n// Int reads a fixed 8-byte big-endian int64.\nfunc (r *Reader) Int() int64 { return int64(r.Uint()) }\n\n// Uvarint reads an unsigned LEB128 varint, or 0 on failure. A non-minimal\n// encoding (trailing 0x80…0x00 groups) decodes to the same value rather than\n// being rejected, so the format is not canonical: do not treat a payload's\n// exact bytes as an identity. Only a value that cannot fit in 64 bits is\n// refused (ErrOverflow).\nfunc (r *Reader) Uvarint() uint64 {\n\tif r.err != nil {\n\t\treturn 0\n\t}\n\tvar x uint64\n\tvar s uint\n\tfor i := 0; i \u003c 10; i++ {\n\t\tif r.off \u003e= len(r.buf) {\n\t\t\tr.fail(ErrShort)\n\t\t\treturn 0\n\t\t}\n\t\tb := r.buf[r.off]\n\t\tr.off++\n\t\tif b \u003c 0x80 {\n\t\t\tif i == 9 \u0026\u0026 b \u003e 1 {\n\t\t\t\tr.fail(ErrOverflow)\n\t\t\t\treturn 0\n\t\t\t}\n\t\t\treturn x | uint64(b)\u003c\u003cs\n\t\t}\n\t\tx |= uint64(b\u00260x7f) \u003c\u003c s\n\t\ts += 7\n\t}\n\tr.fail(ErrOverflow)\n\treturn 0\n}\n\n// Varint reads a zigzag-encoded Varint.\nfunc (r *Reader) Varint() int64 {\n\tu := r.Uvarint()\n\treturn int64(u\u003e\u003e1) ^ -int64(u\u00261)\n}\n\n// Str reads a length-prefixed string, or \"\" on failure.\nfunc (r *Reader) Str() string {\n\tn := r.Uvarint()\n\tb, ok := r.take(int(n))\n\tif !ok {\n\t\treturn \"\"\n\t}\n\treturn string(b)\n}\n\n// Blob reads a length-prefixed byte slice. The result aliases the payload;\n// copy it if you intend to keep it past the payload's lifetime.\nfunc (r *Reader) Blob() []byte {\n\tn := r.Uvarint()\n\tb, ok := r.take(int(n))\n\tif !ok {\n\t\treturn nil\n\t}\n\treturn b\n}\n\n// Addr reads a length-prefixed bech32 string as an address (see Str). The\n// codec does not check validity; use address.IsValid if you must.\nfunc (r *Reader) Addr() address { return address(r.Str()) }\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"/j4O+xVkgiDeAKriMlXuvloJWTAk101Ke2mItscPc/E+PRIpwFtuODMOzZ+3vqhAMSFQeJ+8SIsKu3aQXyDjEg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"host","path":"gno.land/p/aib/ibc/host/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/aib/ibc/host/v0\"\ngno = \"0.9\"\n"},{"name":"packet_key.gno","body":"package host\n\nimport \"gno.land/p/aib/encoding/v0\"\n\nconst (\n\tPacketCommitmentBasePrefix      = byte(1)\n\tPacketReceiptBasePrefix         = byte(2)\n\tPacketAcknowledgementBasePrefix = byte(3)\n)\n\n// PacketCommitmentPrefixKey returns the store key prefix under which packet\n// commitments for a particular client are stored.\n// clientID must be a generated identifier, not provided externally so key\n// collisions are not possible.\nfunc PacketCommitmentPrefixKey(clientID string) []byte {\n\treturn append([]byte(clientID), PacketCommitmentBasePrefix)\n}\n\n// PacketCommitmentKey returns the store key of under which a packet commitment\n// is stored.\n// clientID must be a generated identifier, not provided externally so key\n// collisions are not possible.\nfunc PacketCommitmentKey(clientID string, sequence uint64) []byte {\n\treturn append(PacketCommitmentPrefixKey(clientID), encoding.Uint64ToBigEndian(sequence)...)\n}\n\n// PacketReceiptPrefixKey returns the store key prefix under which packet\n// receipts for a particular channel are stored.\n// clientID must be a generated identifier, not provided externally so key\n// collisions are not possible.\nfunc PacketReceiptPrefixKey(clientID string) []byte {\n\treturn append([]byte(clientID), PacketReceiptBasePrefix)\n}\n\n// PacketReceiptKey returns the store key of under which a packet receipt is\n// stored.\n// clientID must be a generated identifier, not provided externally so key\n// collisions are not possible.\nfunc PacketReceiptKey(clientID string, sequence uint64) []byte {\n\treturn append(PacketReceiptPrefixKey(clientID), encoding.Uint64ToBigEndian(sequence)...)\n}\n\n// PacketAcknowledgementPrefixKey returns the store key prefix under which\n// packet acknowledgements for a particular channel are stored.  clientID must\n// be a generated identifier, not provided externally so key collisions are not\n// possible.\nfunc PacketAcknowledgementPrefixKey(clientID string) []byte {\n\treturn append([]byte(clientID), PacketAcknowledgementBasePrefix)\n}\n\n// PacketAcknowledgementKey returns the store key of under which a packet\n// acknowledgement is stored.\n// clientID must be a generated identifier, not provided externally so key\n// collisions are not possible.\nfunc PacketAcknowledgementKey(clientID string, sequence uint64) []byte {\n\treturn append(PacketAcknowledgementPrefixKey(clientID), encoding.Uint64ToBigEndian(sequence)...)\n}\n"},{"name":"validate.gno","body":"package host\n\nimport (\n\t\"errors\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// DefaultMaxCharacterLength defines the default maximum character length used\n// in validation of identifiers including the client, connection, port and\n// channel identifiers.\n//\n// NOTE: this restriction is specific to this golang implementation of IBC. If\n// your use case demands a higher limit, please open an issue and we will consider\n// adjusting this restriction.\nconst DefaultMaxCharacterLength = 64\n\n// DefaultMaxPortCharacterLength defines the default maximum character length used\n// in validation of port identifiers.\nvar DefaultMaxPortCharacterLength = 128\n\n// IsValidID defines regular expression to check if the string consist of\n// characters in one of the following categories only:\n// - Alphanumeric\n// - `.`, `_`, `+`, `-`, `#`\n// - `[`, `]`, `\u003c`, `\u003e`\nvar IsValidID = regexp.MustCompile(`^[a-zA-Z0-9\\.\\_\\+\\-\\#\\[\\]\\\u003c\\\u003e]+$`).MatchString\n\n// ICS 024 Identifier and Path Validation Implementation\n//\n// This file defines ValidateFn to validate identifier and path strings\n// The spec for ICS 024 can be located here:\n// https://github.com/cosmos/ibc/tree/master/spec/core/ics-024-host-requirements\n\n// ValidateFn function type to validate path and identifier bytestrings\ntype ValidateFn func(string) error\n\nfunc defaultIdentifierValidator(id string, minLength, maxLength int) error {\n\tif strings.TrimSpace(id) == \"\" {\n\t\treturn errors.New(\"identifier cannot be blank\")\n\t}\n\t// valid id MUST NOT contain \"/\" separator\n\tif strings.Contains(id, \"/\") {\n\t\treturn ufmt.Errorf(\"identifier %s cannot contain separator '/'\", id)\n\t}\n\t// valid id must fit the length requirements\n\tif len(id) \u003c minLength || len(id) \u003e maxLength {\n\t\treturn ufmt.Errorf(\"identifier %s has invalid length: %d, must be between %d-%d characters\", id, len(id), minLength, maxLength)\n\t}\n\t// valid id must contain only alphanumeric characters and some allowed symbols.\n\tif !IsValidID(id) {\n\t\treturn ufmt.Errorf(\n\t\t\t\"identifier %s must contain only alphanumeric or the following characters: '.', '_', '+', '-', '#', '[', ']', '\u003c', '\u003e'\",\n\t\t\tid,\n\t\t)\n\t}\n\treturn nil\n}\n\n// ClientIdentifierValidator is the default validator function for Client identifiers.\n// A valid Identifier must be between 4-64 characters and only contain alphanumeric and some allowed\n// special characters (see IsValidID).\nfunc ClientIdentifierValidator(id string) error {\n\terr := defaultIdentifierValidator(id, 4, DefaultMaxCharacterLength)\n\tif err != nil {\n\t\treturn ufmt.Errorf(\"validate client identifier: %v\", err)\n\t}\n\treturn nil\n}\n\n// PortIdentifierValidator is the default validator function for Port identifiers.\n// A valid Identifier must be between 2-64 characters and only contain alphanumeric and some allowed\n// special characters (see IsValidID).\nfunc PortIdentifierValidator(id string) error {\n\terr := defaultIdentifierValidator(id, 2, DefaultMaxPortCharacterLength)\n\tif err != nil {\n\t\treturn ufmt.Errorf(\"validate port identifier: %v\", err)\n\t}\n\treturn nil\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"Pc1scsbhw0eBCT1FxIlRO+Pa5dqCzwXXqbmeeDx1wxB2oMDE1EioOoVzzWIhdiR1ypAuOgP21PITAJlIm0SMkA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"ics23","path":"gno.land/p/aib/ics23/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/aib/ics23/v0\"\ngno = \"0.9\"\n"},{"name":"ops.gno","body":"package ics23\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"errors\"\n\n\t// adds sha256 capability to crypto.SHA256\n\n\t\"crypto/sha256\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\ntype HashOp int32\n\nconst (\n\t// NO_HASH is the default if no data passed. Note this is an illegal argument some places.\n\tHashOp_NO_HASH HashOp = 0\n\tHashOp_SHA256  HashOp = 1\n)\n\n// LengthOp defines how to process the key and value of the LeafOp\n// to include length information. After encoding the length with the given\n// algorithm, the length will be prepended to the key and value bytes.\n// (Each one with it's own encoded length)\ntype LengthOp int32\n\nconst (\n\t// NO_PREFIX don't include any length info\n\tLengthOp_NO_PREFIX LengthOp = 0\n\t// VAR_PROTO uses protobuf (and go-amino) varint encoding of the length\n\tLengthOp_VAR_PROTO LengthOp = 1\n)\n\n// validateIavlOps validates the prefix to ensure it begins with\n// the height, size, and version of the IAVL tree. Each varint must be a bounded value.\n// In addition, the remaining bytes are validated to ensure they correspond to the correct\n// length. The layerNum is the inverse of the tree depth, i.e. depth=0 means leaf, depth\u003e=1 means inner node\nfunc validateIavlOps(op opType, layerNum int) error {\n\tr := bytes.NewReader(op.GetPrefix())\n\n\theight, err := binary.ReadVarint(r)\n\tif err != nil {\n\t\treturn ufmt.Errorf(\"failed to read IAVL height varint: %v\", err)\n\t}\n\tif int(height) \u003c 0 || int(height) \u003c layerNum {\n\t\treturn ufmt.Errorf(\"IAVL height (%d) must be non-negative and greater than or equal to the layer number (%d)\", height, layerNum)\n\t}\n\n\tsize, err := binary.ReadVarint(r)\n\tif err != nil {\n\t\treturn ufmt.Errorf(\"failed to read IAVL size varint: %v\", err)\n\t}\n\n\tif int(size) \u003c 0 {\n\t\treturn ufmt.Errorf(\"IAVL size must be non-negative\")\n\t}\n\n\tversion, err := binary.ReadVarint(r)\n\tif err != nil {\n\t\treturn ufmt.Errorf(\"failed to read IAVL version varint: %v\", err)\n\t}\n\n\tif int(version) \u003c 0 {\n\t\treturn ufmt.Errorf(\"IAVL version must be non-negative\")\n\t}\n\n\t// grab the length of the remainder of the prefix\n\tremLen := r.Len()\n\tif layerNum == 0 {\n\t\t// leaf node\n\t\tif remLen != 0 {\n\t\t\treturn ufmt.Errorf(\"expected remaining prefix length to be 0, got: %d\", remLen)\n\t\t}\n\t\tif height != 0 {\n\t\t\treturn ufmt.Errorf(\"expected leaf node height to be 0, got: %d\", remLen)\n\t\t}\n\t\tif size != 1 {\n\t\t\treturn ufmt.Errorf(\"expected leaf node size to be 1, got: %d\", remLen)\n\t\t}\n\t} else {\n\t\t// inner node\n\t\t//\n\t\t// when the child comes from the left, the suffix if filled in\n\t\t// prefix: height | size | version | length byte (1 remainder)\n\t\t//\n\t\t// when the child comes from the right, the suffix is empty\n\t\t// prefix: height | size | version | length byte | 32 byte hash | next length byte (34 remainder)\n\t\tif remLen != 1 \u0026\u0026 remLen != 34 {\n\t\t\treturn ufmt.Errorf(\"remainder of prefix must be of length 1 or 34, got: %d\", remLen)\n\t\t}\n\t\tif op.GetHash() != HashOp_SHA256 {\n\t\t\treturn ufmt.Errorf(\"IAVL hash op must be %v\", HashOp_SHA256)\n\t\t}\n\t}\n\treturn nil\n}\n\n// validateTendermintOps validates the prefix to ensure it begins with []byte{1}.\nfunc validateTendermintOps(op *InnerOp) error {\n\tif len(op.Prefix) == 0 {\n\t\treturn ufmt.Errorf(\"inner op prefix must not be empty\")\n\t}\n\n\tinnerPrefix := []byte{1}\n\tif op.Suffix != nil {\n\t\tif !bytes.Equal(op.Prefix, innerPrefix) {\n\t\t\treturn ufmt.Errorf(\"expected inner op prefix: %v, got: %v\", innerPrefix, op.Prefix)\n\t\t}\n\t}\n\n\tif !bytes.HasPrefix(op.Prefix, innerPrefix) {\n\t\treturn ufmt.Errorf(\"expected inner op prefix to begin with: %v, got: %v\", innerPrefix, op.Prefix[:1])\n\t}\n\n\treturn nil\n}\n\n// Apply will calculate the leaf hash given the key and value being proven\nfunc (op *LeafOp) Apply(key []byte, value []byte) ([]byte, error) {\n\tif len(key) == 0 {\n\t\treturn nil, errors.New(\"leaf op needs key\")\n\t}\n\tif len(value) == 0 {\n\t\treturn nil, errors.New(\"leaf op needs value\")\n\t}\n\tpkey, err := prepareLeafData(op.PrehashKey, op.Length, key)\n\tif err != nil {\n\t\treturn nil, ufmt.Errorf(\"prehash key, %v\", err)\n\t}\n\tpvalue, err := prepareLeafData(op.PrehashValue, op.Length, value)\n\tif err != nil {\n\t\treturn nil, ufmt.Errorf(\"prehash value, %v\", err)\n\t}\n\n\tdata := op.Prefix\n\tdata = append(data, pkey...)\n\tdata = append(data, pvalue...)\n\n\treturn doHash(op.Hash, data)\n}\n\n// Apply will calculate the hash of the next step, given the hash of the previous step\nfunc (op *InnerOp) Apply(child []byte) ([]byte, error) {\n\tif len(child) == 0 {\n\t\treturn nil, errors.New(\"inner op needs child value\")\n\t}\n\tpreimage := op.Prefix\n\tpreimage = append(preimage, child...)\n\tpreimage = append(preimage, op.Suffix...)\n\treturn doHash(op.Hash, preimage)\n}\n\n// CheckAgainstSpec will verify the LeafOp is in the format defined in spec\nfunc (op *LeafOp) CheckAgainstSpec(spec *ProofSpec) error {\n\tif spec == nil {\n\t\treturn errors.New(\"op and spec must be non-nil\")\n\t}\n\tlspec := spec.LeafSpec\n\tif lspec == nil {\n\t\treturn errors.New(\"spec.LeafSpec must be non-nil\")\n\t}\n\n\tif spec.SpecEquals(IavlSpec()) {\n\t\terr := validateIavlOps(op, 0)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif op.Hash != lspec.Hash {\n\t\treturn ufmt.Errorf(\"unexpected HashOp: %d\", op.Hash)\n\t}\n\tif op.PrehashKey != lspec.PrehashKey {\n\t\treturn ufmt.Errorf(\"unexpected PrehashKey: %d\", op.PrehashKey)\n\t}\n\tif op.PrehashValue != lspec.PrehashValue {\n\t\treturn ufmt.Errorf(\"unexpected PrehashValue: %d\", op.PrehashValue)\n\t}\n\tif op.Length != lspec.Length {\n\t\treturn ufmt.Errorf(\"unexpected LengthOp: %d\", op.Length)\n\t}\n\tif !bytes.HasPrefix(op.Prefix, lspec.Prefix) {\n\t\treturn ufmt.Errorf(\"leaf Prefix doesn't start with %X\", lspec.Prefix)\n\t}\n\treturn nil\n}\n\n// CheckAgainstSpec will verify the InnerOp is in the format defined in spec\nfunc (op *InnerOp) CheckAgainstSpec(spec *ProofSpec, b int) error {\n\tif spec == nil {\n\t\treturn errors.New(\"op and spec must be both non-nil\")\n\t}\n\tif spec.InnerSpec == nil {\n\t\treturn errors.New(\"spec.InnerSpec must be non-nil\")\n\t}\n\tif spec.LeafSpec == nil {\n\t\treturn errors.New(\"spec.LeafSpec must be non-nil\")\n\t}\n\n\tif op.Hash != spec.InnerSpec.Hash {\n\t\treturn ufmt.Errorf(\"unexpected HashOp: %d\", op.Hash)\n\t}\n\n\tif spec.SpecEquals(IavlSpec()) {\n\t\terr := validateIavlOps(op, b)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif spec.SpecEquals(TendermintSpec()) {\n\t\terr := validateTendermintOps(op)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tleafPrefix := spec.LeafSpec.Prefix\n\tif bytes.HasPrefix(op.Prefix, leafPrefix) {\n\t\treturn ufmt.Errorf(\"inner Prefix starts with %X\", leafPrefix)\n\t}\n\tif len(op.Prefix) \u003c int(spec.InnerSpec.MinPrefixLength) {\n\t\treturn ufmt.Errorf(\"innerOp prefix too short (%d)\", len(op.Prefix))\n\t}\n\tmaxLeftChildBytes := (len(spec.InnerSpec.ChildOrder) - 1) * int(spec.InnerSpec.ChildSize)\n\tif len(op.Prefix) \u003e int(spec.InnerSpec.MaxPrefixLength)+maxLeftChildBytes {\n\t\treturn ufmt.Errorf(\"innerOp prefix too long (%d)\", len(op.Prefix))\n\t}\n\n\tif spec.InnerSpec.ChildSize \u003c= 0 {\n\t\treturn errors.New(\"spec.InnerSpec.ChildSize must be \u003e= 1\")\n\t}\n\n\tif spec.InnerSpec.MaxPrefixLength \u003e= spec.InnerSpec.MinPrefixLength+spec.InnerSpec.ChildSize {\n\t\treturn errors.New(\"spec.InnerSpec.MaxPrefixLength must be \u003c spec.InnerSpec.MinPrefixLength + spec.InnerSpec.ChildSize\")\n\t}\n\n\t// ensures soundness, with suffix having to be of correct length\n\tif len(op.Suffix)%int(spec.InnerSpec.ChildSize) != 0 {\n\t\treturn ufmt.Errorf(\"InnerOp suffix malformed\")\n\t}\n\n\treturn nil\n}\n\n// doHash will preform the specified hash on the preimage.\n// if hashOp == NONE, it will return an error (use doHashOrNoop if you want different behavior)\nfunc doHash(hashOp HashOp, preimage []byte) ([]byte, error) {\n\tswitch hashOp {\n\tcase HashOp_SHA256:\n\t\th := sha256.Sum256(preimage)\n\t\treturn h[:], nil\n\t}\n\treturn nil, ufmt.Errorf(\"unsupported hashop: %d\", hashOp)\n}\n\nfunc prepareLeafData(hashOp HashOp, lengthOp LengthOp, data []byte) ([]byte, error) {\n\t// TODO: lengthop before or after hash ???\n\thdata, err := doHashOrNoop(hashOp, data)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn doLengthOp(lengthOp, hdata)\n}\n\ntype opType interface {\n\tGetPrefix() []byte\n\tGetHash() HashOp\n}\n\n// doLengthOp will calculate the proper prefix and return it prepended\n//\n//\tdoLengthOp(op, data) -\u003e length(data) || data\nfunc doLengthOp(lengthOp LengthOp, data []byte) ([]byte, error) {\n\tswitch lengthOp {\n\tcase LengthOp_NO_PREFIX:\n\t\treturn data, nil\n\tcase LengthOp_VAR_PROTO:\n\t\tres := append(encodeVarintProto(len(data)), data...)\n\t\treturn res, nil\n\t}\n\treturn nil, ufmt.Errorf(\"unsupported lengthop: %d\", lengthOp)\n}\n\nfunc encodeVarintProto(l int) []byte {\n\t// avoid multiple allocs for normal case\n\tres := make([]byte, 0, 8)\n\tfor l \u003e= 1\u003c\u003c7 {\n\t\tres = append(res, uint8(l\u00260x7f|0x80))\n\t\tl \u003e\u003e= 7\n\t}\n\tres = append(res, uint8(l))\n\treturn res\n}\n\n// doHashOrNoop will return the preimage untouched if hashOp == NONE,\n// otherwise, perform doHash\nfunc doHashOrNoop(hashOp HashOp, preimage []byte) ([]byte, error) {\n\tif hashOp == HashOp_NO_HASH {\n\t\treturn preimage, nil\n\t}\n\treturn doHash(hashOp, preimage)\n}\n"},{"name":"proof.gno","body":"package ics23\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\ntype CommitmentProof interface {\n\tGetExist() *ExistenceProof\n\tGetNonexist() *NonExistenceProof\n}\n\n// CheckProofs refuses any proof that is not one of this package's own\n// concrete types, CommitmentProof_Exist or CommitmentProof_Nonexist, by\n// value, before any of its methods runs: a caller-supplied implementation\n// would run its own code in the verifier's realm context. Call it on every\n// caller-supplied proof list before calling GetExist or GetNonexist.\nfunc CheckProofs(proofs []CommitmentProof) error {\n\tfor i, p := range proofs {\n\t\tswitch p.(type) {\n\t\tcase CommitmentProof_Exist, CommitmentProof_Nonexist:\n\t\tdefault:\n\t\t\treturn ufmt.Errorf(\"commitment proof #%d is not an ics23 proof\", i)\n\t\t}\n\t}\n\treturn nil\n}\n\ntype CommitmentProof_Exist struct {\n\tExist *ExistenceProof\n}\n\nvar _ CommitmentProof = CommitmentProof_Exist{}\n\nfunc (c CommitmentProof_Exist) GetExist() *ExistenceProof       { return c.Exist }\nfunc (c CommitmentProof_Exist) GetNonexist() *NonExistenceProof { return nil }\n\n// ExistenceProof takes a key and a value and a set of steps to perform on it.\n// The result of peforming all these steps will provide a \"root hash\", which can\n// be compared to the value in a header.\n//\n// Since it is computationally infeasible to produce a hash collission for any of the used\n// cryptographic hash functions, if someone can provide a series of operations to transform\n// a given key and value into a root hash that matches some trusted root, these key and values\n// must be in the referenced merkle tree.\n//\n// The only possible issue is maliablity in LeafOp, such as providing extra prefix data,\n// which should be controlled by a spec. Eg. with lengthOp as NONE,\n// prefix = FOO, key = BAR, value = CHOICE\n// and\n// prefix = F, key = OOBAR, value = CHOICE\n// would produce the same value.\n//\n// With LengthOp this is tricker but not impossible. Which is why the \"leafPrefixEqual\" field\n// in the ProofSpec is valuable to prevent this mutability. And why all trees should\n// length-prefix the data before hashing it.\ntype ExistenceProof struct {\n\tKey   []byte\n\tValue []byte\n\tLeaf  *LeafOp\n\tPath  []*InnerOp\n}\n\n// LeafOp represents the raw key-value data we wish to prove, and\n// must be flexible to represent the internal transformation from\n// the original key-value pairs into the basis hash, for many existing\n// merkle trees.\n//\n// key and value are passed in. So that the signature of this operation is:\n// leafOp(key, value) -\u003e output\n//\n// To process this, first prehash the keys and values if needed (ANY means no hash in this case):\n// hkey = prehashKey(key)\n// hvalue = prehashValue(value)\n//\n// Then combine the bytes, and hash it\n// output = hash(prefix || length(hkey) || hkey || length(hvalue) || hvalue)\ntype LeafOp struct {\n\tHash         HashOp\n\tPrehashKey   HashOp\n\tPrehashValue HashOp\n\tLength       LengthOp\n\t// prefix is a fixed bytes that may optionally be included at the beginning to differentiate\n\t// a leaf node from an inner node.\n\tPrefix []byte\n}\n\nfunc (m *LeafOp) GetHash() HashOp {\n\tif m != nil {\n\t\treturn m.Hash\n\t}\n\treturn HashOp_NO_HASH\n}\n\nfunc (m *LeafOp) GetPrefix() []byte {\n\treturn m.Prefix\n}\n\n// InnerOp represents a merkle-proof step that is not a leaf.\n// It represents concatenating two children and hashing them to provide the next result.\n//\n// The result of the previous step is passed in, so the signature of this op is:\n// innerOp(child) -\u003e output\n//\n// The result of applying InnerOp should be:\n// output = op.hash(op.prefix || child || op.suffix)\n//\n// where the || operator is concatenation of binary data,\n// and child is the result of hashing all the tree below this step.\n//\n// Any special data, like prepending child with the length, or prepending the entire operation with\n// some value to differentiate from leaf nodes, should be included in prefix and suffix.\n// If either of prefix or suffix is empty, we just treat it as an empty string\ntype InnerOp struct {\n\tHash   HashOp\n\tPrefix []byte\n\tSuffix []byte\n}\n\nfunc (m *InnerOp) GetHash() HashOp {\n\tif m != nil {\n\t\treturn m.Hash\n\t}\n\treturn HashOp_NO_HASH\n}\n\nfunc (m *InnerOp) GetPrefix() []byte {\n\treturn m.Prefix\n}\n\n// Verify does all checks to ensure this proof proves this key, value -\u003e root\n// and matches the spec.\nfunc (p *ExistenceProof) Verify(spec *ProofSpec, root, key, value []byte) error {\n\tif err := p.CheckAgainstSpec(spec); err != nil {\n\t\treturn err\n\t}\n\n\tif !bytes.Equal(key, p.Key) {\n\t\treturn ufmt.Errorf(\"provided key doesn't match proof\")\n\t}\n\tif !bytes.Equal(value, p.Value) {\n\t\treturn ufmt.Errorf(\"provided value doesn't match proof\")\n\t}\n\n\tcalc, err := p.calculate(spec)\n\tif err != nil {\n\t\treturn ufmt.Errorf(\"error calculating root, %v\", err)\n\t}\n\tif !bytes.Equal(root, calc) {\n\t\treturn ufmt.Errorf(\"calculated root doesn't match provided root\")\n\t}\n\n\treturn nil\n}\n\n// Calculate determines the root hash that matches the given proof.\n// You must validate the result is what you have in a header.\n// Returns error if the calculations cannot be performed.\nfunc (p *ExistenceProof) Calculate() ([]byte, error) {\n\treturn p.calculate(nil)\n}\n\nfunc (p *ExistenceProof) calculate(spec *ProofSpec) ([]byte, error) {\n\tif p.Leaf == nil {\n\t\treturn nil, errors.New(\"existence Proof needs defined LeafOp\")\n\t}\n\n\t// leaf step takes the key and value as input\n\tres, err := p.Leaf.Apply(p.Key, p.Value)\n\tif err != nil {\n\t\treturn nil, ufmt.Errorf(\"leaf, %v\", err)\n\t}\n\n\t// the rest just take the output of the last step (reducing it)\n\tfor _, step := range p.Path {\n\t\tres, err = step.Apply(res)\n\t\tif err != nil {\n\t\t\treturn nil, ufmt.Errorf(\"inner, %v\", err)\n\t\t}\n\t\tif spec != nil {\n\t\t\tif len(res) \u003e int(spec.InnerSpec.ChildSize) \u0026\u0026 int(spec.InnerSpec.ChildSize) \u003e= 32 {\n\t\t\t\treturn nil, ufmt.Errorf(\"inner, %v\", err)\n\t\t\t}\n\t\t}\n\t}\n\treturn res, nil\n}\n\ntype CommitmentProof_Nonexist struct {\n\tNonexist *NonExistenceProof\n}\n\nvar _ CommitmentProof = CommitmentProof_Nonexist{}\n\nfunc (c CommitmentProof_Nonexist) GetExist() *ExistenceProof       { return nil }\nfunc (c CommitmentProof_Nonexist) GetNonexist() *NonExistenceProof { return c.Nonexist }\n\n// NonExistenceProof takes a proof of two neighbors, one left of the desired key,\n// one right of the desired key. If both proofs are valid AND they are neighbors,\n// then there is no valid proof for the given key.\ntype NonExistenceProof struct {\n\tKey   []byte\n\tLeft  *ExistenceProof\n\tRight *ExistenceProof\n}\n\n// Calculate determines the root hash that matches the given nonexistence proof.\n// You must validate the result is what you have in a header.\n// Returns error if the calculations cannot be performed.\nfunc (p *NonExistenceProof) Calculate() ([]byte, error) {\n\t// A Nonexist proof may have left or right proof nil\n\tswitch {\n\tcase p.Left != nil:\n\t\treturn p.Left.Calculate()\n\tcase p.Right != nil:\n\t\treturn p.Right.Calculate()\n\tdefault:\n\t\treturn nil, errors.New(\"nonexistence proof has empty Left and Right proof\")\n\t}\n}\n\n// CheckAgainstSpec will verify the leaf and all path steps are in the format defined in spec\nfunc (p *ExistenceProof) CheckAgainstSpec(spec *ProofSpec) error {\n\tleaf := p.Leaf\n\tif leaf == nil {\n\t\treturn errors.New(\"existence Proof needs defined LeafOp\")\n\t}\n\tif err := leaf.CheckAgainstSpec(spec); err != nil {\n\t\treturn ufmt.Errorf(\"leaf, %v\", err)\n\t}\n\tif spec.MinDepth \u003e 0 \u0026\u0026 len(p.Path) \u003c int(spec.MinDepth) {\n\t\treturn ufmt.Errorf(\"innerOps depth too short: %d\", len(p.Path))\n\t}\n\n\tmaxDepth := spec.MaxDepth\n\tif maxDepth == 0 {\n\t\tmaxDepth = 128\n\t}\n\n\tif len(p.Path) \u003e int(maxDepth) {\n\t\treturn ufmt.Errorf(\"innerOps depth too long: %d\", len(p.Path))\n\t}\n\n\tlayerNum := 1\n\n\tfor _, inner := range p.Path {\n\t\tif err := inner.CheckAgainstSpec(spec, layerNum); err != nil {\n\t\t\treturn ufmt.Errorf(\"inner, %v\", err)\n\t\t}\n\t\tlayerNum++\n\t}\n\treturn nil\n}\n\n// If we should prehash the key before comparison, do so; otherwise, return the key. Prehashing\n// changes lexical comparison, so we do so before comparison if the spec sets\n// `PrehashKeyBeforeComparison`.\nfunc keyForComparison(spec *ProofSpec, key []byte) []byte {\n\tif !spec.PrehashKeyBeforeComparison {\n\t\treturn key\n\t}\n\thash, _ := doHashOrNoop(spec.LeafSpec.PrehashKey, key)\n\treturn hash\n}\n\n// Verify does all checks to ensure the proof has valid non-existence proofs,\n// and they ensure the given key is not in the CommitmentState\nfunc (p *NonExistenceProof) Verify(spec *ProofSpec, root, key []byte) error {\n\t// ensure the existence proofs are valid\n\tvar leftKey, rightKey []byte\n\tif p.Left != nil {\n\t\tif err := p.Left.Verify(spec, root, p.Left.Key, p.Left.Value); err != nil {\n\t\t\treturn ufmt.Errorf(\"left proof, %v\", err)\n\t\t}\n\t\tleftKey = p.Left.Key\n\t}\n\tif p.Right != nil {\n\t\tif err := p.Right.Verify(spec, root, p.Right.Key, p.Right.Value); err != nil {\n\t\t\treturn ufmt.Errorf(\"right proof, %v\", err)\n\t\t}\n\t\trightKey = p.Right.Key\n\t}\n\n\t// If both proofs are missing, this is not a valid proof\n\tif leftKey == nil \u0026\u0026 rightKey == nil {\n\t\treturn errors.New(\"both left and right proofs missing\")\n\t}\n\n\t// Ensure in valid range\n\tif rightKey != nil {\n\t\tif bytes.Compare(keyForComparison(spec, key), keyForComparison(spec, rightKey)) \u003e= 0 {\n\t\t\treturn errors.New(\"key is not left of right proof\")\n\t\t}\n\t}\n\n\tif leftKey != nil {\n\t\tif bytes.Compare(keyForComparison(spec, key), keyForComparison(spec, leftKey)) \u003c= 0 {\n\t\t\treturn errors.New(\"key is not right of left proof\")\n\t\t}\n\t}\n\n\tswitch {\n\tcase leftKey == nil:\n\t\tisLeftMost, err := IsLeftMost(spec.InnerSpec, p.Right.Path)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif !isLeftMost {\n\t\t\treturn errors.New(\"left proof missing, right proof must be left-most\")\n\t\t}\n\tcase rightKey == nil:\n\t\tisRightMost, err := IsRightMost(spec.InnerSpec, p.Left.Path)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif !isRightMost {\n\t\t\treturn errors.New(\"right proof missing, left proof must be right-most\")\n\t\t}\n\tdefault:\n\t\tisLeftNeighbor, err := IsLeftNeighbor(spec.InnerSpec, p.Left.Path, p.Right.Path)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif !isLeftNeighbor {\n\t\t\treturn errors.New(\"right proof missing, left proof must be right-most\")\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// IsLeftMost returns true if this is the left-most path in the tree, excluding placeholder (empty child) nodes\nfunc IsLeftMost(spec *InnerSpec, path []*InnerOp) (bool, error) {\n\tminPrefix, maxPrefix, suffix, err := getPadding(spec, 0)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\t// ensure every step has a prefix and suffix defined to be leftmost, unless it is a placeholder node\n\tfor _, step := range path {\n\t\tif !hasPadding(step, minPrefix, maxPrefix, suffix) {\n\t\t\tleftBranchesAreEmpty, err := leftBranchesAreEmpty(spec, step)\n\t\t\tif err != nil {\n\t\t\t\treturn false, err\n\t\t\t}\n\n\t\t\tif !leftBranchesAreEmpty {\n\t\t\t\treturn false, nil\n\t\t\t}\n\t\t}\n\t}\n\treturn true, nil\n}\n\n// IsRightMost returns true if this is the right-most path in the tree, excluding placeholder (empty child) nodes\nfunc IsRightMost(spec *InnerSpec, path []*InnerOp) (bool, error) {\n\tlast := len(spec.ChildOrder) - 1\n\tminPrefix, maxPrefix, suffix, err := getPadding(spec, int32(last))\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\t// ensure every step has a prefix and suffix defined to be rightmost, unless it is a placeholder node\n\tfor _, step := range path {\n\t\tif !hasPadding(step, minPrefix, maxPrefix, suffix) {\n\t\t\trightBranchesAreEmpty, err := rightBranchesAreEmpty(spec, step)\n\t\t\tif err != nil {\n\t\t\t\treturn false, err\n\t\t\t}\n\n\t\t\tif !rightBranchesAreEmpty {\n\t\t\t\treturn false, nil\n\t\t\t}\n\t\t}\n\t}\n\treturn true, nil\n}\n\n// IsLeftNeighbor returns true if `right` is the next possible path right of `left`\n//\n//\tFind the common suffix from the Left.Path and Right.Path and remove it. We have LPath and RPath now, which must be neighbors.\n//\tValidate that LPath[len-1] is the left neighbor of RPath[len-1]\n//\tFor step in LPath[0..len-1], validate step is right-most node\n//\tFor step in RPath[0..len-1], validate step is left-most node\nfunc IsLeftNeighbor(spec *InnerSpec, left []*InnerOp, right []*InnerOp) (bool, error) {\n\tif len(left) == 0 || len(right) == 0 {\n\t\treturn false, nil\n\t}\n\n\t// count common tail (from end, near root)\n\tleft, topleft := left[:len(left)-1], left[len(left)-1]\n\tright, topright := right[:len(right)-1], right[len(right)-1]\n\tfor bytes.Equal(topleft.Prefix, topright.Prefix) \u0026\u0026 bytes.Equal(topleft.Suffix, topright.Suffix) {\n\t\tif len(left) == 0 || len(right) == 0 {\n\t\t\treturn false, nil\n\t\t}\n\t\tleft, topleft = left[:len(left)-1], left[len(left)-1]\n\t\tright, topright = right[:len(right)-1], right[len(right)-1]\n\t}\n\n\t// now topleft and topright are the first divergent nodes\n\t// make sure they are left and right of each other\n\tok, err := isLeftStep(spec, topleft, topright)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif !ok {\n\t\treturn false, nil\n\t}\n\n\t// left and right are remaining children below the split,\n\t// ensure left child is the rightmost path, and visa versa\n\tisRightMost, err := IsRightMost(spec, left)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif !isRightMost {\n\t\treturn false, nil\n\t}\n\n\tisLeftMost, err := IsLeftMost(spec, right)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif !isLeftMost {\n\t\treturn false, nil\n\t}\n\treturn true, nil\n}\n\n// isLeftStep assumes left and right have common parents\n// checks if left is exactly one slot to the left of right\nfunc isLeftStep(spec *InnerSpec, left *InnerOp, right *InnerOp) (bool, error) {\n\tleftidx, err := orderFromPadding(spec, left)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\trightidx, err := orderFromPadding(spec, right)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\t// TODO: is it possible there are empty (nil) children???\n\treturn rightidx == leftidx+1, nil\n}\n\n// checks if an op has the expected padding\nfunc hasPadding(op *InnerOp, minPrefix, maxPrefix, suffix int) bool {\n\tif len(op.Prefix) \u003c minPrefix {\n\t\treturn false\n\t}\n\tif len(op.Prefix) \u003e maxPrefix {\n\t\treturn false\n\t}\n\treturn len(op.Suffix) == suffix\n}\n\n// getPadding determines prefix and suffix with the given spec and position in the tree\nfunc getPadding(spec *InnerSpec, branch int32) (minPrefix, maxPrefix, suffix int, err error) {\n\tidx, err := getPosition(spec.ChildOrder, branch)\n\tif err != nil {\n\t\treturn 0, 0, 0, err\n\t}\n\n\t// count how many children are in the prefix\n\tprefix := idx * int(spec.ChildSize)\n\tminPrefix = prefix + int(spec.MinPrefixLength)\n\tmaxPrefix = prefix + int(spec.MaxPrefixLength)\n\n\t// count how many children are in the suffix\n\tsuffix = (len(spec.ChildOrder) - 1 - idx) * int(spec.ChildSize)\n\treturn minPrefix, maxPrefix, suffix, nil\n}\n\n// leftBranchesAreEmpty returns true if the padding bytes correspond to all empty siblings\n// on the left side of a branch, ie. it's a valid placeholder on a leftmost path\nfunc leftBranchesAreEmpty(spec *InnerSpec, op *InnerOp) (bool, error) {\n\tidx, err := orderFromPadding(spec, op)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\t// count branches to left of this\n\tleftBranches := int(idx)\n\tif leftBranches == 0 {\n\t\treturn false, nil\n\t}\n\t// compare prefix with the expected number of empty branches\n\tactualPrefix := len(op.Prefix) - leftBranches*int(spec.ChildSize)\n\tif actualPrefix \u003c 0 {\n\t\treturn false, nil\n\t}\n\tfor i := 0; i \u003c leftBranches; i++ {\n\t\tidx, err := getPosition(spec.ChildOrder, int32(i))\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\tfrom := actualPrefix + idx*int(spec.ChildSize)\n\t\tif !bytes.Equal(spec.EmptyChild, op.Prefix[from:from+int(spec.ChildSize)]) {\n\t\t\treturn false, nil\n\t\t}\n\t}\n\treturn true, nil\n}\n\n// rightBranchesAreEmpty returns true if the padding bytes correspond to all empty siblings\n// on the right side of a branch, ie. it's a valid placeholder on a rightmost path\nfunc rightBranchesAreEmpty(spec *InnerSpec, op *InnerOp) (bool, error) {\n\tidx, err := orderFromPadding(spec, op)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\t// count branches to right of this one\n\trightBranches := len(spec.ChildOrder) - 1 - int(idx)\n\tif rightBranches == 0 {\n\t\treturn false, nil\n\t}\n\t// compare suffix with the expected number of empty branches\n\tif len(op.Suffix) != rightBranches*int(spec.ChildSize) {\n\t\treturn false, nil // sanity check\n\t}\n\tfor i := 0; i \u003c rightBranches; i++ {\n\t\tidx, err := getPosition(spec.ChildOrder, int32(i))\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\tfrom := idx * int(spec.ChildSize)\n\t\tif !bytes.Equal(spec.EmptyChild, op.Suffix[from:from+int(spec.ChildSize)]) {\n\t\t\treturn false, nil\n\t\t}\n\t}\n\treturn true, nil\n}\n\n// getPosition checks where the branch is in the order and returns\n// the index of this branch\nfunc getPosition(order []int32, branch int32) (int, error) {\n\tif branch \u003c 0 || int(branch) \u003e= len(order) {\n\t\treturn -1, ufmt.Errorf(\"invalid branch: %d\", branch)\n\t}\n\tfor i, item := range order {\n\t\tif branch == item {\n\t\t\treturn i, nil\n\t\t}\n\t}\n\n\treturn -1, ufmt.Errorf(\"branch %d not found in order %v\", branch, order)\n}\n\n// This will look at the proof and determine which order it is...\n// So we can see if it is branch 0, 1, 2 etc... to determine neighbors\nfunc orderFromPadding(spec *InnerSpec, inner *InnerOp) (int32, error) {\n\tmaxbranch := int32(len(spec.ChildOrder))\n\tfor branch := int32(0); branch \u003c maxbranch; branch++ {\n\t\tminp, maxp, suffix, err := getPadding(spec, branch)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tif hasPadding(inner, minp, maxp, suffix) {\n\t\t\treturn branch, nil\n\t\t}\n\t}\n\treturn 0, errors.New(\"cannot find any valid spacing for this node\")\n}\n\n// Equal returns true if the two ProofSpec instances are deeply equal across\n// every field. Use this when you need a strict content comparison (e.g. to\n// detect attempts at substituting a different spec for an existing client).\n// For the looser, \"same shape\" comparison historically used to route proof\n// verification through IavlSpec/TendermintSpec, use SpecEquals instead.\nfunc (p *ProofSpec) Equal(other *ProofSpec) bool {\n\tif p == nil || other == nil {\n\t\treturn p == other\n\t}\n\tif p.MaxDepth != other.MaxDepth ||\n\t\tp.MinDepth != other.MinDepth ||\n\t\tp.PrehashKeyBeforeComparison != other.PrehashKeyBeforeComparison {\n\t\treturn false\n\t}\n\tif !p.LeafSpec.Equal(other.LeafSpec) {\n\t\treturn false\n\t}\n\treturn p.InnerSpec.Equal(other.InnerSpec)\n}\n\n// Equal returns true if the two LeafOp instances are deeply equal.\nfunc (p *LeafOp) Equal(other *LeafOp) bool {\n\tif p == nil || other == nil {\n\t\treturn p == other\n\t}\n\treturn p.Hash == other.Hash \u0026\u0026\n\t\tp.PrehashKey == other.PrehashKey \u0026\u0026\n\t\tp.PrehashValue == other.PrehashValue \u0026\u0026\n\t\tp.Length == other.Length \u0026\u0026\n\t\tbytes.Equal(p.Prefix, other.Prefix)\n}\n\n// Equal returns true if the two InnerSpec instances are deeply equal.\nfunc (p *InnerSpec) Equal(other *InnerSpec) bool {\n\tif p == nil || other == nil {\n\t\treturn p == other\n\t}\n\tif p.ChildSize != other.ChildSize ||\n\t\tp.MinPrefixLength != other.MinPrefixLength ||\n\t\tp.MaxPrefixLength != other.MaxPrefixLength ||\n\t\tp.Hash != other.Hash {\n\t\treturn false\n\t}\n\tif len(p.ChildOrder) != len(other.ChildOrder) {\n\t\treturn false\n\t}\n\tfor i := range p.ChildOrder {\n\t\tif p.ChildOrder[i] != other.ChildOrder[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn bytes.Equal(p.EmptyChild, other.EmptyChild)\n}\n\n// SpecEquals returns true if two ProofSpec instances match on the subset of\n// fields that matter for routing proof verification through a known spec\n// (IavlSpec/TendermintSpec). It intentionally ignores MaxDepth/MinDepth,\n// LeafOp.Prefix, ChildOrder contents, and InnerSpec.EmptyChild — it\n// over-declares equality, which we consider fine for that use case.\n// Use Equal for strict field-by-field comparison (e.g. RecoverClient).\nfunc (p *ProofSpec) SpecEquals(spec *ProofSpec) bool {\n\t// 1. Compare LeafSpecs values.\n\tswitch {\n\tcase (p.LeafSpec == nil) != (spec.LeafSpec == nil): // One of them is nil.\n\t\treturn false\n\n\tcase p.LeafSpec != nil \u0026\u0026 spec.LeafSpec != nil:\n\t\tok := p.LeafSpec.Hash == spec.LeafSpec.Hash \u0026\u0026\n\t\t\tp.LeafSpec.PrehashKey == spec.LeafSpec.PrehashKey \u0026\u0026\n\t\t\tp.LeafSpec.PrehashValue == spec.LeafSpec.PrehashValue \u0026\u0026\n\t\t\tp.LeafSpec.Length == spec.LeafSpec.Length\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\n\tdefault: // Both are nil, hence LeafSpec values are equal.\n\t}\n\n\t// 2. Compare InnerSpec values.\n\tswitch {\n\tcase (p.InnerSpec == nil) != (spec.InnerSpec == nil): // One of them is not nil.\n\t\treturn false\n\n\tcase p.InnerSpec != nil \u0026\u0026 spec.InnerSpec != nil: // Both are non-nil\n\t\tok := p.InnerSpec.Hash == spec.InnerSpec.Hash \u0026\u0026\n\t\t\tp.InnerSpec.MinPrefixLength == spec.InnerSpec.MinPrefixLength \u0026\u0026\n\t\t\tp.InnerSpec.MaxPrefixLength == spec.InnerSpec.MaxPrefixLength \u0026\u0026\n\t\t\tp.InnerSpec.ChildSize == spec.InnerSpec.ChildSize \u0026\u0026\n\t\t\tlen(p.InnerSpec.ChildOrder) == len(spec.InnerSpec.ChildOrder)\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\n\tdefault: // Both are nil, hence InnerSpec values are equal.\n\t}\n\n\t// By this point all the above conditions pass so they are equal.\n\treturn true\n}\n"},{"name":"specs.gno","body":"package ics23\n\nimport (\n\t\"gno.land/p/aib/encoding/proto/v0\"\n)\n\n// GetSDKSpecs returns the proof specs of an SDK chain.\nfunc GetSDKProofSpecs() []*ProofSpec {\n\treturn []*ProofSpec{IavlSpec(), TendermintSpec()}\n}\n\n// IavlSpec constrains the format from proofs-iavl (iavl merkle proofs)\nfunc IavlSpec() *ProofSpec {\n\treturn \u0026ProofSpec{\n\t\tLeafSpec: \u0026LeafOp{\n\t\t\tPrefix:       []byte{0},\n\t\t\tPrehashKey:   HashOp_NO_HASH,\n\t\t\tHash:         HashOp_SHA256,\n\t\t\tPrehashValue: HashOp_SHA256,\n\t\t\tLength:       LengthOp_VAR_PROTO,\n\t\t},\n\t\tInnerSpec: \u0026InnerSpec{\n\t\t\tChildOrder:      []int32{0, 1},\n\t\t\tMinPrefixLength: 4,\n\t\t\tMaxPrefixLength: 12,\n\t\t\tChildSize:       33, // (with length byte)\n\t\t\tEmptyChild:      nil,\n\t\t\tHash:            HashOp_SHA256,\n\t\t},\n\t}\n}\n\n// TendermintSpec constrains the format from proofs-tendermint (crypto/merkle SimpleProof)\nfunc TendermintSpec() *ProofSpec {\n\treturn \u0026ProofSpec{\n\t\tLeafSpec: \u0026LeafOp{\n\t\t\tPrefix:       []byte{0},\n\t\t\tPrehashKey:   HashOp_NO_HASH,\n\t\t\tHash:         HashOp_SHA256,\n\t\t\tPrehashValue: HashOp_SHA256,\n\t\t\tLength:       LengthOp_VAR_PROTO,\n\t\t},\n\t\tInnerSpec: \u0026InnerSpec{\n\t\t\tChildOrder:      []int32{0, 1},\n\t\t\tMinPrefixLength: 1,\n\t\t\tMaxPrefixLength: 1,\n\t\t\tChildSize:       32, // (no length byte)\n\t\t\tHash:            HashOp_SHA256,\n\t\t},\n\t}\n}\n\n// ProofSpec defines what the expected parameters are for a given proof type.\n// This can be stored in the client and used to validate any incoming proofs.\n//\n// verify(ProofSpec, Proof) -\u003e Proof | Error\n//\n// As demonstrated in tests, if we don't fix the algorithm used to calculate the\n// LeafHash for a given tree, there are many possible key-value pairs that can\n// generate a given hash (by interpretting the preimage differently).\n// We need this for proper security, requires client knows a priori what\n// tree format server uses. But not in code, rather a configuration object.\ntype ProofSpec struct {\n\t// any field in the ExistenceProof must be the same as in this spec.\n\t// except Prefix, which is just the first bytes of prefix (spec can be longer)\n\tLeafSpec  *LeafOp\n\tInnerSpec *InnerSpec\n\t// max_depth (if \u003e 0) is the maximum number of InnerOps allowed (mainly for fixed-depth tries)\n\t// the max_depth is interpreted as 128 if set to 0\n\tMaxDepth int32\n\t// min_depth (if \u003e 0) is the minimum number of InnerOps allowed (mainly for fixed-depth tries)\n\tMinDepth int32\n\t// prehash_key_before_comparison is a flag that indicates whether to use the\n\t// prehash_key specified by LeafOp to compare lexical ordering of keys for\n\t// non-existence proofs.\n\tPrehashKeyBeforeComparison bool\n}\n\n// InnerSpec contains all store-specific structure info to determine if two proofs from a\n// given store are neighbors.\n//\n// This enables:\n//\n// isLeftMost(spec: InnerSpec, op: InnerOp)\n// isRightMost(spec: InnerSpec, op: InnerOp)\n// isLeftNeighbor(spec: InnerSpec, left: InnerOp, right: InnerOp)\n// Clone is a deep copy of s: no slice or pointer is shared, so a verifier\n// that keeps the copy is unaffected by later writes to s.\nfunc (s *ProofSpec) Clone() *ProofSpec {\n\tif s == nil {\n\t\treturn nil\n\t}\n\tout := *s\n\tif s.LeafSpec != nil {\n\t\tleaf := *s.LeafSpec\n\t\tleaf.Prefix = cloneBytes(s.LeafSpec.Prefix)\n\t\tout.LeafSpec = \u0026leaf\n\t}\n\tif s.InnerSpec != nil {\n\t\tinner := *s.InnerSpec\n\t\tif s.InnerSpec.ChildOrder != nil {\n\t\t\tinner.ChildOrder = append([]int32{}, s.InnerSpec.ChildOrder...)\n\t\t}\n\t\tinner.EmptyChild = cloneBytes(s.InnerSpec.EmptyChild)\n\t\tout.InnerSpec = \u0026inner\n\t}\n\treturn \u0026out\n}\n\nfunc cloneBytes(b []byte) []byte {\n\tif b == nil {\n\t\treturn nil\n\t}\n\treturn append([]byte{}, b...)\n}\n\ntype InnerSpec struct {\n\t// Child order is the ordering of the children node, must count from 0\n\t// iavl tree is [0, 1] (left then right)\n\t// merk is [0, 2, 1] (left, right, here)\n\tChildOrder      []int32\n\tChildSize       int32\n\tMinPrefixLength int32\n\tMaxPrefixLength int32\n\t// empty child is the prehash image that is used when one child is nil (eg. 20 bytes of 0)\n\tEmptyChild []byte\n\t// hash is the algorithm that must be used for each InnerOp\n\tHash HashOp\n}\n\n// ProtoMarshal returns the proto3 wire encoding of the ProofSpec, matching\n// the format produced by gogoproto in cosmos/ics23. A nil receiver is encoded\n// as an empty byte slice (proto3 unset message).\nfunc (p *ProofSpec) ProtoMarshal() []byte {\n\tif p == nil {\n\t\treturn nil\n\t}\n\tvar bz []byte\n\tbz = proto.AppendLengthDelimited(bz, 1, p.LeafSpec.ProtoMarshal())\n\tbz = proto.AppendLengthDelimited(bz, 2, p.InnerSpec.ProtoMarshal())\n\tbz = proto.AppendVarint(bz, 3, uint64(int64(p.MaxDepth)))\n\tbz = proto.AppendVarint(bz, 4, uint64(int64(p.MinDepth)))\n\tif p.PrehashKeyBeforeComparison {\n\t\tbz = proto.AppendVarint(bz, 5, 1)\n\t}\n\treturn bz\n}\n\n// ProtoMarshal returns the proto3 wire encoding of the LeafOp.\nfunc (p *LeafOp) ProtoMarshal() []byte {\n\tif p == nil {\n\t\treturn nil\n\t}\n\tvar bz []byte\n\tbz = proto.AppendVarint(bz, 1, uint64(int64(p.Hash)))\n\tbz = proto.AppendVarint(bz, 2, uint64(int64(p.PrehashKey)))\n\tbz = proto.AppendVarint(bz, 3, uint64(int64(p.PrehashValue)))\n\tbz = proto.AppendVarint(bz, 4, uint64(int64(p.Length)))\n\tbz = proto.AppendLengthDelimited(bz, 5, p.Prefix)\n\treturn bz\n}\n\n// ProtoMarshal returns the proto3 wire encoding of the InnerSpec. ChildOrder\n// is encoded as a packed repeated int32 field per proto3 default.\nfunc (p *InnerSpec) ProtoMarshal() []byte {\n\tif p == nil {\n\t\treturn nil\n\t}\n\tvar bz []byte\n\tbz = proto.AppendPackedVarintInt32(bz, 1, p.ChildOrder)\n\tbz = proto.AppendVarint(bz, 2, uint64(int64(p.ChildSize)))\n\tbz = proto.AppendVarint(bz, 3, uint64(int64(p.MinPrefixLength)))\n\tbz = proto.AppendVarint(bz, 4, uint64(int64(p.MaxPrefixLength)))\n\tbz = proto.AppendLengthDelimited(bz, 5, p.EmptyChild)\n\tbz = proto.AppendVarint(bz, 6, uint64(int64(p.Hash)))\n\treturn bz\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"c85FP/Upanv9jTcSPFNyvFgGl/A42+4He/bJlJnq8mhoMcivZ5nyYyAWylCVEF/SL9lC1oaQNRL/tLc3irBn4Q=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"types","path":"gno.land/p/aib/ibc/types/v0","files":[{"name":"client.gno","body":"package types\n\nimport (\n\t\"math\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/aib/ibc/host/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// ValidateClientType validates the client type. It cannot be blank or empty.\n// It must be a valid client identifier when used with '0' or the maximum\n// uint64 as the sequence.\nfunc ValidateClientType(clientType string) error {\n\tif strings.TrimSpace(clientType) == \"\" {\n\t\treturn ufmt.Errorf(\"client type cannot be blank\")\n\t}\n\n\tsmallestPossibleClientID := FormatClientIdentifier(clientType, 0)\n\tlargestPossibleClientID := FormatClientIdentifier(clientType, uint64(math.MaxUint64))\n\n\t// IsValidClientID will check client type format and if the sequence is a uint64\n\tif !IsValidClientID(smallestPossibleClientID) {\n\t\treturn ufmt.Errorf(\"not a valid clientID: %s\", smallestPossibleClientID)\n\t}\n\n\tif err := host.ClientIdentifierValidator(smallestPossibleClientID); err != nil {\n\t\treturn err\n\t}\n\tif err := host.ClientIdentifierValidator(largestPossibleClientID); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// FormatClientIdentifier returns the client identifier with the sequence appended.\n// This is an SDK specific format not enforced by IBC protocol.\nfunc FormatClientIdentifier(clientType string, sequence uint64) string {\n\treturn ufmt.Sprintf(\"%s-%d\", clientType, sequence)\n}\n\n// IsClientIDFormat checks if a clientID is in the format required on the SDK\n// for parsing client identifiers. The client identifier must be in the form:\n// `{client-type}-{N}` which per the specification only permits ASCII for the\n// {client-type} segment and 1 to 20 digits for the {N} segment.\n// `([\\w-]+\\w)?` allows for a letter or hyphen, with the {client-type} starting\n// with a letter and ending with a letter, i.e.\n// `letter+(letter|hyphen+letter)?`.\nvar IsClientIDFormat = regexp.MustCompile(`^\\w+([\\w-]+\\w)?-[0-9]{1,20}$`).MatchString\n\n// IsValidClientID checks if the clientID is valid and can be parsed into the client\n// identifier format.\nfunc IsValidClientID(clientID string) bool {\n\t_, _, err := ParseClientIdentifier(clientID)\n\treturn err == nil\n}\n\n// ParseClientIdentifier parses the client type and sequence from the client identifier.\nfunc ParseClientIdentifier(clientID string) (string, uint64, error) {\n\tif !IsClientIDFormat(clientID) {\n\t\treturn \"\", 0, ufmt.Errorf(\"invalid client identifier %s is not in format: `{client-type}-{N}`\", clientID)\n\t}\n\n\tsplitStr := strings.Split(clientID, \"-\")\n\tlastIndex := len(splitStr) - 1\n\n\tclientType := strings.Join(splitStr[:lastIndex], \"-\")\n\tif strings.TrimSpace(clientType) == \"\" {\n\t\treturn \"\", 0, ufmt.Errorf(\"client identifier must be in format: `{client-type}-{N}` and client type cannot be blank\")\n\t}\n\n\tsequence, err := strconv.ParseUint(splitStr[lastIndex], 10, 64)\n\tif err != nil {\n\t\treturn \"\", 0, ufmt.Errorf(\"failed to parse client identifier sequence\")\n\t}\n\n\treturn clientType, sequence, nil\n}\n\n// RouterClientIDPrefix is the prefix of the client ids an Ethereum ICS26\n// router (solidity-ibc-eureka) generates from its own counter.\nconst RouterClientIDPrefix = \"client-\"\n\n// IsValidCustomClientID reports whether id is a custom client id an Ethereum\n// ICS26 router accepts (solidity-ibc-eureka IBCIdentifiers.\n// validateCustomIBCIdentifier): 4 to 128 characters of a-z, A-Z, 0-9 and\n// . _ + - # [ ] \u003c \u003e, not starting with \"client-\" (the router's own counter)\n// or \"channel-\". A client id governance adds to the router is one.\nfunc IsValidCustomClientID(id string) bool {\n\tif len(id) \u003c 4 || len(id) \u003e 128 || strings.HasPrefix(id, RouterClientIDPrefix) || strings.HasPrefix(id, \"channel-\") {\n\t\treturn false\n\t}\n\tfor i := 0; i \u003c len(id); i++ {\n\t\tc := id[i]\n\t\tswitch {\n\t\tcase 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\t\tcase c == '.' || c == '_' || c == '+' || c == '-' || c == '#' || c == '[' || c == ']' || c == '\u003c' || c == '\u003e':\n\t\tdefault:\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// IsValidRouterClientID reports whether id can name a client on an Ethereum\n// ICS26 router: one it generated (\"client-\" and a decimal number) or a valid\n// custom id. Such ids are not SDK client ids ({type}-{N}): \"gnoland\" is one.\nfunc IsValidRouterClientID(id string) bool {\n\tif strings.HasPrefix(id, RouterClientIDPrefix) {\n\t\tn := id[len(RouterClientIDPrefix):]\n\t\tif n == \"\" || len(n) \u003e 20 || (len(n) \u003e 1 \u0026\u0026 n[0] == '0') {\n\t\t\treturn false\n\t\t}\n\t\t_, err := strconv.ParseUint(n, 10, 64)\n\t\treturn err == nil\n\t}\n\treturn IsValidCustomClientID(id)\n}\n"},{"name":"commitment.gno","body":"package types\n\nimport (\n\t\"crypto/sha256\"\n\n\t\"gno.land/p/aib/encoding/v0\"\n)\n\n// CommitPacket returns the V2 packet commitment bytes. The commitment consists of:\n// ha256_hash(0x02 + sha256_hash(destinationClient) + sha256_hash(timeout) + sha256_hash(payload)) from a given packet.\n// This results in a fixed length preimage of 32 bytes.\n// NOTE: A fixed length preimage is ESSENTIAL to prevent relayers from being able\n// to malleate the packet fields and create a commitment hash that matches the original packet.\nfunc CommitPacket(packet Packet) []byte {\n\tvar buf []byte\n\tdestIDHash := sha256.Sum256([]byte(packet.DestinationClient))\n\tbuf = append(buf, destIDHash[:]...)\n\n\ttimeoutBytes := encoding.Uint64ToBigEndian(packet.TimeoutTimestamp)\n\ttimeoutHash := sha256.Sum256(timeoutBytes)\n\tbuf = append(buf, timeoutHash[:]...)\n\n\tvar appBytes []byte\n\tfor _, payload := range packet.Payloads {\n\t\tappBytes = append(appBytes, hashPayload(payload)...)\n\t}\n\tappHash := sha256.Sum256(appBytes)\n\tbuf = append(buf, appHash[:]...)\n\n\tbuf = append([]byte{byte(2)}, buf...)\n\n\thash := sha256.Sum256(buf)\n\treturn hash[:]\n}\n\n// CommitPacketReceipt returns the value the IBC core stores at a packet's\n// receipt path (ICS24 receipt key: destination client ‖ 0x02 ‖ sequence) when\n// it receives the packet. ibc-go stores the constant 0x02 there, which says\n// that some packet was received at the sequence but not which; this value\n// identifies the packet, so a reconciliation can tell which packet occupies a\n// sequence (ADR pr6375 R19, §7.12), as eureka's\n// keccak256(abi.encode(packet)) does on Ethereum. The encoding, fixed-length\n// so no field can be malleated into another:\n//\n//\tsha256(0x03 ‖ uint64be(sequence) ‖ sha256(sourceClient) ‖ CommitPacket(packet))\n//\n// CommitPacket covers the destination client, the timeout and every payload\n// (ports, version, encoding and value); the sequence and the source client\n// complete it. Counterparties only ever prove the absence of a receipt (for a\n// timeout), which this value does not affect.\nfunc CommitPacketReceipt(packet Packet) []byte {\n\tbuf := []byte{byte(3)}\n\tbuf = append(buf, encoding.Uint64ToBigEndian(packet.Sequence)...)\n\tsrcHash := sha256.Sum256([]byte(packet.SourceClient))\n\tbuf = append(buf, srcHash[:]...)\n\tbuf = append(buf, CommitPacket(packet)...)\n\thash := sha256.Sum256(buf)\n\treturn hash[:]\n}\n\n// CommitAcknowledgement returns the V2 acknowledgement commitment bytes.\n// The commitment consists of:\n// sha256_hash(0x02 + sha256_hash(ack1) + sha256_hash(ack2) + ...) from a given\n// acknowledgement.\nfunc CommitAcknowledgement(acknowledgement Acknowledgement) []byte {\n\tvar buf []byte\n\tfor _, ack := range acknowledgement.AppAcknowledgements {\n\t\thash := sha256.Sum256(ack)\n\t\tbuf = append(buf, hash[:]...)\n\t}\n\n\tbuf = append([]byte{byte(2)}, buf...)\n\n\thash := sha256.Sum256(buf)\n\treturn hash[:]\n}\n\n// hashPayload returns the hash of the payload.\nfunc hashPayload(data Payload) []byte {\n\tvar buf []byte\n\tsourceHash := sha256.Sum256([]byte(data.SourcePort))\n\tbuf = append(buf, sourceHash[:]...)\n\tdestHash := sha256.Sum256([]byte(data.DestinationPort))\n\tbuf = append(buf, destHash[:]...)\n\tpayloadVersionHash := sha256.Sum256([]byte(data.Version))\n\tbuf = append(buf, payloadVersionHash[:]...)\n\tpayloadEncodingHash := sha256.Sum256([]byte(data.Encoding))\n\tbuf = append(buf, payloadEncodingHash[:]...)\n\tpayloadValueHash := sha256.Sum256(data.Value)\n\tbuf = append(buf, payloadValueHash[:]...)\n\thash := sha256.Sum256(buf)\n\treturn hash[:]\n}\n"},{"name":"events.gno","body":"package types\n\n// IBC client events\nconst (\n\tEventTypeCreateClient               = \"create_client\"\n\tEventTypeUpdateClient               = \"update_client\"\n\tEventTypeUpgradeClient              = \"upgrade_client\"\n\tEventTypeSubmitMisbehaviour         = \"client_misbehaviour\"\n\tEventTypeRecoverClient              = \"recover_client\"\n\tEventTypeScheduleIBCSoftwareUpgrade = \"schedule_ibc_software_upgrade\"\n\tEventTypeUpgradeChain               = \"upgrade_chain\"\n\n\tAttributeKeyClientID           = \"client_id\"\n\tAttributeKeySubjectClientID    = \"subject_client_id\"\n\tAttributeKeySubstituteClientID = \"substitute_client_id\"\n\tAttributeKeyClientType         = \"client_type\"\n\tAttributeKeyConsensusHeights   = \"consensus_heights\"\n\tAttributeKeyConsensusHeight    = \"consensus_height\"\n\tAttributeKeyUpgradeStore       = \"upgrade_store\"\n\tAttributeKeyUpgradePlanHeight  = \"upgrade_plan_height\"\n\tAttributeKeyUpgradePlanTitle   = \"title\"\n)\n\n// IBC core events\nconst (\n\tEventTypeSendPacket        = \"send_packet\"\n\tEventTypeRecvPacket        = \"recv_packet\"\n\tEventTypeTimeoutPacket     = \"timeout_packet\"\n\tEventTypeAcknowledgePacket = \"acknowledge_packet\"\n\tEventTypeWriteAck          = \"write_acknowledgement\"\n\n\tAttributeKeySrcClient        = \"packet_source_client\"\n\tAttributeKeyDstClient        = \"packet_dest_client\"\n\tAttributeKeySequence         = \"packet_sequence\"\n\tAttributeKeyTimeoutTimestamp = \"packet_timeout_timestamp\"\n\tAttributeKeyEncodedPacketHex = \"encoded_packet_hex\"\n\tAttributeKeyEncodedAckHex    = \"encoded_acknowledgement_hex\"\n)\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/aib/ibc/types/v0\"\ngno = \"0.9\"\n"},{"name":"height.gno","body":"package types\n\nimport (\n\t\"chain/runtime\"\n\t\"encoding/binary\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/aib/encoding/proto/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Height is a monotonically increasing data type\n// that can be compared against another Height for the purposes of updating and\n// freezing clients\n//\n// Normally the RevisionHeight is incremented at each height while keeping\n// RevisionNumber the same. However some consensus algorithms may choose to\n// reset the height in certain conditions e.g. hard forks, state-machine\n// breaking changes In these cases, the RevisionNumber is incremented so that\n// height continues to be monitonically increasing even as the RevisionHeight\n// gets reset\n//\n// Please note that json tags for generated Go code are overridden to explicitly exclude the omitempty jsontag.\n// This enforces the Go json marshaller to always emit zero values for both revision_number and revision_height.\ntype Height struct {\n\t// the revision that the client is currently on\n\tRevisionNumber uint64\n\t// the height within the given revision\n\tRevisionHeight uint64\n}\n\nfunc NewHeight(revision, height uint64) Height {\n\treturn Height{\n\t\tRevisionNumber: revision,\n\t\tRevisionHeight: height,\n\t}\n}\n\n// ParseHeight is a utility function that takes a string representation of the\n// height and returns a Height struct.\nfunc ParseHeight(heightStr string) (Height, error) {\n\tsplitStr := strings.Split(heightStr, \"/\")\n\tif len(splitStr) != 2 {\n\t\treturn Height{}, ufmt.Errorf(\"expected height string format: {revision}/{height}. Got: %s\", heightStr)\n\t}\n\trevisionNumber, err := strconv.ParseUint(splitStr[0], 10, 64)\n\tif err != nil {\n\t\treturn Height{}, ufmt.Errorf(\"invalid revision number. parse err: %s\", err)\n\t}\n\trevisionHeight, err := strconv.ParseUint(splitStr[1], 10, 64)\n\tif err != nil {\n\t\treturn Height{}, ufmt.Errorf(\"invalid revision height. parse err: %s\", err)\n\t}\n\treturn NewHeight(revisionNumber, revisionHeight), nil\n}\n\nfunc ZeroHeight() Height { return Height{} }\n\nfunc (h Height) String() string {\n\treturn ufmt.Sprintf(\"%d/%d\", h.RevisionNumber, h.RevisionHeight)\n}\n\n// StringNatSort returns a natural sortable representation of height that can\n// be used for store keys.\nfunc (h Height) StringNatSort() string {\n\tbz := make([]byte, 16)\n\tbinary.BigEndian.PutUint64(bz, h.RevisionNumber)\n\tbinary.BigEndian.PutUint64(bz[8:], h.RevisionHeight)\n\treturn string(bz)\n}\n\n// ParseHeightNatSort is a utility function that takes a string nat sorted\n// representation of the height (see StringBatSort) and returns a Height\n// struct.\nfunc ParseHeightNatSort(heightNatSort string) (h Height) {\n\tbz := []byte(heightNatSort)\n\th.RevisionNumber = binary.BigEndian.Uint64(bz)\n\th.RevisionHeight = binary.BigEndian.Uint64(bz[8:])\n\treturn\n}\n\n// GTE Helper comparison function returns true if h \u003e= other\nfunc (h Height) GTE(other Height) bool {\n\tcmp := h.Compare(other)\n\treturn cmp \u003e= 0\n}\n\n// GT Helper comparison function returns true if h \u003e other\nfunc (h Height) GT(other Height) bool {\n\treturn h.Compare(other) == 1\n}\n\n// LTE Helper comparison function returns true if h \u003c= other\nfunc (h Height) LTE(other Height) bool {\n\tcmp := h.Compare(other)\n\treturn cmp \u003c= 0\n}\n\n// LT Helper comparison function returns true if h \u003c other\nfunc (h Height) LT(other Height) bool {\n\treturn h.Compare(other) == -1\n}\n\n// EQ Helper comparison function returns true if h == other\nfunc (h Height) EQ(other Height) bool {\n\treturn h.Compare(other) == 0\n}\n\n// IsZero returns true if height revision and revision-height are both 0\nfunc (h Height) IsZero() bool {\n\treturn h.RevisionNumber == 0 \u0026\u0026 h.RevisionHeight == 0\n}\n\n// Compare implements a method to compare two heights. When comparing two heights a, b\n// we can call a.Compare(b) which will return\n// -1 if a \u003c b\n// 0  if a = b\n// 1  if a \u003e b\n//\n// It first compares based on revision numbers, whichever has the higher revision number is the higher height\n// If revision number is the same, then the revision height is compared\nfunc (h Height) Compare(height Height) int64 {\n\tif h.RevisionNumber \u003c height.RevisionNumber {\n\t\treturn -1\n\t}\n\tif h.RevisionNumber \u003e height.RevisionNumber {\n\t\treturn 1\n\t}\n\tif h.RevisionHeight \u003c height.RevisionHeight {\n\t\treturn -1\n\t}\n\tif h.RevisionHeight \u003e height.RevisionHeight {\n\t\treturn 1\n\t}\n\treturn 0\n}\n\n// GetSelfHeight is a utility function that returns self height given context.\n// Revision number is retrieved from runtime.ChainID()\nfunc GetSelfHeight() Height {\n\trevision := ParseChainID(runtime.ChainID())\n\treturn NewHeight(revision, uint64(runtime.ChainHeight()))\n}\n\n// IsRevisionFormat checks if a chainID is in the format required for parsing\n// revisions\n// The chainID must be in the form: `{chainID}-{revision}`.\n// 24-host may enforce stricter checks on chainID\nvar IsRevisionFormat = regexp.MustCompile(`^.*[^\\n-]-{1}[1-9][0-9]*$`).MatchString\n\n// ParseChainID is a utility function that returns a revision number from the\n// given ChainID.\n// ParseChainID attempts to parse a chain id in the format:\n// `{chainID}-{revision}` and return the revisionnumber as a uint64.\n// If the chainID is not in the expected format, a default revision value of 0\n// is returned.\nfunc ParseChainID(chainID string) uint64 {\n\tif !IsRevisionFormat(chainID) {\n\t\t// chainID is not in revision format, return 0 as default\n\t\treturn 0\n\t}\n\tsplitStr := strings.Split(chainID, \"-\")\n\trevision, err := strconv.ParseUint(splitStr[len(splitStr)-1], 10, 64)\n\t// sanity check: error should always be nil since regex only allows numbers in last element\n\tif err != nil {\n\t\tpanic(ufmt.Errorf(\"regex allowed non-number value as last split element for chainID: %s\", chainID))\n\t}\n\treturn revision\n}\n\n// SetRevisionNumber takes a chainID in valid revision format and swaps the\n// revision number in the chainID with the given revision number.\nfunc SetRevisionNumber(chainID string, revision uint64) (string, error) {\n\tif !IsRevisionFormat(chainID) {\n\t\treturn \"\", ufmt.Errorf(\"chainID is not in revision format: %s\", chainID)\n\t}\n\n\tsplitStr := strings.Split(chainID, \"-\")\n\t// swap out revision number with given revision\n\tsplitStr[len(splitStr)-1] = strconv.FormatUint(revision, 10)\n\treturn strings.Join(splitStr, \"-\"), nil\n}\n\nfunc (h Height) ProtoMarshal() (bz []byte) {\n\tbz = proto.AppendVarint(bz, 1, h.RevisionNumber)\n\tbz = proto.AppendVarint(bz, 2, h.RevisionHeight)\n\treturn\n}\n"},{"name":"merkle.gno","body":"package types\n\nimport \"strings\"\n\n// MerklePath is the path used to verify commitment proofs, which can be an\n// arbitrary structured object (defined by a commitment type).\n// ICS-23 verification supports membership proofs for nested merkle trees.\n// The ICS-24 standard provable keys MUST be stored in the lowest level tree\n// with an optional prefix.\n// The IC24 provable tree may then be stored in a higher level tree(s) that\n// hash up to the root hash stored in the consensus state of the client.\n// Each element of the path represents the key of a merkle tree from the root\n// to the leaf.\n// The elements of the path before the final element must be the path to the\n// tree that contains the ICS24 provable store. Thus, it should remain constant\n// for all ICS24 proofs.\n// The final element of the path is the key of the leaf in the ICS24 provable\n// store, thus IBC core will append the ICS24 path to the final element of the\n// MerklePath stored in the counterparty to create the full path to the leaf\n// for proof verification.\n//\n// Examples:\n// Cosmos SDK:\n// The Cosmos SDK commits to a multi-tree where each store is an IAVL tree and\n// all store hashes are hashed in a simple merkle tree to get the final root\n// hash. Thus, the MerklePath in the counterparty MerklePrefix has the\n// following structure: [\"ibc\", \"\"]\n// The core IBC handler will append the ICS24 path to the final element of the\n// MerklePath like so: [\"ibc\", \"{packetCommitmentPath}\"] which will then be\n// used for final verification.\ntype MerklePath struct {\n\tKeyPath [][]byte\n}\n\nfunc (mp MerklePath) String() string {\n\tvar strs []string\n\tfor _, p := range mp.KeyPath {\n\t\tstrs = append(strs, string(p))\n\t}\n\treturn \"[\" + strings.Join(strs, \", \") + \"]\"\n}\n\n// BuildMerklePath takes the merkle path prefix and an ICS24 path and builds a\n// new path by appending the ICS24 path to the last element of the merkle path\n// prefix.\nfunc BuildMerklePath(prefix [][]byte, path []byte) MerklePath {\n\tprefixLength := len(prefix)\n\tif prefixLength == 0 {\n\t\tpanic(\"cannot build merkle path with empty prefix\")\n\t}\n\n\t// copy prefix to avoid modifying the original slice\n\tfullPath := make([][]byte, len(prefix))\n\tfor i, p := range prefix {\n\t\tfullPath[i] = make([]byte, len(p))\n\t\tcopy(fullPath[i], p)\n\t}\n\t// append path to last element\n\tfullPath[prefixLength-1] = append(fullPath[prefixLength-1], path...)\n\treturn MerklePath{KeyPath: fullPath}\n}\n"},{"name":"msgs.gno","body":"package types\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"errors\"\n\n\t\"gno.land/p/aib/ibc/host/v0\"\n\t\"gno.land/p/aib/ics23/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/onbloc/json/v0\"\n)\n\n// MsgSendPacket sends an outgoing IBC packet.\ntype MsgSendPacket struct {\n\tSourceClient string\n\t// TimeoutTimestamp holds the seconds after which the packet times out.\n\tTimeoutTimestamp uint64\n\tPayloads         []Payload\n\t// Ext carries data a later core implementation may need on this\n\t// entry point. The proxy signatures are frozen, so this is the only\n\t// place a new message field can be added. Unused by impl/v0.\n\tExt any\n}\n\n// NewMsgSendPacket creates a new MsgSendPacket instance.\nfunc NewMsgSendPacket(sourceClient string, timeoutTimestamp uint64, payloads ...Payload) *MsgSendPacket {\n\treturn \u0026MsgSendPacket{\n\t\tSourceClient:     sourceClient,\n\t\tTimeoutTimestamp: timeoutTimestamp,\n\t\tPayloads:         payloads,\n\t}\n}\n\n// ValidateBasic performs basic checks on a MsgSendPacket.\nfunc (msg *MsgSendPacket) ValidateBasic() error {\n\tif err := host.ClientIdentifierValidator(msg.SourceClient); err != nil {\n\t\treturn err\n\t}\n\n\tif msg.TimeoutTimestamp == 0 {\n\t\treturn ufmt.Errorf(\"timeout must not be 0\")\n\t}\n\n\t// See Packet.ValidateBasic: multi-payload packets are not supported.\n\tif len(msg.Payloads) != 1 {\n\t\treturn ufmt.Errorf(\"payloads must contain exactly one payload\")\n\t}\n\n\tfor i, pd := range msg.Payloads {\n\t\tif err := pd.ValidateBasic(); err != nil {\n\t\t\treturn ufmt.Errorf(\"invalid payload #%d: %v\", i, err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// MsgRecvPacket receives an incoming IBC packet.\ntype MsgRecvPacket struct {\n\tPacket          Packet\n\tProofCommitment []ics23.CommitmentProof\n\tProofHeight     Height\n\t// Ext carries data a later core implementation may need on this\n\t// entry point. The proxy signatures are frozen, so this is the only\n\t// place a new message field can be added. Unused by impl/v0.\n\tExt any\n}\n\n// ValidateBasic performs basic checks on a MsgRecvPacket.\nfunc (msg *MsgRecvPacket) ValidateBasic() error {\n\t// Before anything else touches a proof: no caller-supplied code runs.\n\tif err := ics23.CheckProofs(msg.ProofCommitment); err != nil {\n\t\treturn err\n\t}\n\tif len(msg.ProofCommitment) == 0 {\n\t\treturn ufmt.Errorf(\"proof commitment can not be empty\")\n\t}\n\treturn msg.Packet.ValidateBasic()\n}\n\n// MsgAcknowledgement receives incoming IBC acknowledgement.\ntype MsgAcknowledgement struct {\n\tPacket          Packet\n\tAcknowledgement Acknowledgement\n\tProofAcked      []ics23.CommitmentProof\n\tProofHeight     Height\n\t// Ext carries data a later core implementation may need on this\n\t// entry point. The proxy signatures are frozen, so this is the only\n\t// place a new message field can be added. Unused by impl/v0.\n\tExt any\n}\n\n// ValidateBasic performs basic checks on a MsgAcknowledgement.\nfunc (msg *MsgAcknowledgement) ValidateBasic() error {\n\t// Before anything else touches a proof: no caller-supplied code runs.\n\tif err := ics23.CheckProofs(msg.ProofAcked); err != nil {\n\t\treturn err\n\t}\n\tif len(msg.ProofAcked) == 0 {\n\t\treturn errors.New(\"cannot submit an empty acknowledgement proof\")\n\t}\n\tfor i, proof := range msg.ProofAcked {\n\t\tif proof.GetExist() == nil {\n\t\t\treturn ufmt.Errorf(\"invalid commitment proof #%d: proof.GetExists() is nil\", i)\n\t\t}\n\t}\n\n\tif err := msg.Acknowledgement.Validate(); err != nil {\n\t\treturn err\n\t}\n\n\treturn msg.Packet.ValidateBasic()\n}\n\nfunc UniversalErrorAcknowledgement() []byte {\n\tbz := sha256.Sum256([]byte(\"UNIVERSAL_ERROR_ACKNOWLEDGEMENT\"))\n\treturn bz[:]\n}\n\n// AppAcknowledgement is the recommended acknowledgement format to be used by\n// app-specific protocols.\ntype AppAcknowledgement struct {\n\tResponse AppAcknowledgementResponse\n}\n\ntype AppAcknowledgementResponse struct {\n\tResult []byte\n\tError  string\n}\n\nfunc NewErrorAppAcknowledgement(err error) AppAcknowledgement {\n\treturn AppAcknowledgement{Response: AppAcknowledgementResponse{Error: err.Error()}}\n}\n\nfunc NewResultAppAcknowledgement(result []byte) AppAcknowledgement {\n\treturn AppAcknowledgement{\n\t\tResponse: AppAcknowledgementResponse{\n\t\t\tResult: result,\n\t\t},\n\t}\n}\n\nfunc (a AppAcknowledgement) Success() bool {\n\treturn a.Response.Error == \"\"\n}\n\nfunc (a *AppAcknowledgement) MarshalJSON() []byte {\n\tvar node *json.Node\n\tif a.Response.Error != \"\" {\n\t\tnode = json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\t\"error\": json.StringNode(\"\", a.Response.Error),\n\t\t})\n\t} else {\n\t\tnode = json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\t\"result\": json.StringNode(\"\", base64.StdEncoding.EncodeToString(a.Response.Result)),\n\t\t})\n\t}\n\tbz, _ := json.Marshal(node)\n\treturn bz\n}\n\nfunc (a *AppAcknowledgement) UnmarshalJSON(bz []byte) error {\n\troot, err := json.Unmarshal(bz)\n\tif err != nil {\n\t\treturn err\n\t}\n\tresult, err := root.GetKey(\"result\")\n\tif err != nil {\n\t\trerror, err := root.GetKey(\"error\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ta.Response = AppAcknowledgementResponse{Error: rerror.MustString()}\n\t\treturn nil\n\t}\n\tresBz, err := base64.StdEncoding.DecodeString(result.MustString())\n\tif err != nil {\n\t\treturn err\n\t}\n\ta.Response = AppAcknowledgementResponse{Result: resBz}\n\treturn nil\n}\n\n// MsgTimeout receives timed-out packet\ntype MsgTimeout struct {\n\tPacket          Packet\n\tProofUnreceived []ics23.CommitmentProof\n\tProofHeight     Height\n\t// Ext carries data a later core implementation may need on this\n\t// entry point. The proxy signatures are frozen, so this is the only\n\t// place a new message field can be added. Unused by impl/v0.\n\tExt any\n}\n\nfunc (msg *MsgTimeout) ValidateBasic() error {\n\t// Before anything else touches a proof: no caller-supplied code runs.\n\tif err := ics23.CheckProofs(msg.ProofUnreceived); err != nil {\n\t\treturn err\n\t}\n\tif len(msg.ProofUnreceived) == 0 {\n\t\treturn errors.New(\"cannot submit an empty timeout proof\")\n\t}\n\treturn msg.Packet.ValidateBasic()\n}\n"},{"name":"packet.gno","body":"package types\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"strings\"\n\n\t\"gno.land/p/aib/encoding/proto/v0\"\n\t\"gno.land/p/aib/ibc/host/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// PacketStatus specifies the status of a RecvPacketResult.\ntype PacketStatus int32\n\nconst (\n\t// PACKET_STATUS_UNSPECIFIED indicates an unknown packet status.\n\tPacketStatus_NONE PacketStatus = 0\n\t// PACKET_STATUS_SUCCESS indicates a successful packet receipt.\n\tPacketStatus_Success PacketStatus = 1\n\t// PACKET_STATUS_FAILURE indicates a failed packet receipt.\n\tPacketStatus_Failure PacketStatus = 2\n\t// PACKET_STATUS_ASYNC indicates that the packet was received and its\n\t// acknowledgement will be written later by the application.\n\tPacketStatus_Async PacketStatus = 3\n)\n\n// Packet defines a type that carries data across different chains through IBC\ntype Packet struct {\n\t// number corresponds to the order of sends and receives, where a Packet\n\t// with an earlier sequence number must be sent and received before a Packet\n\t// with a later sequence number.\n\tSequence uint64\n\t// identifies the sending client on the sending chain.\n\tSourceClient string\n\t// identifies the receiving client on the receiving chain.\n\tDestinationClient string\n\t// timeout timestamp in seconds after which the packet times out.\n\tTimeoutTimestamp uint64\n\t// a list of payloads, each one for a specific application.\n\tPayloads []Payload\n}\n\n// NewPacket constructs a new packet.\nfunc NewPacket(sequence uint64, sourceClient, destinationClient string, timeoutTimestamp uint64, payloads ...Payload) Packet {\n\treturn Packet{\n\t\tSequence:          sequence,\n\t\tSourceClient:      sourceClient,\n\t\tDestinationClient: destinationClient,\n\t\tTimeoutTimestamp:  timeoutTimestamp,\n\t\tPayloads:          payloads,\n\t}\n}\n\nconst MaximumPayloadsSize = 262144 // 256 KiB. This is the maximum size of all payloads combined\n\n// ValidateBasic validates that a Packet satisfies the basic requirements.\nfunc (p Packet) ValidateBasic() error {\n\t// Multi-payload packets are not supported: RecvPacket invokes the app\n\t// callbacks in sequence and stops at the first failure without reverting\n\t// the ones that already succeeded, so a partially applied packet would be\n\t// acknowledged as failed and refunded by the counterparty. ibc-go enforces\n\t// the same restriction, so enabling multi-payload has to be a deliberate\n\t// change on both sides, with atomicity decided up front.\n\tif len(p.Payloads) != 1 {\n\t\treturn ufmt.Errorf(\"payloads must contain exactly one payload\")\n\t}\n\n\ttotalPayloadsSize := 0\n\tfor i, pd := range p.Payloads {\n\t\tif err := pd.ValidateBasic(); err != nil {\n\t\t\treturn ufmt.Errorf(\"invalid Payload #%d: %v\", i, err)\n\t\t}\n\t\ttotalPayloadsSize += len(pd.Value)\n\t}\n\n\tif totalPayloadsSize \u003e MaximumPayloadsSize {\n\t\treturn ufmt.Errorf(\"packet data bytes cannot exceed %d bytes\", MaximumPayloadsSize)\n\t}\n\n\tif err := host.ClientIdentifierValidator(p.SourceClient); err != nil {\n\t\treturn ufmt.Errorf(\"invalid source ID: %v\", err)\n\t}\n\tif err := host.ClientIdentifierValidator(p.DestinationClient); err != nil {\n\t\treturn ufmt.Errorf(\"invalid destination ID: %v\", err)\n\t}\n\n\tif p.Sequence == 0 {\n\t\treturn ufmt.Errorf(\"packet sequence cannot be 0\")\n\t}\n\tif p.TimeoutTimestamp == 0 {\n\t\treturn ufmt.Errorf(\"packet timeout timestamp cannot be 0\")\n\t}\n\n\treturn nil\n}\n\n// ValidateBasicRouter is ValidateBasic for a packet of an Ethereum client:\n// the side that is the ICS26 router's may also be a router client id\n// (\"client-N\", or a custom id of up to 128 characters). Every other packet\n// is checked by ValidateBasic, the SDK's rule.\nfunc (p Packet) ValidateBasicRouter() error {\n\t// Multi-payload packets are not supported: RecvPacket invokes the app\n\t// callbacks in sequence and stops at the first failure without reverting\n\t// the ones that already succeeded, so a partially applied packet would be\n\t// acknowledged as failed and refunded by the counterparty. ibc-go enforces\n\t// the same restriction, so enabling multi-payload has to be a deliberate\n\t// change on both sides, with atomicity decided up front.\n\tif len(p.Payloads) != 1 {\n\t\treturn ufmt.Errorf(\"payloads must contain exactly one payload\")\n\t}\n\n\ttotalPayloadsSize := 0\n\tfor i, pd := range p.Payloads {\n\t\tif err := pd.ValidateBasic(); err != nil {\n\t\t\treturn ufmt.Errorf(\"invalid Payload #%d: %v\", i, err)\n\t\t}\n\t\ttotalPayloadsSize += len(pd.Value)\n\t}\n\n\tif totalPayloadsSize \u003e MaximumPayloadsSize {\n\t\treturn ufmt.Errorf(\"packet data bytes cannot exceed %d bytes\", MaximumPayloadsSize)\n\t}\n\n\tif err := packetClientID(p.SourceClient); err != nil {\n\t\treturn ufmt.Errorf(\"invalid source ID: %v\", err)\n\t}\n\tif err := packetClientID(p.DestinationClient); err != nil {\n\t\treturn ufmt.Errorf(\"invalid destination ID: %v\", err)\n\t}\n\n\tif p.Sequence == 0 {\n\t\treturn ufmt.Errorf(\"packet sequence cannot be 0\")\n\t}\n\tif p.TimeoutTimestamp == 0 {\n\t\treturn ufmt.Errorf(\"packet timeout timestamp cannot be 0\")\n\t}\n\n\treturn nil\n}\n\n// ProtoMarshal returns the protobuf encoding of a Packet.\n//\n//\tmessage Packet {\n//\t  uint64 sequence = 1;\n//\t  string source_client = 2;\n//\t  string destination_client = 3;\n//\t  uint64 timeout_timestamp = 4;\n//\t  repeated Payload payloads = 5 [(gogoproto.nullable) = false];\n//\t}\nfunc (p Packet) ProtoMarshal() (buf []byte) {\n\t// Field 1: sequence (varint)\n\tbuf = proto.AppendVarint(buf, 1, uint64(p.Sequence))\n\n\t// Field 2: source_client (length-delimited)\n\tbuf = proto.AppendLengthDelimited(buf, 2, []byte(p.SourceClient))\n\n\t// Field 3: destination_client (length-delimited)\n\tbuf = proto.AppendLengthDelimited(buf, 3, []byte(p.DestinationClient))\n\n\t// Field 4: timeout_timestamp (varint)\n\tbuf = proto.AppendVarint(buf, 4, p.TimeoutTimestamp)\n\n\t// Field 5: payloads\n\tfor _, payload := range p.Payloads {\n\t\tbz := payload.ProtoMarshal()\n\t\tbuf = proto.AppendLengthDelimited(buf, 5, bz)\n\t}\n\treturn\n}\n\ntype Payload struct {\n\t// specifies the source port of the packet.\n\tSourcePort string\n\t// specifies the destination port of the packet.\n\tDestinationPort string\n\t// version of the specified application.\n\tVersion string\n\t// the encoding used for the provided value.\n\tEncoding string\n\t// the raw bytes for the payload.\n\tValue []byte\n}\n\n// NewPayload constructs a new Payload\nfunc NewPayload(sourcePort, destPort, version, encoding string, value []byte) Payload {\n\treturn Payload{\n\t\tSourcePort:      sourcePort,\n\t\tDestinationPort: destPort,\n\t\tVersion:         version,\n\t\tEncoding:        encoding,\n\t\tValue:           value,\n\t}\n}\n\n// ValidateBasic validates a Payload.\nfunc (p Payload) ValidateBasic() error {\n\tif err := host.PortIdentifierValidator(p.SourcePort); err != nil {\n\t\treturn ufmt.Errorf(\"invalid source port: %v\", err)\n\t}\n\tif err := host.PortIdentifierValidator(p.DestinationPort); err != nil {\n\t\treturn ufmt.Errorf(\"invalid destination port: %v\", err)\n\t}\n\tif strings.TrimSpace(p.Version) == \"\" {\n\t\treturn ufmt.Errorf(\"payload version cannot be empty\")\n\t}\n\tif strings.TrimSpace(p.Encoding) == \"\" {\n\t\treturn ufmt.Errorf(\"payload encoding cannot be empty\")\n\t}\n\tif len(p.Value) == 0 {\n\t\treturn ufmt.Errorf(\"payload value cannot be empty\")\n\t}\n\treturn nil\n}\n\n// ProtoMarshal returns the protobuf encoding of a Payload.\n//\n//\tmessage Payload {\n//\t  string source_port = 1;\n//\t  string destination_port = 2;\n//\t  string version = 3;\n//\t  string encoding = 4;\n//\t  bytes value = 5;\n//\t}\nfunc (p Payload) ProtoMarshal() []byte {\n\tvar buf []byte\n\n\t// Field 1: source_port (length-delimited)\n\tbuf = proto.AppendLengthDelimited(buf, 1, []byte(p.SourcePort))\n\n\t// Field 2: destination_port (length-delimited)\n\tbuf = proto.AppendLengthDelimited(buf, 2, []byte(p.DestinationPort))\n\n\t// Field 3: version (length-delimited)\n\tbuf = proto.AppendLengthDelimited(buf, 3, []byte(p.Version))\n\n\t// Field 4: encoding (length-delimited)\n\tbuf = proto.AppendLengthDelimited(buf, 4, []byte(p.Encoding))\n\n\t// Field 5: value (length-delimited)\n\tbuf = proto.AppendLengthDelimited(buf, 5, p.Value)\n\n\treturn buf\n}\n\n// RecvPacketResult speecifies the status of a packet as well as the acknowledgement bytes.\ntype RecvPacketResult struct {\n\t// status of the packet\n\tStatus PacketStatus\n\t// acknowledgement of the packet\n\tAcknowledgement []byte\n}\n\n// Acknowledgement contains a list of all ack results associated with a single packet.\n// In the case of a successful receive, the acknowledgement will contain an app acknowledgement\n// for each application that received a payload in the same order that the payloads were sent\n// in the packet.\n// If the receive is not successful, the acknowledgement will contain a single app acknowledgment\n// which will be a constant error acknowledgment as defined by the IBC v2 protocol.\ntype Acknowledgement struct {\n\tAppAcknowledgements [][]byte\n}\n\n// Validate performs a basic validation of the acknowledgement\nfunc (ack Acknowledgement) Validate() error {\n\t// acknowledgement list should be non-empty\n\tif len(ack.AppAcknowledgements) == 0 {\n\t\treturn errors.New(\"app acknowledgements must be non-empty\")\n\t}\n\n\tfor _, a := range ack.AppAcknowledgements {\n\t\t// Each app acknowledgement should be non-empty\n\t\tif len(a) == 0 {\n\t\t\treturn errors.New(\"app acknowledgement cannot be empty\")\n\t\t}\n\n\t\t// Ensure that the app acknowledgement contains ErrorAcknowledgement\n\t\t// **if and only if** the app acknowledgement list has a single element\n\t\tif len(ack.AppAcknowledgements) \u003e 1 {\n\t\t\tif bytes.Equal(a, UniversalErrorAcknowledgement()) {\n\t\t\t\treturn errors.New(\"cannot have the error acknowledgement in multi acknowledgement list\")\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Success returns true if the acknowledgement is successful\n// it implements the exported.Acknowledgement interface\nfunc (ack Acknowledgement) Success() bool {\n\treturn !bytes.Equal(ack.AppAcknowledgements[0], UniversalErrorAcknowledgement())\n}\n\n// ProtoMarshal returns the protobuf encoding of a Acknowledgement.\n//\n//\tmessage Acknowledgement {\n//\t  repeated bytes app_acknowledgements = 1;\n//\t}\nfunc (ack Acknowledgement) ProtoMarshal() (buf []byte) {\n\tfor _, appAck := range ack.AppAcknowledgements {\n\t\tbuf = append(buf, proto.AppendLengthDelimited(nil, 1, appAck)...)\n\t}\n\treturn\n}\n\n// packetClientID checks a client id a packet of an Ethereum client names: an\n// ICS24 identifier (4 to 64 characters), as every local client id is, or an\n// Ethereum ICS26 router's client id (IsValidRouterClientID). The core matches\n// the side that is not local against the counterparty registered for the\n// local client, which was checked by that client's own rule.\nfunc packetClientID(id string) error {\n\tif IsValidRouterClientID(id) {\n\t\treturn nil\n\t}\n\treturn host.ClientIdentifierValidator(id)\n}\n"},{"name":"result.gno","body":"package types\n\n// ResponseResultType defines the possible outcomes of the execution of a message\ntype ResponseResultType int32\n\nconst (\n\t// Default zero value enumeration\n\tRESPONSE_UNSPECIFIED ResponseResultType = 0\n\t// The message did not call the IBC application callbacks (because, for\n\t// example, the packet had already been relayed)\n\tRESPONSE_NOOP ResponseResultType = 1\n\t// The message was executed successfully\n\tRESPONSE_SUCCESS ResponseResultType = 2\n\t// The message was executed unsuccessfully\n\tRESPONSE_FAILURE ResponseResultType = 3\n)\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"WJCT54On/DpBbjUBGC35ZRf4vdH3JxIhjmg2xRw5NEUW0sE0KtBJOcKQFV7UNRdpUXJKye1UMmPYC1tIldxR4g=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"app","path":"gno.land/p/aib/ibc/app/v0","files":[{"name":"app.gno","body":"package app\n\nimport (\n\t\"gno.land/p/aib/ibc/types/v0\"\n)\n\n// IBCApp defines an interface that implements all the callbacks that apps must\n// define as specified in IBC Protocol V2.\n//\n// CONTRACT: the callbacks are exported methods, and any realm can obtain a\n// usable value of an implementing type (a zero-value declaration needs no\n// composite literal), so an implementation must verify that it is being invoked\n// by the IBC core realm, typically with\n// `cur.Previous().PkgPath() == \"gno.land/r/aib/ibc/core\"`, before touching\n// funds or state. Core crosses into the app, so that is what Previous() reports\n// on every legitimate call.\ntype IBCApp interface {\n\t// OnSendPacket is executed when a packet is being sent from sending chain.\n\t// This callback is provided with the source and destination IDs, the signer,\n\t// the packet sequence and the packet data for this specific application.\n\tOnSendPacket(\n\t\tcur realm,\n\t\tsourceClient string,\n\t\tdestinationClient string,\n\t\tsequence uint64,\n\t\tpayload types.Payload,\n\t) error\n\n\t// OnRecvPacket is executed when a packet is received from receiving chain.\n\t//\n\t// CONTRACT: unlike other functions of this interface, OnRecvPacket does not\n\t// return an error, this is because in case of error (i.e. when\n\t// RecvPacketResult.Status == Failure), the underlying transaction is not\n\t// aborted. So implementer of this interface must keep in mind that any\n\t// writes to the store and all emitted events in this function aren't not\n\t// canceled if it returns RecvPacketResult.Status == Failure.\n\tOnRecvPacket(\n\t\tcur realm,\n\t\tsourceClient string,\n\t\tdestinationClient string,\n\t\tsequence uint64,\n\t\tpayload types.Payload,\n\t) types.RecvPacketResult\n\n\t// OnTimeoutPacket is executed when a packet has timed out on the receiving\n\t// chain.\n\tOnTimeoutPacket(\n\t\tcur realm,\n\t\tsourceClient string,\n\t\tdestinationClient string,\n\t\tsequence uint64,\n\t\tpayload types.Payload,\n\t) error\n\n\t// OnAcknowledgementPacket is executed when a packet gets acknowledged.\n\tOnAcknowledgementPacket(\n\t\tcur realm,\n\t\tsourceClient string,\n\t\tdestinationClient string,\n\t\tsequence uint64,\n\t\tacknowledgement []byte,\n\t\tpayload types.Payload,\n\t) error\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/aib/ibc/app/v0\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"aimiG5PPnGFc0gEMHxmAlpinVG3pyK1s/bTyVplC2fEL+LyXo4OL/X6OiybQy9aQH2yGer5Q5mRVd5QVZWLsYA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"ics20","path":"gno.land/p/aib/ibc/ics20/v0","files":[{"name":"denom.gno","body":"// Package ics20 holds the ICS-20 fungible token transfer types shared by the\n// transfer realm and its implementations: denominations with their trace,\n// the packet data and its protobuf codec.\npackage ics20\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/aib/ibc/host/v0\"\n\t\"gno.land/p/aib/ibc/types/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/onbloc/json/v0\"\n)\n\n// Denom holds the base denom of a Token and a trace of the chains it was sent\n// through.\n// denomPrefix is the prefix of IBC voucher denominations (\"ibc/\u003chash\u003e\").\nconst denomPrefix = \"ibc\"\n\ntype Denom struct {\n\t// the base token denomination\n\tBase string\n\t// the trace of the token\n\tTrace []Hop\n}\n\n// NewDenom creates a new Denom instance given the base denomination and a\n// variable number of hops.\nfunc NewDenom(base string, trace ...Hop) Denom {\n\treturn Denom{\n\t\tBase:  base,\n\t\tTrace: trace,\n\t}\n}\n\n// ValidateBasic performs a basic validation of the Denom fields.\nfunc (d Denom) ValidateBasic() error {\n\t// NOTE: base denom validation cannot be performed as each chain may define\n\t// its own base denom validation\n\tif strings.TrimSpace(d.Base) == \"\" {\n\t\treturn ufmt.Errorf(\"base denomination cannot be blank\")\n\t}\n\n\tfor _, hop := range d.Trace {\n\t\tif err := hop.ValidateBasic(); err != nil {\n\t\t\treturn ufmt.Errorf(\"invalid trace\")\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// IsNative returns true if the denomination is native, thus containing no\n// trace history.\nfunc (d Denom) IsNative() bool {\n\treturn len(d.Trace) == 0\n}\n\n// Path returns the full denomination according to the ICS20 specification:\n// trace + \"/\" + baseDenom\n// If there exists no trace then the base denomination is returned.\nfunc (d Denom) Path() string {\n\tif d.IsNative() {\n\t\treturn d.Base\n\t}\n\n\tvar sb strings.Builder\n\tfor _, t := range d.Trace {\n\t\tsb.WriteString(t.String())\n\t\tsb.WriteByte('/')\n\t}\n\tsb.WriteString(d.Base)\n\treturn sb.String()\n}\n\n// HasPrefix returns true if the first element of the trace of the denom\n// matches the provided portId and clientId.\nfunc (d Denom) HasPrefix(portID, clientID string) bool {\n\t// if the denom is native, then it is not prefixed by any port/channel pair\n\tif d.IsNative() {\n\t\treturn false\n\t}\n\treturn d.Trace[0].PortId == portID \u0026\u0026 d.Trace[0].ClientId == clientID\n}\n\n// Hash returns the hex bytes of the SHA256 hash of the Denom fields using the\n// following formula:\n//\n// hash = sha256(trace + \"/\" + baseDenom)\nfunc (d Denom) Hash() []byte {\n\thash := sha256.Sum256([]byte(d.Path()))\n\treturn hash[:]\n}\n\nfunc (d Denom) HashHex() string {\n\treturn strings.ToUpper(hex.EncodeToString(d.Hash()))\n}\n\n// IBCDenom a coin denomination for an ICS20 fungible token in the format\n// 'ibc/{hash(trace + baseDenom)}'. If the trace is empty, it will return the\n// base denomination.\nfunc (d Denom) IBCDenom() string {\n\tif d.IsNative() {\n\t\treturn d.Base\n\t}\n\treturn ufmt.Sprintf(\"%s/%s\", denomPrefix, d.HashHex())\n}\n\n// Implements jsonpage.JSONRenderer\nfunc (d Denom) RenderJSON() *json.Node {\n\treturn json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\"base\":  json.StringNode(\"\", d.Base),\n\t\t\"path\":  json.StringNode(\"\", d.Path()),\n\t\t\"denom\": json.StringNode(\"\", d.IBCDenom()),\n\t})\n}\n\nfunc (d Denom) MarshalJSON() []byte {\n\treturn []byte(d.RenderJSON().String())\n}\n\n// ExtractDenomFromPath returns the denom from the full path, its hops'\n// client ids in the SDK's {type}-{N} format.\nfunc ExtractDenomFromPath(fullPath string) Denom {\n\treturn ExtractDenomFromPathFunc(fullPath, types.IsValidClientID)\n}\n\n// IsHopClientID reports whether id can be a hop's client id: an SDK client\n// id, or an Ethereum ICS26 router's (\"client-N\", or a custom id of up to 128\n// characters such as \"gnoland\").\nfunc IsHopClientID(id string) bool {\n\treturn types.IsValidClientID(id) || types.IsValidRouterClientID(id)\n}\n\n// ExtractDenomFromPathFunc is ExtractDenomFromPath with the client ids of the\n// hops recognized by isClientID: on an Ethereum route, IsHopClientID, so a\n// gno-native token returning as \"transfer/gnoland/ugnot\" has its hop.\nfunc ExtractDenomFromPathFunc(fullPath string, isClientID func(string) bool) Denom {\n\tdenomSplit := strings.Split(fullPath, \"/\")\n\n\tif denomSplit[0] == fullPath {\n\t\treturn Denom{\n\t\t\tBase: fullPath,\n\t\t}\n\t}\n\n\tvar (\n\t\ttrace          []Hop\n\t\tbaseDenomSlice []string\n\t)\n\n\tlength := len(denomSplit)\n\tfor i := 0; i \u003c length; i += 2 {\n\t\t// The IBC specification does not guarantee the expected format of the\n\t\t// destination port or destination channel identifier. A short term\n\t\t// solution to determine base denomination is to expect the channel\n\t\t// identifier to be the one ibc-go specifies. A longer term solution is to\n\t\t// separate the path and base denomination in the ICS20 packet. If an\n\t\t// intermediate hop prefixes the full denom with a channel identifier\n\t\t// format different from our own, the base denomination will be incorrectly\n\t\t// parsed, but the token will continue to be treated correctly as an IBC\n\t\t// denomination. The hash used to store the token internally on our chain\n\t\t// will be the same value as the base denomination being correctly parsed.\n\t\tif i \u003c length-1 \u0026\u0026 length \u003e 2 \u0026\u0026 isClientID(denomSplit[i+1]) {\n\t\t\ttrace = append(trace, NewHop(denomSplit[i], denomSplit[i+1]))\n\t\t} else {\n\t\t\tbaseDenomSlice = denomSplit[i:]\n\t\t\tbreak\n\t\t}\n\t}\n\n\tbase := strings.Join(baseDenomSlice, \"/\")\n\n\treturn Denom{\n\t\tBase:  base,\n\t\tTrace: trace,\n\t}\n}\n\n// Hop defines a port ID, client ID pair specifying a unique \"hop\" in a trace\ntype Hop struct {\n\tPortId   string\n\tClientId string\n}\n\n// NewHop creates a Hop with the given port ID and client ID.\nfunc NewHop(portID, clientID string) Hop {\n\treturn Hop{portID, clientID}\n}\n\n// ValidateBasic performs a basic validation of the Hop fields.\nfunc (h Hop) ValidateBasic() error {\n\tif err := host.PortIdentifierValidator(h.PortId); err != nil {\n\t\treturn ufmt.Errorf(\"invalid hop source port ID %s\", h.PortId)\n\t}\n\tif err := host.ClientIdentifierValidator(h.ClientId); err != nil {\n\t\treturn ufmt.Errorf(\"invalid hop source client ID %s\", h.ClientId)\n\t}\n\n\treturn nil\n}\n\n// String returns the Hop in the format:\n// \u003cportID\u003e/\u003cclientID\u003e\nfunc (h Hop) String() string {\n\treturn ufmt.Sprintf(\"%s/%s\", h.PortId, h.ClientId)\n}\n\n// ValidateBasicEthereum validates the packet data of an Ethereum route, by\n// the route's own rules alone (the SDK's ValidateBasic stays every other\n// route's): the amount strictly positive, the sender set, the denom's hops\n// recognized and checked by the ICS26 router's client id grammar\n// (IsHopClientID; a hop's client id is an ICS24 identifier or a router\n// client id of up to 128 characters), and its base set. The receiver is not\n// checked: an Ethereum route books a receive whose receiver is no address,\n// blank included, as an owed sweep entry (ADR pr6375 §6.8, §7.1). It returns\n// the denom it parsed.\nfunc (f FungibleTokenPacketData) ValidateBasicEthereum() (Denom, error) {\n\tamount, err := strconv.ParseUint256(f.Amount, 10)\n\tif err != nil {\n\t\treturn Denom{}, ufmt.Errorf(\"unable to parse transfer amount %q: %v\", f.Amount, err)\n\t}\n\tif amount == 0 {\n\t\treturn Denom{}, ufmt.Errorf(\"amount must be strictly positive: got %s\", f.Amount)\n\t}\n\tif strings.TrimSpace(f.Sender) == \"\" {\n\t\treturn Denom{}, ufmt.Errorf(\"sender address cannot be blank\")\n\t}\n\td := ExtractDenomFromPathFunc(f.Denom, IsHopClientID)\n\tif strings.TrimSpace(d.Base) == \"\" {\n\t\treturn Denom{}, ufmt.Errorf(\"base denomination cannot be blank\")\n\t}\n\tfor _, h := range d.Trace {\n\t\tif host.PortIdentifierValidator(h.PortId) != nil {\n\t\t\treturn Denom{}, ufmt.Errorf(\"invalid hop source port ID %s\", h.PortId)\n\t\t}\n\t\tif host.ClientIdentifierValidator(h.ClientId) != nil \u0026\u0026 !types.IsValidRouterClientID(h.ClientId) {\n\t\t\treturn Denom{}, ufmt.Errorf(\"invalid hop source client ID %s\", h.ClientId)\n\t\t}\n\t}\n\treturn d, nil\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/aib/ibc/ics20/v0\"\ngno = \"0.9\"\n"},{"name":"packet.gno","body":"package ics20\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/aib/encoding/proto/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// FungibleTokenPacketData defines a struct for the packet payload\n// See FungibleTokenPacketData spec:\n// https://github.com/cosmos/ibc/tree/master/spec/app/ics-020-fungible-token-transfer#data-structures\ntype FungibleTokenPacketData struct {\n\t// the token denomination to be transferred\n\tDenom string\n\t// the token amount to be transferred\n\tAmount string\n\t// the sender address\n\tSender string\n\t// the recipient address on the destination chain\n\tReceiver string\n\t// optional memo\n\tMemo string\n}\n\n// NewFungibleTokenPacketData constructs a new FungibleTokenPacketData instance\nfunc NewFungibleTokenPacketData(\n\tdenom string, amount string,\n\tsender, receiver string,\n\tmemo string,\n) FungibleTokenPacketData {\n\treturn FungibleTokenPacketData{\n\t\tDenom:    denom,\n\t\tAmount:   amount,\n\t\tSender:   sender,\n\t\tReceiver: receiver,\n\t\tMemo:     memo,\n\t}\n}\n\n// ProtoMarshal returns the protobuf encoding of f.\n//\n//\tmessage FungibleTokenPacketData {\n//\t\tstring denom = 1;\n//\t\tstring amount = 2;\n//\t\tstring sender = 3;\n//\t\tstring receiver = 4;\n//\t\tstring memo = 5;\n//\t}\nfunc (f *FungibleTokenPacketData) ProtoMarshal() (buf []byte) {\n\t// Field 1: denom (length-delimited)\n\tbuf = proto.AppendLengthDelimited(buf, 1, []byte(f.Denom))\n\n\t// Field 2: amount (length-delimited)\n\tbuf = proto.AppendLengthDelimited(buf, 2, []byte(f.Amount))\n\n\t// Field 3: sender (length-delimited)\n\tbuf = proto.AppendLengthDelimited(buf, 3, []byte(f.Sender))\n\n\t// Field 4: receiver (length-delimited)\n\tbuf = proto.AppendLengthDelimited(buf, 4, []byte(f.Receiver))\n\n\t// Field 5: memo (length-delimited)\n\tbuf = proto.AppendLengthDelimited(buf, 5, []byte(f.Memo))\n\n\treturn\n}\n\n// ProtoUnmarshal reads the protobuf encoded bz into f.\n//\n//\tmessage FungibleTokenPacketData {\n//\t\tstring denom = 1;\n//\t\tstring amount = 2;\n//\t\tstring sender = 3;\n//\t\tstring receiver = 4;\n//\t\tstring memo = 5;\n//\t}\nfunc (f *FungibleTokenPacketData) ProtoUnmarshal(bz []byte) error {\n\tvar pos int\n\tfor pos \u003c len(bz) {\n\t\t// Read field tag (key)\n\t\tkey, newPos, err := proto.DecodeVarint(bz, pos)\n\t\tif err != nil {\n\t\t\treturn ufmt.Errorf(\"error reading field tag: %v\", err)\n\t\t}\n\t\tpos = newPos\n\n\t\tfieldNum := key \u003e\u003e 3\n\t\twireType := key \u0026 0x07\n\n\t\tif wireType != 2 { // All fields are strings (wire type 2)\n\t\t\treturn ufmt.Errorf(\"unexpected wire type %d for field %d\", wireType, fieldNum)\n\t\t}\n\n\t\t// Read string value\n\t\tvalue, newPos, err := proto.DecodeString(bz, pos)\n\t\tif err != nil {\n\t\t\treturn ufmt.Errorf(\"error reading string value: %v\", err)\n\t\t}\n\t\tpos = newPos\n\n\t\t// Assign to appropriate field\n\t\tswitch fieldNum {\n\t\tcase 1:\n\t\t\tf.Denom = value\n\t\tcase 2:\n\t\t\tf.Amount = value\n\t\tcase 3:\n\t\t\tf.Sender = value\n\t\tcase 4:\n\t\t\tf.Receiver = value\n\t\tcase 5:\n\t\t\tf.Memo = value\n\t\tdefault:\n\t\t\treturn ufmt.Errorf(\"unknown field aren't authorized in FungibleTokenPacketData\")\n\t\t}\n\t}\n\treturn nil\n}\n\n// ValidateBasic is used for validating the token transfer.\nfunc (f FungibleTokenPacketData) ValidateBasic() error {\n\tamount, err := strconv.ParseUint256(f.Amount, 10)\n\tif err != nil {\n\t\treturn ufmt.Errorf(\"unable to parse transfer amount %q: %v\", f.Amount, err)\n\t}\n\tif amount == 0 {\n\t\treturn ufmt.Errorf(\"amount must be strictly positive: got %s\", f.Amount)\n\t}\n\tif strings.TrimSpace(f.Sender) == \"\" {\n\t\treturn ufmt.Errorf(\"sender address cannot be blank\")\n\t}\n\tif strings.TrimSpace(f.Receiver) == \"\" {\n\t\treturn ufmt.Errorf(\"receiver address cannot be blank\")\n\t}\n\tdenom := ExtractDenomFromPath(f.Denom)\n\treturn denom.ValidateBasic()\n}\n"},{"name":"token.gno","body":"package ics20\n\nimport (\n\t\"chain\"\n\t\"math\"\n\t\"strconv\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Token defines a struct which represents a token to be transferred.\ntype Token struct {\n\t// the token denomination\n\tDenom Denom\n\t// the token amount to be transferred, in decimal digits\n\tAmount string\n}\n\n// NewToken constructs a Token with the given native base denom and amount\n// (no trace, no prefix hops). Provided so external callers — which cannot\n// construct foreign-stamped composite literals under v2 borrow rule #1 —\n// can still build Token values for tests and packet assembly.\nfunc NewToken(baseDenom, amount string) Token {\n\treturn Token{\n\t\tDenom:  NewDenom(baseDenom),\n\t\tAmount: amount,\n\t}\n}\n\n// AmountUint256 parses the Token.Amount field: decimal digits only (no sign,\n// no space, no separator; leading zeros are read as strconv.ParseUint reads\n// them), below 2^256, the range of an ICS20 amount on the wire.\nfunc (t Token) AmountUint256() (uint256, error) {\n\tamt, err := strconv.ParseUint256(t.Amount, 10)\n\tif err != nil {\n\t\treturn 0, ufmt.Errorf(\"unable to parse amount %q into uint256\", t.Amount)\n\t}\n\treturn amt, nil\n}\n\n// ToCoin converts a Token to a chain.Coin, whose amount is an int64: an\n// amount above math.MaxInt64 is refused, since no coin can hold it.\nfunc (t Token) ToCoin() (chain.Coin, error) {\n\tamt, err := t.AmountUint256()\n\tif err != nil {\n\t\treturn chain.Coin{}, err\n\t}\n\tif amt \u003e math.MaxInt64 {\n\t\treturn chain.Coin{}, ufmt.Errorf(\"amount %s is above the chain's coin maximum\", t.Amount)\n\t}\n\treturn chain.NewCoin(t.Denom.IBCDenom(), int64(amt)), nil\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"/GTFYFTduLl1RHruV9TnEgSc/p50DsTbd5XujYyCzR81exSF37gCfS29U4V1NZIgyPmaEw98T48kX8ciUeaYeg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"abi","path":"gno.land/p/aib/ibc/ics20/abi/v0","files":[{"name":"abi.gno","body":"// Package abi encodes ICS20 FungibleTokenPacketData the way the Solidity\n// ICS20 application of cosmos/solidity-ibc-eureka does: as\n// abi.encode(FungibleTokenPacketData{string denom; string sender;\n// string receiver; uint256 amount; string memo}), under the payload encoding\n// \"application/x-solidity-abi\".\npackage abi\n\nimport (\n\t\"errors\"\n\n\tabienc \"gno.land/p/aib/encoding/abi/v0\"\n\t\"gno.land/p/aib/ibc/ics20/v0\"\n)\n\n// Encoding is the ICS20 payload encoding of Solidity ABI packet data.\nconst Encoding = \"application/x-solidity-abi\"\n\nconst word = abienc.Word\n\n// MaxAmount is the largest amount Ethereum's transfer app takes on a send or\n// a receive, 2^96 - 1 (gno-eth-ibc GnoICS20Transfer: ICS20InvalidAmount above\n// it; ADR pr6375 E6, §7.2). A gno.land send above it would only come back\n// refused, and a receive above it can only be a forgery, so the transfer app\n// refuses both before any state change.\nconst MaxAmount uint256 = 1\u003c\u003c96 - 1\n\nvar errDecode = errors.New(\"abi: invalid FungibleTokenPacketData encoding\")\n\n// Marshal ABI-encodes data. The amount must be a decimal number below 2^256.\nfunc Marshal(data ics20.FungibleTokenPacketData) ([]byte, error) {\n\tamount, err := abienc.Uint256Decimal(data.Amount)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn abienc.Encode(abienc.Tuple(abienc.String(data.Denom), abienc.String(data.Sender), abienc.String(data.Receiver), amount, abienc.String(data.Memo))), nil\n}\n\n// Unmarshal decodes ABI-encoded packet data. Like Solidity's abi.decode it\n// accepts any in-range offsets, and it rejects amounts that do not fit the\n// decimal string ICS20 uses (none: every uint256 does).\nfunc Unmarshal(bz []byte) (ics20.FungibleTokenPacketData, error) {\n\tvar data ics20.FungibleTokenPacketData\n\ttupleOff, err := readUint(bz, 0)\n\tif err != nil {\n\t\treturn data, err\n\t}\n\ttuple, err := slice(bz, tupleOff, uint64(len(bz))-tupleOff)\n\tif err != nil {\n\t\treturn data, err\n\t}\n\tfields := make([]string, 4)\n\tfor i, headIdx := range []uint64{0, 1, 2, 4} {\n\t\toff, err := readUint(tuple, headIdx*word)\n\t\tif err != nil {\n\t\t\treturn data, err\n\t\t}\n\t\ts, err := readString(tuple, off)\n\t\tif err != nil {\n\t\t\treturn data, err\n\t\t}\n\t\tfields[i] = s\n\t}\n\tamountWord, err := slice(tuple, 3*word, word)\n\tif err != nil {\n\t\treturn data, err\n\t}\n\tdata.Denom = fields[0]\n\tdata.Sender = fields[1]\n\tdata.Receiver = fields[2]\n\tvar amount [word]byte\n\tcopy(amount[:], amountWord)\n\tdata.Amount = abienc.WordDecimal(amount)\n\tdata.Memo = fields[3]\n\treturn data, nil\n}\n\nfunc slice(b []byte, off, n uint64) ([]byte, error) {\n\tif off \u003e uint64(len(b)) || n \u003e uint64(len(b))-off {\n\t\treturn nil, errDecode\n\t}\n\treturn b[off : off+n], nil\n}\n\n// readUint reads the word at off as a uint64; larger values are invalid\n// offsets or lengths.\nfunc readUint(b []byte, off uint64) (uint64, error) {\n\tw, err := slice(b, off, word)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tfor _, c := range w[:word-8] {\n\t\tif c != 0 {\n\t\t\treturn 0, errDecode\n\t\t}\n\t}\n\tvar v uint64\n\tfor _, c := range w[word-8:] {\n\t\tv = v\u003c\u003c8 | uint64(c)\n\t}\n\treturn v, nil\n}\n\nfunc readString(tuple []byte, off uint64) (string, error) {\n\tn, err := readUint(tuple, off)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\ts, err := slice(tuple, off+word, n)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(s), nil\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/aib/ibc/ics20/abi/v0\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"VNl1tSEpC0uAr4sOaUzHQmC0mFbICKEwitCN25ACEPFwvrfLYmbh+oKXmAlhw/kLMUxHKhLN+DxwhhCX/MylVA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"controls","path":"gno.land/p/aib/ibc/ics20/controls/v0","files":[{"name":"arith.gno","body":"package controls\n\nimport (\n\t\"math\"\n\t\"math/overflow\"\n\t\"strconv\"\n)\n\n// Arithmetic of the controls. Every amount, total and meter level is a\n// uint256 (the range of an ICS20 amount on the wire, and of a grc20 v1\n// voucher), and every aggregate goes through one of these, by what reads it:\n//\n//   - checkedAdd for a liability or payment total a check reads: the\n//     operation that would pass 2^256 - 1 is refused before anything changes\n//     (a credit, a loss recorded by decision, a sweep payment);\n//   - mustAdd and mustSub where an earlier check bounds the result (a move\n//     between totals whose sum was checked, a payment within what is owed, a\n//     resolution bounded by its losses): an overflow or a negative result\n//     there is a broken invariant and aborts the transaction;\n//   - boundAdd for a sum only ever used as the upper bound of what is reserved\n//     or booked (Reserved, booked, a route's loss totals): saturating at the\n//     maximum can only make the check it feeds refuse more;\n//   - statAdd for a statistic no check reads (credited, paid, fees, sent,\n//     refunded...): it saturates, and the JSON shows the maximum;\n//   - sub0 for a reading that floors at zero (the free lock, a supply counted\n//     from before the controls).\n//\n// On an Ethereum route every packet carries at most 2^96 - 1 (abi.MaxAmount),\n// so no total of this package reaches 2^256 in any chain's lifetime; the\n// rules above only say what happens if one did. Counters that grow by one per\n// transaction (queue and refund sequences, authorization ids, record counts)\n// stay int64.\n\n// maxAmount is the largest amount: every saturating sum stops there.\nconst maxAmount uint256 = math.MaxUint256\n\n// checkedAdd is a + b, and whether it fits a uint256.\nfunc checkedAdd(a, b uint256) (uint256, bool) { return overflow.Addu256(a, b) }\n\n// mustAdd is a + b where a check bounded the sum.\nfunc mustAdd(a, b uint256) uint256 {\n\ts, ok := overflow.Addu256(a, b)\n\tif !ok {\n\t\tpanic(\"controls: an aggregate would overflow\")\n\t}\n\treturn s\n}\n\n// mustSub is a - b where an invariant keeps b within a.\nfunc mustSub(a, b uint256) uint256 {\n\tif b \u003e a {\n\t\tpanic(\"controls: a counter went negative\")\n\t}\n\treturn a - b\n}\n\n// sub0 is a - b, or 0 when b is more than a.\nfunc sub0(a, b uint256) uint256 {\n\tif b \u003e= a {\n\t\treturn 0\n\t}\n\treturn a - b\n}\n\n// boundAdd is a + b, saturating at the maximum.\nfunc boundAdd(a, b uint256) uint256 {\n\ts, ok := overflow.Addu256(a, b)\n\tif !ok {\n\t\treturn maxAmount\n\t}\n\treturn s\n}\n\n// statAdd is boundAdd for a statistic.\nfunc statAdd(a, b uint256) uint256 { return boundAdd(a, b) }\n\n// u is an amount in decimal digits (ufmt takes no uint256).\nfunc u(v uint256) string { return strconv.FormatUint256(v, 10) }\n\n// qu is an amount as a JSON string: amounts are quoted in every JSON this\n// package renders, so a 256-bit value survives a reader that parses numbers\n// as doubles.\nfunc qu(v uint256) string { return \"\\\"\" + u(v) + \"\\\"\" }\n"},{"name":"codec.gno","body":"package controls\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/aib/encoding/wire/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// The governance operations a transfer implementation applies on a decision\n// of gno.land/r/aib/ibc/ibcgov (through the transfer proxy's GovApply). Their\n// parameters travel as Params in the wire format.\nconst (\n\tOpEnroll       = \"enroll\"       // Route, To (fee collector)\n\tOpDenom        = \"denom\"        // Route, Denom, Config\n\tOpCollector    = \"collector\"    // Route, To\n\tOpRefuse       = \"refuse\"       // Route, Keys (Ethereum addresses)\n\tOpUnrefuse     = \"unrefuse\"     // Route, Keys (Ethereum addresses)\n\tOpVoid         = \"void\"         // Route, Keys, Forged, Incident\n\tOpRestore      = \"restore\"      // Route, Keys\n\tOpCredit       = \"credit\"       // Route, Key, Denom, To, Amount\n\tOpSweep        = \"sweep\"        // Route, Keys, To\n\tOpLoss         = \"loss\"         // Route, Key, Denom, Amount, Chain, Incident, Cause\n\tOpRecapitalize = \"recapitalize\" // Route, Denom, Amount, Incident\n\tOpRevokeRecap  = \"revoke recap\" // Route, ID (the authorization id)\n\tOpReduce       = \"reduce\"       // Route, Key, Amount, Incident\n\tOpAccept       = \"accept\"       // Route, Denom, Amount, Incident\n\tOpSettle       = \"settle\"       // Route, Client (the retired client), Items, Incident\n\tOpReplace      = \"replace\"      // Route, Client (the new client), Height (the suspect interval start)\n\tOpReopen       = \"reopen\"       // Route\n)\n\n// Params are the parameters of a governance operation; each operation reads\n// the fields its comment above names.\ntype Params struct {\n\tRoute    string\n\tDenom    string\n\tKey      string\n\tClient   string\n\tChain    string\n\tIncident string\n\tCause    string\n\tTo       address\n\tKeys     []string\n\tForged   bool\n\tAmount   uint256\n\tID       int64\n\tConfig   DenomConfig\n\tItems    []SettleItem\n\tHeight   int64\n}\n\n// Encode writes p in the wire format, every field in a fixed order.\nfunc (p Params) Encode() []byte {\n\tw := wire.NewWriter()\n\tw.Str(p.Route).Str(p.Denom).Str(p.Key).Str(p.Client).Str(p.Chain).Str(p.Incident).Str(p.Cause).Addr(p.To)\n\tw.Uvarint(uint64(len(p.Keys)))\n\tfor _, k := range p.Keys {\n\t\tw.Str(k)\n\t}\n\tw.Bool(p.Forged).Uint256(p.Amount).Int(p.ID)\n\tc := p.Config\n\tw.Str(c.Kind).Uint256(c.CapFloor).Uint256(c.SupplyCap).Uint256(c.Fee).Uint256(c.FeeFloor).Uint256(c.SendMax).Uint256(c.SendMin).Uint256(c.RefundCap).Str(c.Remote)\n\tw.Uvarint(uint64(c.Decimals))\n\tw.Uvarint(uint64(len(p.Items)))\n\tfor _, it := range p.Items {\n\t\tw.Uvarint(it.Seq).Uvarint(uint64(it.Action)).Uint256(it.Amount)\n\t}\n\tw.Int(p.Height)\n\treturn w.Bytes()\n}\n\n// DecodeParams reads what Encode writes, refusing trailing bytes.\nfunc DecodeParams(bz []byte) (Params, error) {\n\tvar p Params\n\tr := wire.NewReader(bz)\n\tp.Route, p.Denom, p.Key, p.Client, p.Chain, p.Incident, p.Cause = r.Str(), r.Str(), r.Str(), r.Str(), r.Str(), r.Str(), r.Str()\n\tp.To = r.Addr()\n\tn := r.Uvarint()\n\tif r.Err() == nil \u0026\u0026 n \u003e MaxKeys {\n\t\treturn Params{}, ufmt.Errorf(\"controls: more than %d keys\", MaxKeys)\n\t}\n\tfor i := uint64(0); i \u003c n \u0026\u0026 r.Err() == nil; i++ {\n\t\tp.Keys = append(p.Keys, r.Str())\n\t}\n\tp.Forged = r.Bool()\n\tp.Amount = r.Uint256()\n\tp.ID = r.Int()\n\tp.Config.Kind = r.Str()\n\tp.Config.CapFloor, p.Config.SupplyCap, p.Config.Fee, p.Config.FeeFloor = r.Uint256(), r.Uint256(), r.Uint256(), r.Uint256()\n\tp.Config.SendMax, p.Config.SendMin, p.Config.RefundCap, p.Config.Remote = r.Uint256(), r.Uint256(), r.Uint256(), r.Str()\n\tdecimals := r.Uvarint()\n\tif r.Err() == nil \u0026\u0026 decimals \u003e MaxDecimals {\n\t\treturn Params{}, ufmt.Errorf(\"controls: params: more than %d decimals\", MaxDecimals)\n\t}\n\tp.Config.Decimals = int(decimals)\n\tn = r.Uvarint()\n\tif r.Err() == nil \u0026\u0026 n \u003e MaxKeys {\n\t\treturn Params{}, ufmt.Errorf(\"controls: more than %d items\", MaxKeys)\n\t}\n\tfor i := uint64(0); i \u003c n \u0026\u0026 r.Err() == nil; i++ {\n\t\tseq, action, amount := r.Uvarint(), r.Uvarint(), r.Uint256()\n\t\tif action \u003e 255 {\n\t\t\treturn Params{}, errors.New(\"controls: params: action out of range\")\n\t\t}\n\t\tp.Items = append(p.Items, SettleItem{Seq: seq, Action: uint8(action), Amount: amount})\n\t}\n\tp.Height = r.Int()\n\tif err := r.Err(); err != nil {\n\t\treturn Params{}, ufmt.Errorf(\"controls: params: %v\", err)\n\t}\n\tif !r.Done() {\n\t\treturn Params{}, errors.New(\"controls: params: trailing bytes\")\n\t}\n\treturn p, nil\n}\n"},{"name":"config.gno","body":"package controls\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/aib/encoding/ethaddr/v0\"\n\t\"gno.land/p/nt/grc20/v1\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// MaxDecimals is the most decimals a voucher token takes (grc20 v1's bound).\nconst MaxDecimals = grc20.MaxDecimals\n\n// The two accountings of a route's denom (ADR pr6375 §7.1).\nconst (\n\t// KindUnescrow is a gno-native asset: sends escrow it here and returns\n\t// from Ethereum release it; its lock is its escrow, which caps payouts.\n\tKindUnescrow = \"unescrow\"\n\t// KindMint is an Ethereum-native asset: receives mint vouchers here and\n\t// sends back burn them; it has no lock, so it gets an absolute day cap and\n\t// a cap on its outstanding supply.\n\tKindMint = \"mint\"\n)\n\n// DenomConfig is the configuration of one denom on one route. Amounts are base\n// units of the denom, as uint256: an 18-decimal token's caps are set in its\n// own units.\ntype DenomConfig struct {\n\tKind string\n\t// CapFloor is, for KindUnescrow, the floor of the day cap\n\t// max(CapFloor, lock/10); for KindMint, the day cap itself.\n\tCapFloor uint256\n\t// SupplyCap (KindMint only) bounds the vouchers outstanding plus the open\n\t// mint claims; a receive beyond it is refused with an error ack.\n\tSupplyCap uint256\n\t// Fee is charged on every send of the denom (KindUnescrow) and is the\n\t// first installment of every claim, paid to the route's fee collector.\n\tFee uint256\n\t// FeeFloor is the least the fee may be (ADR pr6375 §9: 2 GNOT).\n\tFeeFloor uint256\n\t// SendMax and SendMin bound a send (KindUnescrow only; 0 for KindMint,\n\t// whose sends are voucher returns and are never limited).\n\tSendMax uint256\n\tSendMin uint256\n\t// RefundCap is the refund meter's day cap (ADR pr6375 §7.13).\n\tRefundCap uint256\n\t// Remote (KindUnescrow only) is the lower-case Ethereum address of the\n\t// denom's token on Ethereum (its GnoVoucher): a send to it is refused,\n\t// since that contract could never move what it receives (§7.8). A minted\n\t// denom's token is its base, refused the same way.\n\tRemote string\n\t// Decimals (KindMint only) is the decimals the denom's voucher token is\n\t// created with on its first receive, 0 to MaxDecimals: an ICS20 packet\n\t// carries no token metadata, so governance names the counterparty's.\n\t// Amounts stay base units whatever it is; it only tells wallets how to\n\t// show them. A voucher's decimals never change once it exists (the\n\t// transfer app refuses a configuration that would), so a change is\n\t// accepted only before the first receive. An unescrowed denom's token is\n\t// gno-native and takes none.\n\tDecimals int\n}\n\n// Validate checks a configuration on its own.\nfunc (c DenomConfig) Validate() error {\n\tif c.Decimals \u003c 0 || c.Decimals \u003e MaxDecimals {\n\t\treturn ufmt.Errorf(\"controls: a voucher has 0 to %d decimals, not %d\", MaxDecimals, c.Decimals)\n\t}\n\tswitch c.Kind {\n\tcase KindUnescrow:\n\t\tif c.SendMin == 0 || c.SendMax \u003c c.SendMin {\n\t\t\treturn ufmt.Errorf(\"controls: send limits must satisfy 0 \u003c min (%s) \u003c= max (%s)\", u(c.SendMin), u(c.SendMax))\n\t\t}\n\t\tif c.SupplyCap != 0 {\n\t\t\treturn errors.New(\"controls: an unescrowed denom has no supply cap\")\n\t\t}\n\t\tif c.Decimals != 0 {\n\t\t\treturn errors.New(\"controls: an unescrowed denom's token is gno-native and takes no decimals\")\n\t\t}\n\t\ta, err := ethaddr.Parse(c.Remote)\n\t\tif err != nil || a.IsZero() || a.Lower() != c.Remote {\n\t\t\treturn ufmt.Errorf(\"controls: an unescrowed denom names its Ethereum token, a lower-case address, not %q\", c.Remote)\n\t\t}\n\tcase KindMint:\n\t\tif c.SupplyCap == 0 {\n\t\t\treturn errors.New(\"controls: a minted denom needs a positive supply cap\")\n\t\t}\n\t\tif c.SendMax != 0 || c.SendMin != 0 {\n\t\t\treturn errors.New(\"controls: a minted denom's sends are voucher returns and take no send limits\")\n\t\t}\n\t\tif c.Remote != \"\" {\n\t\t\treturn errors.New(\"controls: a minted denom's Ethereum token is its base\")\n\t\t}\n\tdefault:\n\t\treturn ufmt.Errorf(\"controls: unknown accounting %q\", c.Kind)\n\t}\n\tif c.CapFloor == 0 {\n\t\treturn errors.New(\"controls: the day cap must be positive\")\n\t}\n\tif c.FeeFloor == 0 || c.Fee \u003c c.FeeFloor {\n\t\treturn ufmt.Errorf(\"controls: the fee (%s) must be at least the fee floor (%s), which is positive\", u(c.Fee), u(c.FeeFloor))\n\t}\n\t// The fee ceiling is cap/4 and the cap is never below CapFloor; equally,\n\t// the cap floor is never below 4 fees.\n\tif c.Fee \u003e c.CapFloor/4 {\n\t\treturn ufmt.Errorf(\"controls: the fee (%s) is above its ceiling of a quarter of the day cap (%s)\", u(c.Fee), u(c.CapFloor/4))\n\t}\n\tif c.RefundCap == 0 {\n\t\treturn errors.New(\"controls: the refund cap must be positive\")\n\t}\n\treturn nil\n}\n\n// Loosens reports whether moving from old to new loosens anything, so the\n// change waits for the 7-day delay (ADR pr6375 §7.5, §9): a higher cap, supply\n// cap, fee, send maximum or refund cap, a lower send minimum, or any change of\n// the fee floor, of the Ethereum token or of the voucher's decimals (a\n// metadata commitment the voucher keeps for good once it exists: neither\n// direction tightens, so both wait). Everything else (lowering a cap, a fee,\n// a maximum, raising a minimum) tightens and applies at once.\nfunc Loosens(old, new DenomConfig) bool {\n\treturn new.Kind != old.Kind ||\n\t\tnew.CapFloor \u003e old.CapFloor ||\n\t\tnew.SupplyCap \u003e old.SupplyCap ||\n\t\tnew.Fee \u003e old.Fee ||\n\t\tnew.FeeFloor != old.FeeFloor ||\n\t\tnew.SendMax \u003e old.SendMax ||\n\t\tnew.SendMin \u003c old.SendMin ||\n\t\tnew.RefundCap \u003e old.RefundCap ||\n\t\tnew.Remote != old.Remote ||\n\t\tnew.Decimals != old.Decimals\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/aib/ibc/ics20/controls/v0\"\ngno = \"0.9\"\n"},{"name":"meter.gno","body":"package controls\n\nimport \"math/overflow\"\n\n// Meter is a token bucket: a level that refills continuously at a rate per\n// day up to a ceiling, with no midnight reset, so at most two ceilings can\n// leave in any 24 hours (a full bucket plus a day of refill). A meter belongs\n// to a payouts epoch: when the epoch changes (a payouts flag was cleared) it\n// empties first, as of the epoch's start (ADR pr6375 §7.1: meters start\n// empty at launch, after a migration and whenever a payouts flag is cleared).\n// Frac carries the refill's remainder in base units × seconds (below a day),\n// so refills in any pieces sum to exactly rate a day.\n//\n// Ported from gnolang/bridges (ethereum-draft, a58efe81c9eb),\n// solana/realm/p/hlroute/v0/hlroute.gno (Meter, refill), Copyright (C) 2026\n// NewTendermint, LLC, GNO Network General Public License.\ntype Meter struct {\n\tLevel uint256\n\tAt    int64 // last refill, unix seconds\n\tEpoch int64 // the payouts epoch it last saw\n\tFrac  uint256\n\t// The rate and ceiling in effect since At, set by the last refill: the\n\t// time since At refills at them, never at a rate or ceiling given later\n\t// (a cap change, of the configuration or of the lock a cap derives from,\n\t// never rewrites the refill before it was seen).\n\tRate, Ceil uint256\n\tSet        bool\n}\n\n// NewMeter is an empty meter as of now, in epoch.\nfunc NewMeter(now, epoch int64) Meter {\n\treturn Meter{At: now, Epoch: epoch}\n}\n\n// Refill brings the meter up to now and then sets rate and ceil for the\n// time to come: the time since the last refill refills at the rate and up to\n// the ceiling that refill set (the ones given now on a meter's first\n// refill), so a new cap applies only from the refill that first sees it,\n// never to the time before (ADR pr6375 §7.1, as Ethereum's buckets settle at\n// the old cap); a configuration change refills its meters at the old cap,\n// then at the new with no time elapsed (Route.Reconfigure), so the new one\n// applies from the change. A new epoch empties the meter as of epochAt\n// first. A ceiling below the level (the cap fell) brings the level down to\n// it.\nfunc (m *Meter) Refill(now int64, rate, ceil uint256, epoch, epochAt int64) {\n\tif m.Epoch != epoch {\n\t\tm.Level, m.At, m.Epoch, m.Frac = 0, epochAt, epoch, 0\n\t}\n\tnewRate, newCeil := rate, ceil\n\tif m.Set {\n\t\trate, ceil = m.Rate, m.Ceil\n\t}\n\tm.Rate, m.Ceil, m.Set = newRate, newCeil, true\n\tif now \u003e m.At {\n\t\tel := now - m.At\n\t\tif el \u003e Day {\n\t\t\tel = Day\n\t\t\tm.Frac = 0\n\t\t}\n\t\te, day := uint256(el), uint256(Day)\n\t\t// (rate%Day)*el + Frac stays below 2·Day², so only the whole days'\n\t\t// part and the addition to the level can overflow: either fills the\n\t\t// meter, which saturates at the ceiling.\n\t\tnum := (rate%day)*e + m.Frac\n\t\twhole, ok := overflow.Mulu256(rate/day, e)\n\t\tadd, ok2 := overflow.Addu256(whole, num/day)\n\t\tm.Frac = num % day\n\t\tm.At = now\n\t\tif !ok || !ok2 || m.Level \u003e= ceil || add \u003e= ceil-m.Level {\n\t\t\tm.Level = ceil\n\t\t} else {\n\t\t\tm.Level += add\n\t\t}\n\t}\n\tif m.Level \u003e= ceil {\n\t\tm.Level, m.Frac = ceil, 0\n\t}\n\tif m.Level \u003e= newCeil {\n\t\tm.Level, m.Frac = newCeil, 0\n\t}\n}\n\n// Take removes amount from the level. The caller checked it is available.\nfunc (m *Meter) Take(amount uint256) { m.Level = sub0(m.Level, amount) }\n"},{"name":"reader.gno","body":"package controls\n\nimport (\n\t\"encoding/hex\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/aib/jsonmd/v0\"\n\n\t\"gno.land/p/aib/ibc/types/v0\"\n\t\"gno.land/p/nt/avl/v0\"\n)\n\n// Reader is a read-only view of a Registry: it exposes no mutator and hands\n// out values only, so the transfer proxy can give it to anyone (its renders,\n// ibcgov's builders, monitors). Its registry pointer is unexported, so no\n// other package can reach the Registry's mutators through it.\ntype Reader struct {\n\tr *Registry\n}\n\n// Valid reports whether the reader views a registry.\nfunc (rd Reader) Valid() bool { return rd.r != nil }\n\nfunc (rd Reader) route(key string) *Route {\n\tif rd.r == nil {\n\t\treturn nil\n\t}\n\treturn rd.r.Route(key)\n}\n\n// Routes lists the route keys.\nfunc (rd Reader) Routes() []string {\n\tif rd.r == nil {\n\t\treturn nil\n\t}\n\treturn rd.r.Keys()\n}\n\n// RouteOf is the key of the route whose current client is clientID, or \"\".\nfunc (rd Reader) RouteOf(clientID string) string {\n\tif rd.r == nil {\n\t\treturn \"\"\n\t}\n\tif rt := rd.r.RouteOf(clientID); rt != nil {\n\t\treturn rt.Key\n\t}\n\treturn \"\"\n}\n\n// RetiredRouteOf is the key of the route a replacement retired clientID\n// from, or \"\".\nfunc (rd Reader) RetiredRouteOf(clientID string) string {\n\tif rd.r == nil {\n\t\treturn \"\"\n\t}\n\treturn rd.r.RetiredRouteOf(clientID)\n}\n\n// Client is the current client of a route, or \"\".\nfunc (rd Reader) Client(route string) string {\n\tif rt := rd.route(route); rt != nil {\n\t\treturn rt.Client\n\t}\n\treturn \"\"\n}\n\n// SuspectUnsettled is how many send records of a retired client processed\n// in its suspect interval are not settled (see Route.SuspectUnsettled).\nfunc (rd Reader) SuspectUnsettled(route, clientID string) int64 {\n\tif rt := rd.route(route); rt != nil {\n\t\treturn rt.SuspectUnsettled(clientID)\n\t}\n\treturn 0\n}\n\n// ReopenBlockers is every retired client still blocking the route's\n// reopening (see Route.ReopenBlockers): empty when the reopening may apply.\nfunc (rd Reader) ReopenBlockers(route string) []string {\n\tif rt := rd.route(route); rt != nil {\n\t\treturn rt.ReopenBlockers()\n\t}\n\treturn nil\n}\n\n// SuspectFrom is the suspect height recorded when a replacement retired\n// clientID from the route, and whether one did.\nfunc (rd Reader) SuspectFrom(route, clientID string) (int64, bool) {\n\tif rt := rd.route(route); rt != nil {\n\t\treturn rt.SuspectFrom(clientID)\n\t}\n\treturn 0, false\n}\n\n// OpenSends is how many send records of clientID on the route are open.\nfunc (rd Reader) OpenSends(route, clientID string) int64 {\n\tif rt := rd.route(route); rt != nil {\n\t\treturn rt.OpenSends(clientID)\n\t}\n\treturn 0\n}\n\n// LossClosed reports whether a route takes no new exposure because a loss\n// has no restoring decision (see Route.ExposureClosed).\nfunc (rd Reader) LossClosed(route string) bool {\n\tif rt := rd.route(route); rt != nil {\n\t\treturn rt.LossClosed()\n\t}\n\treturn false\n}\n\n// Closed reports whether a replacement closed a route (see Route.IsClosed).\nfunc (rd Reader) Closed(route string) bool {\n\tif rt := rd.route(route); rt != nil {\n\t\treturn rt.IsClosed()\n\t}\n\treturn false\n}\n\n// IsFeeCollector reports whether a is the fee collector of any route.\nfunc (rd Reader) IsFeeCollector(a address) bool {\n\treturn rd.r != nil \u0026\u0026 rd.r.IsFeeCollector(a)\n}\n\n// FeeCollector is a route's fee collector, or \"\".\nfunc (rd Reader) FeeCollector(route string) address {\n\tif rt := rd.route(route); rt != nil {\n\t\treturn rt.FeeCollector\n\t}\n\treturn \"\"\n}\n\n// Denoms lists a route's denoms.\nfunc (rd Reader) Denoms(route string) []string {\n\tif rt := rd.route(route); rt != nil {\n\t\treturn rt.Denoms()\n\t}\n\treturn nil\n}\n\n// DenomConfig is a denom's configuration, and whether it is enabled.\nfunc (rd Reader) DenomConfig(route, denom string) (DenomConfig, bool) {\n\tif rt := rd.route(route); rt != nil {\n\t\tif d := rt.Denom(denom); d != nil {\n\t\t\treturn d.Config, true\n\t\t}\n\t}\n\treturn DenomConfig{}, false\n}\n\n// Totals is a denom's accounting, and whether it is enabled.\nfunc (rd Reader) Totals(route, denom string) (Totals, bool) {\n\tif rt := rd.route(route); rt != nil {\n\t\tif d := rt.Denom(denom); d != nil {\n\t\t\treturn d.Totals, true\n\t\t}\n\t}\n\treturn Totals{}, false\n}\n\n// Claim is a claim by packet key.\nfunc (rd Reader) Claim(route, key string) (Claim, bool) {\n\tif rt := rd.route(route); rt != nil {\n\t\treturn rt.Claim(key)\n\t}\n\treturn Claim{}, false\n}\n\n// Sweep is a sweep entry by packet key.\nfunc (rd Reader) Sweep(route, key string) (SweepEntry, bool) {\n\tif rt := rd.route(route); rt != nil {\n\t\treturn rt.Sweep(key)\n\t}\n\treturn SweepEntry{}, false\n}\n\n// Send is a send record by packet key.\nfunc (rd Reader) Send(route, key string) (SendRecord, bool) {\n\tif rt := rd.route(route); rt != nil {\n\t\treturn rt.Send(key)\n\t}\n\treturn SendRecord{}, false\n}\n\n// Voidable reports whether a void can take key as it stands.\nfunc (rd Reader) Voidable(route, key string, forged bool) bool {\n\tif rt := rd.route(route); rt != nil {\n\t\treturn rt.Voidable(key, forged)\n\t}\n\treturn false\n}\n\n// Unrecovered is a route's economic open loss total.\nfunc (rd Reader) Unrecovered(route string) uint256 {\n\tif rt := rd.route(route); rt != nil {\n\t\treturn rt.Unrecovered()\n\t}\n\treturn 0\n}\n\n// Unresolved is a route's loss total still waiting for a restoring decision.\nfunc (rd Reader) Unresolved(route string) uint256 {\n\tif rt := rd.route(route); rt != nil {\n\t\treturn rt.Unresolved()\n\t}\n\treturn 0\n}\n\n// Meters is what the bridge meter and a recipient's meter of a denom hold at\n// now under the payouts epoch (epoch, as of epochAt) and lock, as PayClaims\n// would find them, computed on copies: the state does not change. A\n// recipient without a queue reads 0.\nfunc (rd Reader) Meters(route, denom string, to address, now, epoch, epochAt int64, lock uint256) (bridge, recipient uint256) {\n\trt := rd.route(route)\n\tif rt == nil {\n\t\treturn 0, 0\n\t}\n\td := rt.Denom(denom)\n\tif d == nil {\n\t\treturn 0, 0\n\t}\n\tc := d.DayCap(lock)\n\tm := d.meter\n\tm.Refill(now, c, c, epoch, epochAt)\n\tif v := d.queues.Get(string(to)); v != nil {\n\t\tq := v.(*queue).meter\n\t\tq.Refill(now, c/4, c/4, epoch, epochAt)\n\t\trecipient = q.Level\n\t}\n\treturn m.Level, recipient\n}\n\n// IsRefused reports whether addr is a refused send recipient of the route.\nfunc (rd Reader) IsRefused(route, addr string) bool {\n\tif rt := rd.route(route); rt != nil {\n\t\treturn rt.IsRefused(addr)\n\t}\n\treturn false\n}\n\n// Recap is a recapitalization authorization of a route.\nfunc (rd Reader) Recap(route string, id int64) (Recap, bool) {\n\tif rt := rd.route(route); rt != nil {\n\t\treturn rt.Recap(id)\n\t}\n\treturn Recap{}, false\n}\n\n// ------------------------------------------------------------------- JSON\n\n// q is s as a JSON string, inert in Markdown (jsonmd.Quote): the records\n// keep raw, attacker-chosen strings, such as an undecodable receiver.\nfunc q(s string) string { return jsonmd.Quote(s) }\n\nfunc strList(ss []string) string {\n\tparts := make([]string, len(ss))\n\tfor i, s := range ss {\n\t\tparts[i] = q(s)\n\t}\n\treturn \"[\" + strings.Join(parts, \",\") + \"]\"\n}\n\n// RoutesJSON is every route's JSON, as an array.\nfunc (rd Reader) RoutesJSON() string {\n\tvar parts []string\n\tfor _, k := range rd.Routes() {\n\t\tparts = append(parts, rd.RouteJSON(k))\n\t}\n\treturn \"[\" + strings.Join(parts, \",\") + \"]\"\n}\n\n// RouteJSON is a route's state for monitors (ADR pr6375 §7.11): its client,\n// its retired clients, its fee collector, its refused recipients, and per\n// denom the configuration, the meter levels as last refilled and the\n// accounting terms. \"null\" for an unknown route. Every amount is a decimal\n// string (a 256-bit value is lossy as a JSON number for readers that parse\n// doubles); heights, times and counts are numbers.\nfunc (rd Reader) RouteJSON(route string) string {\n\trt := rd.route(route)\n\tif rt == nil {\n\t\treturn \"null\"\n\t}\n\tvar b strings.Builder\n\tb.WriteString(\"{\\\"route\\\":\" + q(rt.Key) + \",\\\"client\\\":\" + q(rt.Client) + \",\\\"replaced\\\":\" + strList(rt.Replaced) +\n\t\t\",\\\"enrolled\\\":\" + itoa(rt.Enrolled) + \",\\\"activated\\\":\" + itoa(rt.Activated) + \",\\\"closed\\\":\" + strconv.FormatBool(rt.IsClosed()) + \",\\\"loss_closed\\\":\" + strconv.FormatBool(rt.LossClosed()) + \",\\\"fee_collector\\\":\" + q(string(rt.FeeCollector)) +\n\t\t\",\\\"refused_recipients\\\":\" + strList(rt.Refused()) + \",\\\"protected_recipients\\\":\" + strList(rt.Protected()) + \",\\\"unrecovered\\\":\" + qu(rt.Unrecovered()) + \",\\\"unresolved\\\":\" + qu(rt.Unresolved()) + \",\\\"denoms\\\":[\")\n\tfor i, name := range rt.Denoms() {\n\t\tif i \u003e 0 {\n\t\t\tb.WriteString(\",\")\n\t\t}\n\t\tb.WriteString(denomJSON(rt.Denom(name)))\n\t}\n\tb.WriteString(\"]}\")\n\treturn b.String()\n}\n\nfunc denomJSON(d *Denom) string {\n\tc, t := d.Config, d.Totals\n\treturn \"{\\\"denom\\\":\" + q(d.Name) + \",\\\"kind\\\":\" + q(c.Kind) +\n\t\t\",\\\"cap_floor\\\":\" + qu(c.CapFloor) + \",\\\"launch_cap_floor\\\":\" + qu(d.LaunchCapFloor) +\n\t\t\",\\\"supply_cap\\\":\" + qu(c.SupplyCap) + \",\\\"fee\\\":\" + qu(c.Fee) + \",\\\"fee_floor\\\":\" + qu(c.FeeFloor) +\n\t\t\",\\\"send_max\\\":\" + qu(c.SendMax) + \",\\\"send_min\\\":\" + qu(c.SendMin) + \",\\\"refund_cap\\\":\" + qu(c.RefundCap) + \",\\\"remote\\\":\" + q(c.Remote) +\n\t\t\",\\\"decimals\\\":\" + itoa(int64(c.Decimals)) +\n\t\t\",\\\"meter\\\":{\\\"level\\\":\" + qu(d.meter.Level) + \",\\\"at\\\":\" + itoa(d.meter.At) + \",\\\"epoch\\\":\" + itoa(d.meter.Epoch) + \"}\" +\n\t\t\",\\\"refund_meter\\\":{\\\"level\\\":\" + qu(d.refundMeter.Level) + \",\\\"at\\\":\" + itoa(d.refundMeter.At) + \"}\" +\n\t\t\",\\\"waiting\\\":\" + itoa(int64(d.waiting.Size())) + \",\\\"queued_refunds\\\":\" + itoa(int64(d.refundQueue.Size())) +\n\t\t\",\\\"totals\\\":{\\\"credited\\\":\" + qu(t.Credited) + \",\\\"owed\\\":\" + qu(t.Owed) + \",\\\"withheld\\\":\" + qu(t.Withheld) +\n\t\t\",\\\"swept\\\":\" + qu(t.Swept) + \",\\\"paid\\\":\" + qu(t.Paid) + \",\\\"fees\\\":\" + qu(t.Fees) +\n\t\t\",\\\"sweep_paid\\\":\" + qu(t.SweepPaid) + \",\\\"outstanding\\\":\" + qu(t.Outstanding) + \",\\\"returning\\\":\" + qu(t.Returning) + \",\\\"proven_owed\\\":\" + qu(t.ProvenOwed) + \",\\\"in_flight\\\":\" + qu(t.InFlight) + \",\\\"sent\\\":\" + qu(t.Sent) +\n\t\t\",\\\"send_fees\\\":\" + qu(t.SendFees) + \",\\\"refunded\\\":\" + qu(t.Refunded) + \",\\\"queued_refunds\\\":\" + qu(t.QueuedRefunds) +\n\t\t\",\\\"lost\\\":\" + qu(t.Lost) + \",\\\"recapitalized\\\":\" + qu(t.Recapitalized) + \",\\\"reduced\\\":\" + qu(t.Reduced) +\n\t\t\",\\\"accepted\\\":\" + qu(t.Accepted) + \",\\\"unrecovered\\\":\" + qu(t.Unrecovered()) + \",\\\"unresolved\\\":\" + qu(t.Unresolved()) + \"}}\"\n}\n\n// ClaimJSON is a claim's JSON.\nfunc ClaimJSON(c Claim) string {\n\treturn \"{\\\"key\\\":\" + q(c.Key) + \",\\\"denom\\\":\" + q(c.Denom) + \",\\\"to\\\":\" + q(string(c.To)) +\n\t\t\",\\\"amount\\\":\" + qu(c.Amount) + \",\\\"paid\\\":\" + qu(c.Paid) + \",\\\"reduced\\\":\" + qu(c.Reduced) + \",\\\"fee\\\":\" + qu(c.Fee) +\n\t\t\",\\\"state\\\":\" + q(c.State) + \",\\\"by\\\":\" + q(c.By) + \",\\\"at\\\":\" + itoa(c.At) + \",\\\"height\\\":\" + itoa(c.Height) +\n\t\t\",\\\"seq\\\":\" + itoa(c.Seq) + \"}\"\n}\n\n// SweepJSON is a sweep entry's JSON.\nfunc SweepJSON(e SweepEntry) string {\n\treturn \"{\\\"key\\\":\" + q(e.Key) + \",\\\"denom\\\":\" + q(e.Denom) + \",\\\"to\\\":\" + q(e.To) + \",\\\"amount\\\":\" + qu(e.Amount) + \",\\\"reduced\\\":\" + qu(e.Reduced) +\n\t\t\",\\\"reason\\\":\" + q(e.Reason) + \",\\\"height\\\":\" + itoa(e.Height) + \",\\\"voided\\\":\" + strconv.FormatBool(e.Voided) +\n\t\t\",\\\"paid\\\":\" + strconv.FormatBool(e.Paid) + \",\\\"paid_to\\\":\" + q(e.PaidTo) + fromClaimJSON(e) + \"}\"\n}\n\n// fromClaimJSON is what a sweep entry moved from a claim adds to its JSON.\nfunc fromClaimJSON(e SweepEntry) string {\n\tif !e.FromClaim {\n\t\treturn \"\"\n\t}\n\treturn \",\\\"from_claim\\\":true,\\\"claim_paid\\\":\" + qu(e.ClaimPaid)\n}\n\n// SendJSON is a send record's JSON: the full packet (hex payload values), its\n// commitment, the send's metadata and what the callbacks attached.\nfunc SendJSON(r SendRecord) string {\n\treturn \"{\\\"key\\\":\" + q(r.Key) + \",\\\"packet\\\":\" + packetJSON(r.Packet) + \",\\\"commitment\\\":\" + q(hex.EncodeToString(r.Commitment)) +\n\t\t\",\\\"sender\\\":\" + q(string(r.Sender)) + \",\\\"receiver\\\":\" + q(r.Receiver) + \",\\\"denom\\\":\" + q(r.Denom) +\n\t\t\",\\\"kind\\\":\" + q(r.Kind) + \",\\\"amount\\\":\" + qu(r.Amount) + \",\\\"fee\\\":\" + qu(r.Fee) +\n\t\t\",\\\"sent_height\\\":\" + itoa(r.SentHeight) + \",\\\"sent_at\\\":\" + itoa(r.SentAt) + \",\\\"outcome\\\":\" + q(r.Outcome) +\n\t\t\",\\\"processed_height\\\":\" + itoa(r.ProcessedHeight) + \",\\\"processed_at\\\":\" + itoa(r.ProcessedAt) +\n\t\t\",\\\"payment\\\":\" + q(r.Payment) + \",\\\"refunded\\\":\" + qu(r.Refunded) + \",\\\"settlement\\\":\" + itoa(int64(r.Settlement)) + \",\\\"reservation\\\":\" + qu(r.Reservation) + \",\\\"proven\\\":\" + itoa(int64(r.Proven)) + \",\\\"proven_at\\\":\" + strconv.FormatUint(r.ProvenAt, 10) + \",\\\"proven_by\\\":\" + q(r.ProvenBy) + \",\\\"proven_gen\\\":\" + strconv.FormatUint(r.ProvenGen, 10) + \",\\\"incident\\\":\" + q(r.Incident) + \"}\"\n}\n\nfunc packetJSON(p types.Packet) string {\n\tvar pl []string\n\tfor _, x := range p.Payloads {\n\t\tpl = append(pl, \"{\\\"source_port\\\":\"+q(x.SourcePort)+\",\\\"destination_port\\\":\"+q(x.DestinationPort)+\n\t\t\t\",\\\"version\\\":\"+q(x.Version)+\",\\\"encoding\\\":\"+q(x.Encoding)+\",\\\"value\\\":\"+q(hex.EncodeToString(x.Value))+\"}\")\n\t}\n\treturn \"{\\\"sequence\\\":\" + strconv.FormatUint(p.Sequence, 10) + \",\\\"source_client\\\":\" + q(p.SourceClient) +\n\t\t\",\\\"destination_client\\\":\" + q(p.DestinationClient) + \",\\\"timeout_timestamp\\\":\" +\n\t\tstrconv.FormatUint(p.TimeoutTimestamp, 10) + \",\\\"payloads\\\":[\" + strings.Join(pl, \",\") + \"]}\"\n}\n\n// LossJSON is a loss's JSON.\nfunc LossJSON(l Loss) string {\n\treturn \"{\\\"key\\\":\" + q(l.Key) + \",\\\"direction\\\":\" + q(l.Direction) + \",\\\"denom\\\":\" + q(l.Denom) + \",\\\"amount\\\":\" + qu(l.Amount) + \",\\\"chain\\\":\" + q(l.Chain) +\n\t\t\",\\\"incident\\\":\" + q(l.Incident) + \",\\\"cause\\\":\" + q(l.Cause) + \"}\"\n}\n\n// page walks a tree from the first storage key at or after from, up to\n// PageLines entries: {\"entries\":[...],\"next\":\u003ccursor\u003e}. next is the storage\n// key of the first entry left out, the from of the following page, or \"\"\n// when the page reaches the end. The cursor is opaque: a claim's, sweep\n// entry's or send record's storage key is its packet key, a loss's is its\n// direction and packet key.\nfunc page(t *avl.Tree, from string, fn func(v any) string) string {\n\tvar parts []string\n\tnext := \"\"\n\tt.Iterate(from, \"\", func(k string, v any) bool {\n\t\tif len(parts) == PageLines {\n\t\t\tnext = k\n\t\t\treturn true\n\t\t}\n\t\tparts = append(parts, fn(v))\n\t\treturn false\n\t})\n\treturn \"{\\\"entries\\\":[\" + strings.Join(parts, \",\") + \"],\\\"next\\\":\" + q(next) + \"}\"\n}\n\nconst emptyPage = \"{\\\"entries\\\":[],\\\"next\\\":\\\"\\\"}\"\n\n// ClaimsJSON is a page of a route's claims (every state), by packet key from\n// from on.\nfunc (rd Reader) ClaimsJSON(route, from string) string {\n\trt := rd.route(route)\n\tif rt == nil {\n\t\treturn emptyPage\n\t}\n\treturn page(\u0026rt.claims, from, func(v any) string { return ClaimJSON(*v.(*Claim)) })\n}\n\n// SweepsJSON is a page of a route's sweep entries, by packet key.\nfunc (rd Reader) SweepsJSON(route, from string) string {\n\trt := rd.route(route)\n\tif rt == nil {\n\t\treturn emptyPage\n\t}\n\treturn page(\u0026rt.sweeps, from, func(v any) string { return SweepJSON(*v.(*SweepEntry)) })\n}\n\n// SendsJSON is a page of a route's send records, by packet key.\nfunc (rd Reader) SendsJSON(route, from string) string {\n\trt := rd.route(route)\n\tif rt == nil {\n\t\treturn emptyPage\n\t}\n\treturn page(\u0026rt.sends, from, func(v any) string { return SendJSON(*v.(*SendRecord)) })\n}\n\n// LossesJSON is a page of a route's loss ledger.\nfunc (rd Reader) LossesJSON(route, from string) string {\n\trt := rd.route(route)\n\tif rt == nil {\n\t\treturn emptyPage\n\t}\n\treturn page(\u0026rt.losses, from, func(v any) string { return LossJSON(*v.(*Loss)) })\n}\n"},{"name":"refunds.gno","body":"package controls\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\n\t\"gno.land/p/aib/ibc/types/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Send outcomes, attached by the acknowledgement and timeout callbacks.\nconst (\n\tOutcomePending = \"\"        // in flight: no acknowledgement or timeout processed yet\n\tOutcomeSuccess = \"success\" // a success acknowledgement\n\tOutcomeError   = \"error\"   // an error acknowledgement: refunded\n\tOutcomeTimeout = \"timeout\" // a timeout: refunded\n)\n\n// Payment states of a send record (ADR pr6375 §7.13).\nconst (\n\tPayNone       = \"\"           // nothing owed: in flight, or acknowledged as a success\n\tPayPaid       = \"paid\"       // refunded at once\n\tPayQueued     = \"queued\"     // refund owed, waiting under the refund meter, a refunds hold or a short escrow\n\tPayClaimed    = \"claimed\"    // a queued refund paid in full later\n\tPayRejected   = \"rejected\"   // a queued refund a reconciliation settled unpaid\n\tPayReconciled = \"reconciled\" // a refund a reconciliation paid by decision\n)\n\n// SendRecord is the immutable record of one send, written by the send entry\n// point in the same transaction as the send (ADR pr6375 §7.13): the full\n// packet (timeout and payload included), its commitment and the send's\n// metadata. The acknowledgement and timeout callbacks only attach the outcome,\n// the local processing height and the payment state. A reconciliation (§7.12)\n// authenticates a packet whose commitment its processing deleted by this\n// record.\ntype SendRecord struct {\n\tKey        string // packet key: source client / sequence\n\tPacket     types.Packet\n\tCommitment []byte // types.CommitPacket(Packet)\n\tSender     address\n\tReceiver   string\n\tDenom      string // the accounting denom: escrow denom or voucher IBC denom\n\tKind       string // KindUnescrow (escrowed) or KindMint (burned)\n\tAmount     uint256\n\tFee        uint256 // charged on top of Amount, paid to the collector\n\tSentHeight int64\n\tSentAt     int64\n\n\tOutcome         string\n\tProcessedHeight int64\n\tProcessedAt     int64\n\tPayment         string\n\tRefunded        uint256 // what was refunded so far, at once, by claims or by decision\n\tSettlement      uint8   // the reconciliation's action (SettlePay...), 0 while unsettled\n\tReservation     uint256 // KindMint: the supply this send still holds against the cap, released or consumed once\n\tProven          uint8   // the destination's outcome, proven through the replacement client (ProvenDelivered...)\n\tProvenBy        string  // the client that verified it: the outcome is void once it is no longer Active\n\tProvenAt        uint64  // the proof's height on the replacement client\n\tProvenGen       uint64  // ProvenBy's trust generation then: void once a recovery, freeze, upgrade, migration or substitution starts another\n\tProvenReserve   uint256 // what its proven refund reserves (ProvenOwed) until it settles: a send a forged success released, proven refundable\n\tIncident        string\n\n\tinFlight uint256 // what InFlight counts of it while it is open\n}\n\nfunc copyPacket(p types.Packet) types.Packet {\n\tout := p\n\tout.Payloads = make([]types.Payload, len(p.Payloads))\n\tfor i, pl := range p.Payloads {\n\t\tpl.Value = append([]byte{}, pl.Value...)\n\t\tout.Payloads[i] = pl\n\t}\n\treturn out\n}\n\n// RecordSend writes a send's record and books the send: it escrowed the\n// amount (KindUnescrow) or burned it (KindMint, lowering the outstanding\n// vouchers). The packet and the commitment are copied, so the caller keeps no\n// alias into the record. A key is written once.\nfunc (rt *Route) RecordSend(rec SendRecord) error {\n\tif _, _, ok := ParsePacketKey(rec.Key); !ok {\n\t\treturn errors.New(\"controls: not a packet key: \" + rec.Key)\n\t}\n\tif rt.sends.Has(rec.Key) {\n\t\treturn errors.New(\"controls: a send record exists for \" + rec.Key)\n\t}\n\td := rt.Denom(rec.Denom)\n\tif d == nil {\n\t\treturn errors.New(\"controls: denom \" + rec.Denom + \" is not enabled on route \" + rt.Key)\n\t}\n\tif rec.Kind != d.Config.Kind {\n\t\treturn errors.New(\"controls: the send's accounting differs from its denom's\")\n\t}\n\tif rec.Amount == 0 {\n\t\treturn errors.New(\"controls: invalid send amounts\")\n\t}\n\tr := rec\n\tr.Packet = copyPacket(rec.Packet)\n\tr.Commitment = append([]byte{}, rec.Commitment...)\n\tr.Outcome, r.ProcessedHeight, r.ProcessedAt, r.Payment, r.Refunded, r.Settlement, r.Incident = OutcomePending, 0, 0, PayNone, 0, 0, \"\"\n\tr.Reservation = 0\n\trt.sends.Set(r.Key, \u0026r)\n\trt.track(\u0026r, false)\n\trt.addPending(r.Key, 1)\n\td.Totals.Sent = statAdd(d.Totals.Sent, r.Amount)\n\td.Totals.SendFees = statAdd(d.Totals.SendFees, r.Fee)\n\tif d.Config.Kind == KindMint {\n\t\t// The burned vouchers leave circulation but stay counted against\n\t\t// the supply cap until the send settles: a refund mints them again.\n\t\t// Vouchers minted before the route was controlled floor it at 0.\n\t\td.Totals.Outstanding = sub0(d.Totals.Outstanding, r.Amount)\n\t\tr.Reservation = r.Amount\n\t\td.Totals.Returning = mustAdd(d.Totals.Returning, r.Amount)\n\t}\n\treturn nil\n}\n\n// isOpen reports whether a send record is still open (ADR pr6375 §5.4 kind\n// 13): no reconciliation settled it, and its outcome is pending (the\n// commitment outstanding) or its refund is queued.\nfunc isOpen(r *SendRecord) bool {\n\treturn r.Settlement == 0 \u0026\u0026 (r.Outcome == OutcomePending || r.Payment == PayQueued)\n}\n\n// track updates the count of open records of r's client after a change to r,\n// which was open (was) before it.\nfunc (rt *Route) track(r *SendRecord, was bool) {\n\tnow := isOpen(r)\n\tif now == was {\n\t\treturn\n\t}\n\td := rt.Denom(r.Denom)\n\t// InFlight counts what an open record may still owe; an installment\n\t// paid while it is open comes off at once (PayRefunds).\n\tif now {\n\t\tr.inFlight = mustSub(r.Amount, r.Refunded)\n\t\td.Totals.InFlight = mustAdd(d.Totals.InFlight, r.inFlight)\n\t} else {\n\t\td.Totals.InFlight = mustSub(d.Totals.InFlight, r.inFlight)\n\t\tr.inFlight = 0\n\t}\n\tclient := clientOfKey(r.Key)\n\tvar n int64\n\tif v := rt.openSends.Get(client); v != nil {\n\t\tn = v.(int64)\n\t}\n\tif now {\n\t\tn++\n\t} else {\n\t\tn--\n\t}\n\tif n == 0 {\n\t\trt.openSends.Remove(client)\n\t} else {\n\t\trt.openSends.Set(client, n)\n\t}\n\trt.reblock(client)\n}\n\n// processedSet is a client's send records that were processed (an\n// acknowledgement or a timeout attached) and are not settled by\n// reconciliation, ordered by processing height: the records processed at or\n// after a height are counted in logarithmic work (ADR pr6375 §7.12).\ntype processedSet struct {\n\troot *avl.Node // processedKey -\u003e true\n}\n\nfunc processedKey(height int64, key string) string { return seqKey(height) + \"|\" + key }\n\nfunc (rt *Route) processedOf(client string, create bool) *processedSet {\n\tif v := rt.processed.Get(client); v != nil {\n\t\treturn v.(*processedSet)\n\t}\n\tif !create {\n\t\treturn nil\n\t}\n\tps := \u0026processedSet{}\n\trt.processed.Set(client, ps)\n\treturn ps\n}\n\n// markProcessed adds a record whose outcome was just attached.\nfunc (rt *Route) markProcessed(r *SendRecord) {\n\tclient := clientOfKey(r.Key)\n\tps := rt.processedOf(client, true)\n\tps.root, _ = ps.root.Set(processedKey(r.ProcessedHeight, r.Key), true)\n\trt.reblock(client)\n}\n\n// unmarkProcessed removes a record a reconciliation settled.\nfunc (rt *Route) unmarkProcessed(r *SendRecord) {\n\tif r.Outcome == OutcomePending {\n\t\treturn\n\t}\n\tclient := clientOfKey(r.Key)\n\tif ps := rt.processedOf(client, false); ps != nil {\n\t\tps.root, _, _, _ = ps.root.Remove(processedKey(r.ProcessedHeight, r.Key))\n\t}\n\trt.reblock(client)\n}\n\n// SuspectUnsettled is how many send records of a retired client were\n// processed at or after the replacement's suspect height and are not settled\n// by reconciliation; 0 for a client no replacement retired.\nfunc (rt *Route) SuspectUnsettled(clientID string) int64 {\n\tv := rt.suspect.Get(clientID)\n\tps := rt.processedOf(clientID, false)\n\tif v == nil || ps == nil {\n\t\treturn 0\n\t}\n\tbelow, _, _ := ps.root.Get(seqKey(v.(int64)))\n\treturn int64(ps.root.Size() - below)\n}\n\n// ReopenBlockers is every retired client of the route that still blocks\n// its reopening (a record open, or one processed in its suspect interval\n// and not settled), in client id order: the route-wide check before a\n// reopening, whatever the number of replacements.\nfunc (rt *Route) ReopenBlockers() []string {\n\tvar out []string\n\trt.blocking.Iterate(\"\", \"\", func(k string, _ any) bool {\n\t\tout = append(out, k)\n\t\treturn false\n\t})\n\treturn out\n}\n\n// SuspectFrom is the suspect height the replacement retiring clientID\n// recorded, and whether a replacement retired it from the route.\nfunc (rt *Route) SuspectFrom(clientID string) (int64, bool) {\n\tif v := rt.suspect.Get(clientID); v != nil {\n\t\treturn v.(int64), true\n\t}\n\treturn 0, false\n}\n\n// OpenSends is how many send records of clientID are still open.\nfunc (rt *Route) OpenSends(clientID string) int64 {\n\tif v := rt.openSends.Get(clientID); v != nil {\n\t\treturn v.(int64)\n\t}\n\treturn 0\n}\n\n// release ends a minted send's supply reservation, once: the record owes no\n// more refund (a success, a rejected refund, a reconciliation's settlement),\n// so what was not refunded stays burned. A record released already releases\n// nothing more.\nfunc (rt *Route) release(d *Denom, r *SendRecord) {\n\td.Totals.Returning = mustSub(d.Totals.Returning, r.Reservation)\n\tr.Reservation = 0\n}\n\n// Send is the send record of a packet key, by value (the packet copied), and\n// whether it exists.\nfunc (rt *Route) Send(key string) (SendRecord, bool) {\n\tr := rt.send(key)\n\tif r == nil {\n\t\treturn SendRecord{}, false\n\t}\n\tc := *r\n\tc.Packet = copyPacket(r.Packet)\n\tc.Commitment = append([]byte{}, r.Commitment...)\n\treturn c, true\n}\n\n// SendPayment is the payment state of the send record at key, \"\" if none:\n// Send without copying the record.\nfunc (rt *Route) SendPayment(key string) string {\n\tif r := rt.send(key); r != nil {\n\t\treturn r.Payment\n\t}\n\treturn \"\"\n}\n\nfunc (rt *Route) send(key string) *SendRecord {\n\tv := rt.sends.Get(key)\n\tif v == nil {\n\t\treturn nil\n\t}\n\treturn v.(*SendRecord)\n}\n\n// Settle attaches an acknowledgement's or a timeout's outcome to a send record\n// at the local processing height. A success owes nothing. An error or a\n// timeout owes a refund of the amount minus what a reconciliation already\n// paid on it (the fee is not refunded): paid at once when no refunds hold\n// stands, no refund of the denom is queued ahead, the refund meter holds it\n// and, for an unescrowed denom, the lock (the escrow) covers it; queued\n// otherwise, to be paid by PayRefunds. lock is ignored for a minted denom. It\n// returns what to refund now (0 when queued). A record is settled once.\nfunc (rt *Route) Settle(now, height int64, key, outcome string, hold bool, lock uint256) (uint256, error) {\n\tr := rt.send(key)\n\tif r == nil {\n\t\treturn 0, errors.New(\"controls: no send record for \" + key)\n\t}\n\tif r.Outcome != OutcomePending {\n\t\treturn 0, errors.New(\"controls: send \" + key + \" is settled already (\" + r.Outcome + \")\")\n\t}\n\tif outcome != OutcomeSuccess \u0026\u0026 outcome != OutcomeError \u0026\u0026 outcome != OutcomeTimeout {\n\t\treturn 0, errors.New(\"controls: unknown outcome \" + outcome)\n\t}\n\td := rt.Denom(r.Denom)\n\tdefer rt.track(r, isOpen(r))\n\trt.addPending(r.Key, -1)\n\tr.Outcome, r.ProcessedHeight, r.ProcessedAt = outcome, height, now\n\tif r.Settlement == 0 {\n\t\trt.markProcessed(r)\n\t}\n\tif r.Settlement != 0 {\n\t\treturn 0, nil // a reconciliation settled it: nothing more moves, nothing is paid\n\t}\n\towed := mustSub(r.Amount, r.Refunded)\n\tif outcome == OutcomeSuccess {\n\t\trt.release(d, r)\n\t\treturn 0, nil\n\t}\n\tif owed == 0 {\n\t\treturn 0, nil\n\t}\n\t// A minted refund mints the vouchers again, which nothing bounds; an\n\t// escrowed one needs the lock (the escrow) to cover it.\n\tcovered := d.Config.Kind == KindMint || lock \u003e= owed\n\td.refundMeter.Refill(now, d.Config.RefundCap, d.Config.RefundCap, 0, 0)\n\tif !hold \u0026\u0026 covered \u0026\u0026 d.refundQueue.Size() == 0 \u0026\u0026 d.refundMeter.Level \u003e= owed {\n\t\td.refundMeter.Take(owed)\n\t\trt.refunded(d, r, owed)\n\t\tr.Payment = PayPaid\n\t\treturn owed, nil\n\t}\n\td.queueRefund(key, owed)\n\tr.Payment = PayQueued\n\treturn 0, nil\n}\n\n// queueRefund appends the refund of the send key, amount owed, to the denom's\n// refund queue, indexed by key so it leaves the queue in logarithmic work.\nfunc (d *Denom) queueRefund(key string, amount uint256) {\n\td.refundSeq++\n\tk := seqKey(d.refundSeq)\n\td.refundQueue.Set(k, key)\n\td.refundIdx.Set(key, k)\n\td.Totals.QueuedRefunds = mustAdd(d.Totals.QueuedRefunds, amount)\n}\n\n// refunded books amount refunded on r.\nfunc (rt *Route) refunded(d *Denom, r *SendRecord, amount uint256) {\n\tr.Refunded += amount // at most the record's amount\n\td.Totals.Refunded = statAdd(d.Totals.Refunded, amount)\n\tif d.Config.Kind == KindMint {\n\t\t// The burned vouchers are minted again: back in circulation. They\n\t\t// consume what the send still reserves, and no more: a refund a\n\t\t// reconciliation pays after the reservation was released (a forged\n\t\t// success) mints vouchers no reservation held.\n\t\td.Totals.Outstanding = mustAdd(d.Totals.Outstanding, amount)\n\t\tconsumed := amount\n\t\tif consumed \u003e r.Reservation {\n\t\t\tconsumed = r.Reservation\n\t\t}\n\t\tr.Reservation -= consumed\n\t\td.Totals.Returning = mustSub(d.Totals.Returning, consumed)\n\t}\n}\n\n// RefundPayment is what a refund moves to the sender: Amount out of the\n// denom's escrow (KindUnescrow) or as minted vouchers (KindMint).\ntype RefundPayment struct {\n\tKey    string\n\tDenom  string\n\tTo     address\n\tAmount uint256\n}\n\n// PayRefunds pays up to n queued refunds of a denom, oldest first, as the\n// refund meter allows (ADR pr6375 §7.13): a refund is paid in installments,\n// each at least min(what it still owes, an hour of the refund cap), and the\n// queue stops at the first refund the meter, or for an unescrowed denom the\n// lock (the escrow), cannot serve. A refunds hold pays nothing. It books the payments and returns them.\nfunc (rt *Route) PayRefunds(now int64, denom string, n int, hold bool, lock uint256) ([]RefundPayment, error) {\n\td := rt.Denom(denom)\n\tif d == nil {\n\t\treturn nil, errors.New(\"controls: denom \" + denom + \" is not enabled on route \" + rt.Key)\n\t}\n\tif n \u003c 1 {\n\t\treturn nil, errors.New(\"controls: n must be positive\")\n\t}\n\tif n \u003e MaxClaims {\n\t\tn = MaxClaims\n\t}\n\tif hold {\n\t\treturn nil, nil\n\t}\n\td.refundMeter.Refill(now, d.Config.RefundCap, d.Config.RefundCap, 0, 0)\n\t// The escrow proven refunds reserve is theirs: a queued refund never\n\t// takes it.\n\tlock = sub0(lock, d.Totals.ProvenOwed)\n\tvar out []RefundPayment\n\tfor len(out) \u003c n {\n\t\tk, key, ok := d.headRefund()\n\t\tif !ok {\n\t\t\tbreak\n\t\t}\n\t\tr := rt.send(key)\n\t\trem := mustSub(r.Amount, r.Refunded)\n\t\tneed := d.Config.RefundCap / 24\n\t\tif need == 0 {\n\t\t\tneed = 1\n\t\t}\n\t\tif need \u003e rem {\n\t\t\tneed = rem\n\t\t}\n\t\tif d.refundMeter.Level \u003c need {\n\t\t\tbreak\n\t\t}\n\t\tpay := rem\n\t\tif pay \u003e d.refundMeter.Level {\n\t\t\tpay = d.refundMeter.Level\n\t\t}\n\t\tif d.Config.Kind == KindUnescrow {\n\t\t\tif pay \u003e lock {\n\t\t\t\tpay = lock\n\t\t\t}\n\t\t\tif pay \u003c need {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tlock -= pay\n\t\t}\n\t\td.refundMeter.Take(pay)\n\t\twas := isOpen(r)\n\t\trt.refunded(d, r, pay)\n\t\td.Totals.QueuedRefunds = mustSub(d.Totals.QueuedRefunds, pay)\n\t\tif was {\n\t\t\t// What the open record may still owe shrinks at once.\n\t\t\td.Totals.InFlight = mustSub(d.Totals.InFlight, pay)\n\t\t\tr.inFlight = mustSub(r.inFlight, pay)\n\t\t}\n\t\tif r.Refunded == r.Amount {\n\t\t\td.refundQueue.Remove(k)\n\t\t\td.refundIdx.Remove(key)\n\t\t\tr.Payment = PayClaimed\n\t\t\tif r.Settlement != 0 {\n\t\t\t\tr.Payment = PayReconciled // a settlement's parked refund\n\t\t\t}\n\t\t\trt.track(r, was)\n\t\t}\n\t\tout = append(out, RefundPayment{Key: key, Denom: denom, To: r.Sender, Amount: pay})\n\t}\n\treturn out, nil\n}\n\nfunc (d *Denom) headRefund() (string, string, bool) {\n\tvar k, key string\n\tfound := false\n\td.refundQueue.Iterate(\"\", \"\", func(kk string, v any) bool {\n\t\tk, key, found = kk, v.(string), true\n\t\treturn true\n\t})\n\treturn k, key, found\n}\n\n// QueuedRefundCount is how many refunds of a denom wait.\nfunc (rt *Route) QueuedRefundCount(denom string) int {\n\td := rt.Denom(denom)\n\tif d == nil {\n\t\treturn 0\n\t}\n\treturn d.refundQueue.Size()\n}\n\n// Settlement actions of a reconciliation (ADR pr6375 §5.4 kind 12, §7.12),\n// one per send record, the same numbers as on Ethereum.\nconst (\n\tSettlePay           = uint8(1) // a genuine refund: pay its whole remainder, Amount = what the packet carried minus what was refunded (0 if paid in full)\n\tSettleReject        = uint8(2) // reject a queued refund, none of it paid: the packet was delivered\n\tSettleDelivered     = uint8(3) // the packet was delivered, no refund paid: settled, nothing owed (a queued refund is rejected)\n\tSettleLoss          = uint8(4) // a forged refund: the unpaid remainder rejected, what was paid (Amount) recorded as a loss\n\tSettleForgedReceive = uint8(5) // a forged receive: Seq is an inbound sequence; its claim is forged (remainder dropped), what it paid out (Amount) a loss, once\n)\n\n// Proven outcomes of a send, read from its destination through the route's\n// replacement client (ADR pr6375 §7.12 \"Proven outcomes\"): a settlement's\n// action must match.\nconst (\n\tProvenNone       = uint8(0)\n\tProvenDelivered  = uint8(1) // the receipt is the packet's and its acknowledgement a success\n\tProvenRefundable = uint8(2) // no receipt, or the packet's receipt with an error acknowledgement\n\tProvenCollision  = uint8(3) // another packet's receipt occupies the sequence: the packet is refundable\n)\n\n// CauseForgedReceive is the cause of a forged receive's loss.\nconst CauseForgedReceive = \"forged-receive\"\n\n// SettleItem is one reconciliation outcome: the send at Seq of the settled\n// client.\ntype SettleItem struct {\n\tSeq    uint64\n\tAction uint8\n\tAmount uint256\n}\n\n// SettleRecords applies a reconciliation's outcomes to the send records of\n// client, a client a replacement retired from the route (ADR pr6375 §7.12):\n// every record named is settled exactly once, after which no acknowledgement,\n// timeout or decision moves it again. A queued refund leaves the queue under\n// any action, paid only by SettlePay. lock is the escrow of an unescrowed\n// denom: payments never take what open claims reserve; a genuine refund the\n// free escrow cannot pay is parked, owed and queued, not refused. Every item is checked\n// before anything changes. It returns the refunds to pay. trusted reports\n// whether a verifier still vouches for what it proved under a trust\n// generation: Active, not retired, and that generation still current.\nfunc (rt *Route) SettleRecords(client string, items []SettleItem, incident string, lock func(denom string) uint256, trusted func(client string, gen uint64) bool) ([]RefundPayment, error) {\n\tplan, err := rt.PlanSettlement(client, items, incident, lock, trusted)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn rt.ApplySettlement(plan), nil\n}\n\n// SettlePlan is a checked settlement, not applied yet: the refunds it pays\n// at once (Payments) and the refunds it parks. It holds for the\n// state it was planned on: apply it in the same transaction, nothing\n// changed in between.\ntype SettlePlan struct {\n\tclient, incident string\n\titems            []SettleItem\n\tparked           map[uint64]bool // outbound sequences whose refund is parked\n\t// Payments are the refunds the settlement pays at once, in item order.\n\tPayments []RefundPayment\n}\n\n// Parked lists the outbound sequences whose refund the plan parks.\nfunc (p *SettlePlan) Parked() []uint64 {\n\tvar out []uint64\n\tfor _, it := range p.items {\n\t\tif p.parked[it.Seq] \u0026\u0026 it.Action == SettlePay {\n\t\t\tout = append(out, it.Seq)\n\t\t}\n\t}\n\treturn out\n}\n\n// PlanSettlement checks a reconciliation's outcomes (see SettleRecords) and\n// plans them without changing anything: which genuine refunds are paid at\n// once and which are parked. An escrowed denom's refunds of the batch are\n// paid only all together, when the free escrow once the batch applies backs\n// them all; otherwise they all park, so no payment spends a sibling's\n// backing whatever the item order. A minted denom's refunds are always paid:\n// the vouchers they mint again are bounded by nothing (the supply cap\n// governs admission only), so none is ever parked.\nfunc (rt *Route) PlanSettlement(client string, items []SettleItem, incident string, lock func(denom string) uint256, trusted func(client string, gen uint64) bool) (*SettlePlan, error) {\n\tif err := rt.checkPrepared(); err != nil {\n\t\treturn nil, err\n\t}\n\t// Indexed: every client a replacement retired has its suspect height\n\t// recorded, read in logarithmic work whatever the route's history.\n\tif !rt.retiredHere(client) {\n\t\treturn nil, errors.New(\"controls: \" + client + \" is no client a replacement retired from route \" + rt.Key + \": only its sends are settled by reconciliation\")\n\t}\n\tif len(items) == 0 || len(items) \u003e MaxKeys {\n\t\treturn nil, ufmt.Errorf(\"controls: settle 1 to %d sends\", MaxKeys)\n\t}\n\tif incident == \"\" {\n\t\treturn nil, errors.New(\"controls: a settlement names its incident\")\n\t}\n\tseen := map[uint64]bool{}\n\tseenIn := map[uint64]bool{}\n\t// What the batch takes off each liability that reserves the escrow,\n\t// whatever the items' order: every settled record's queued refund (each\n\t// action unqueues it), and every forged receive's unpaid claim remainder\n\t// (owed or withheld). The payments are checked against the escrow the\n\t// liabilities still reserve once the whole batch applies; each is exact\n\t// on its own, so the subtraction happens before any saturating sum.\n\t// Each release is within its total (a queued refund within\n\t// QueuedRefunds, a proven reservation within ProvenOwed, a claim's\n\t// remainder within Owed or Withheld), so the sums never overflow.\n\trelQueued, relProven := map[string]uint256{}, map[string]uint256{}\n\trelOwed, relWithheld := map[string]uint256{}, map[string]uint256{}\n\t// The losses the batch books (forged refunds and receives), summed per\n\t// denom, must fit the ledger's balances with every earlier loss (lossesFit).\n\tlosses := map[string]uint256{}\n\tfor i, it := range items {\n\t\tkey := PacketKey(client, it.Seq)\n\t\tif it.Action == SettleForgedReceive {\n\t\t\tif it.Seq == 0 || seenIn[it.Seq] {\n\t\t\t\treturn nil, ufmt.Errorf(\"controls: item %d: no sequence, or named twice\", i)\n\t\t\t}\n\t\t\tseenIn[it.Seq] = true\n\t\t\tdenom, paid, err := rt.forgedReceive(key)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, ufmt.Errorf(\"controls: item %d: %v\", i, err)\n\t\t\t}\n\t\t\tif it.Amount == 0 || it.Amount != paid {\n\t\t\t\treturn nil, ufmt.Errorf(\"controls: item %d: the receive %s paid out %s, not %s\", i, key, u(paid), u(it.Amount))\n\t\t\t}\n\t\t\tif err := rt.checkIncidentKind(denom, incident, false); err != nil {\n\t\t\t\treturn nil, ufmt.Errorf(\"controls: item %d: %v\", i, err)\n\t\t\t}\n\t\t\tif !addLossTo(losses, denom, paid) {\n\t\t\t\treturn nil, ufmt.Errorf(\"controls: item %d: %v\", i, errLossOverflow)\n\t\t\t}\n\t\t\tif c := rt.claim(key); c != nil {\n\t\t\t\tswitch c.State {\n\t\t\t\tcase Open:\n\t\t\t\t\trelOwed[denom] = mustAdd(relOwed[denom], c.Remaining()) // within Owed\n\t\t\t\tcase Withheld:\n\t\t\t\t\trelWithheld[denom] = mustAdd(relWithheld[denom], c.Remaining()) // within Withheld\n\t\t\t\t}\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tr := rt.send(key)\n\t\tswitch {\n\t\tcase it.Seq == 0 || seen[it.Seq]:\n\t\t\treturn nil, ufmt.Errorf(\"controls: item %d: no sequence, or named twice\", i)\n\t\tcase r == nil:\n\t\t\treturn nil, ufmt.Errorf(\"controls: item %d: no send record for %s\", i, key)\n\t\tcase r.Settlement != 0:\n\t\t\treturn nil, ufmt.Errorf(\"controls: item %d: send %s is settled already\", i, key)\n\t\t}\n\t\tseen[it.Seq] = true\n\t\trelProven[r.Denom] = mustAdd(relProven[r.Denom], r.ProvenReserve) // within ProvenOwed\n\t\tif r.Payment == PayQueued {\n\t\t\trelQueued[r.Denom] = mustAdd(relQueued[r.Denom], mustSub(r.Amount, r.Refunded)) // within QueuedRefunds\n\t\t}\n\t\t// The action must match the outcome proven through the replacement\n\t\t// client (ADR pr6375 §7.12): governance chooses, the proof decides.\n\t\tswitch {\n\t\tcase r.Proven != ProvenNone \u0026\u0026 !trusted(r.ProvenBy, r.ProvenGen):\n\t\t\treturn nil, ufmt.Errorf(\"controls: item %d: send %s's outcome was proven through %s under trust generation %d, no longer Active or current: prove it again\", i, key, r.ProvenBy, r.ProvenGen)\n\t\tcase (it.Action == SettlePay) \u0026\u0026 r.Proven != ProvenRefundable \u0026\u0026 r.Proven != ProvenCollision,\n\t\t\t(it.Action == SettleReject || it.Action == SettleDelivered || it.Action == SettleLoss) \u0026\u0026 r.Proven != ProvenDelivered:\n\t\t\treturn nil, ufmt.Errorf(\"controls: item %d: action %d does not match send %s's proven outcome (%d)\", i, it.Action, key, r.Proven)\n\t\t}\n\t\tswitch it.Action {\n\t\tcase SettlePay:\n\t\t\t// A genuine refund: its whole remainder, never cut short (ADR\n\t\t\t// pr6375 §7.9); zero for one paid in full already, which only\n\t\t\t// settles the record.\n\t\t\tif rem := mustSub(r.Amount, r.Refunded); it.Amount != rem {\n\t\t\t\treturn nil, ufmt.Errorf(\"controls: item %d: a refund pays send %s's whole remainder (%s), not %s\", i, key, u(rem), u(it.Amount))\n\t\t\t}\n\t\t\tif it.Amount == 0 {\n\t\t\t\tbreak // paid in full already: settles the record, moves nothing\n\t\t\t}\n\t\tcase SettleReject:\n\t\t\tif r.Payment != PayQueued || it.Amount != 0 {\n\t\t\t\treturn nil, ufmt.Errorf(\"controls: item %d: send %s has no queued refund, or names an amount\", i, key)\n\t\t\t}\n\t\t\tif r.Refunded \u003e 0 {\n\t\t\t\treturn nil, ufmt.Errorf(\"controls: item %d: send %s was refunded in part already: a delivered packet's paid refund is a forged refund (action 4)\", i, key)\n\t\t\t}\n\t\tcase SettleDelivered:\n\t\t\tif it.Amount != 0 {\n\t\t\t\treturn nil, ufmt.Errorf(\"controls: item %d: a delivered send names no amount\", i)\n\t\t\t}\n\t\t\tif r.Refunded \u003e 0 {\n\t\t\t\treturn nil, ufmt.Errorf(\"controls: item %d: send %s was refunded: a delivered packet's paid refund is a forged refund (action 4)\", i, key)\n\t\t\t}\n\t\tcase SettleLoss:\n\t\t\t// A forged refund: what was paid is the loss, the remainder is\n\t\t\t// rejected.\n\t\t\tif r.Refunded == 0 || it.Amount != r.Refunded {\n\t\t\t\treturn nil, ufmt.Errorf(\"controls: item %d: a forged refund's loss is what send %s was refunded (%s), not %s\", i, key, u(r.Refunded), u(it.Amount))\n\t\t\t}\n\t\t\tif rt.LossBooked(DirSend, key) {\n\t\t\t\treturn nil, ufmt.Errorf(\"controls: item %d: the loss of %s is recorded already\", i, key)\n\t\t\t}\n\t\t\tif err := rt.checkIncidentKind(r.Denom, incident, false); err != nil {\n\t\t\t\treturn nil, ufmt.Errorf(\"controls: item %d: %v\", i, err)\n\t\t\t}\n\t\t\tif !addLossTo(losses, r.Denom, it.Amount) {\n\t\t\t\treturn nil, ufmt.Errorf(\"controls: item %d: %v\", i, errLossOverflow)\n\t\t\t}\n\t\tdefault:\n\t\t\treturn nil, ufmt.Errorf(\"controls: item %d: unknown action %d\", i, it.Action)\n\t\t}\n\t}\n\tif err := rt.lossesFit(losses, incident); err != nil {\n\t\treturn nil, err\n\t}\n\tplan := \u0026SettlePlan{client: client, incident: incident, items: items, parked: map[uint64]bool{}}\n\t// Every genuine refund of the batch is a liability before any is paid:\n\t// an escrowed one is paid only if the escrow, less what the liabilities\n\t// outside the batch still reserve, backs all of the batch's escrowed\n\t// refunds of its denom at once; otherwise all of them park. Paying one\n\t// lowers the escrow and the liabilities alike, so no payment ever leaves\n\t// a sibling, paid later, parked or reserved, with less backing than it\n\t// had, whatever the item order (as settled alone, each would park too).\n\t// The total is exact: past 2^256 no escrow can back it, and the denom's\n\t// refunds all park. A minted refund is paid whatever the batch holds. No\n\t// settlement is refused for want of backing.\n\tbatch, over := map[string]uint256{}, map[string]bool{}\n\tfor _, it := range items {\n\t\tif it.Action != SettlePay || it.Amount == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tr := rt.send(PacketKey(client, it.Seq))\n\t\tif rt.Denom(r.Denom).Config.Kind != KindMint {\n\t\t\ts, ok := checkedAdd(batch[r.Denom], it.Amount)\n\t\t\tif !ok {\n\t\t\t\tover[r.Denom] = true\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tbatch[r.Denom] = s\n\t\t}\n\t}\n\tbacked := map[string]bool{}\n\tfor _, it := range items {\n\t\tif it.Action != SettlePay || it.Amount == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tr := rt.send(PacketKey(client, it.Seq))\n\t\td := rt.Denom(r.Denom)\n\t\tif d.Config.Kind != KindMint {\n\t\t\tif _, ok := backed[r.Denom]; !ok {\n\t\t\t\treserved := boundAdd(boundAdd(boundAdd(mustSub(d.Totals.Owed, relOwed[r.Denom]), mustSub(d.Totals.Withheld, relWithheld[r.Denom])),\n\t\t\t\t\tmustSub(d.Totals.QueuedRefunds, relQueued[r.Denom])), mustSub(d.Totals.ProvenOwed, relProven[r.Denom]))\n\t\t\t\t// What is left once the liabilities outside the batch are\n\t\t\t\t// reserved must back every refund of the batch.\n\t\t\t\tl := lock(r.Denom)\n\t\t\t\tbacked[r.Denom] = !over[r.Denom] \u0026\u0026 l \u003e= reserved \u0026\u0026 l-reserved \u003e= batch[r.Denom]\n\t\t\t}\n\t\t\tif !backed[r.Denom] {\n\t\t\t\tplan.parked[it.Seq] = true\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tplan.Payments = append(plan.Payments, RefundPayment{Key: r.Key, Denom: r.Denom, To: r.Sender, Amount: it.Amount})\n\t}\n\treturn plan, nil\n}\n\n// ApplySettlement applies a settlement PlanSettlement checked, in the same\n// transaction: it settles every record, books the losses, parks the refunds\n// the plan parks, and returns the refunds to pay now (the plan's Payments).\nfunc (rt *Route) ApplySettlement(plan *SettlePlan) []RefundPayment {\n\tclient, incident := plan.client, plan.incident\n\t// First every settled send releases what it holds, its supply\n\t// reservation and its queued refund, before anything is added.\n\twas := map[uint64]bool{}\n\tfor _, it := range plan.items {\n\t\tif it.Action == SettleForgedReceive {\n\t\t\tcontinue\n\t\t}\n\t\tr := rt.send(PacketKey(client, it.Seq))\n\t\td := rt.Denom(r.Denom)\n\t\twas[it.Seq] = isOpen(r)\n\t\trt.unmarkProcessed(r)\n\t\tif r.Payment == PayQueued {\n\t\t\trt.unqueue(d, r)\n\t\t\tr.Payment = PayRejected\n\t\t}\n\t\trt.release(d, r)\n\t\td.Totals.ProvenOwed = mustSub(d.Totals.ProvenOwed, r.ProvenReserve)\n\t\tr.ProvenReserve = 0\n\t}\n\t// Then the parked refunds are added.\n\tfor _, it := range plan.items {\n\t\tif it.Action == SettlePay \u0026\u0026 it.Amount \u003e 0 \u0026\u0026 plan.parked[it.Seq] {\n\t\t\tr := rt.send(PacketKey(client, it.Seq))\n\t\t\tr.Settlement = it.Action\n\t\t\trt.park(rt.Denom(r.Denom), r, it.Amount)\n\t\t}\n\t}\n\t// Then the records, the losses and the refunds paid now.\n\tvar out []RefundPayment\n\tfor _, it := range plan.items {\n\t\tkey := PacketKey(client, it.Seq)\n\t\tif it.Action == SettleForgedReceive {\n\t\t\tdenom, _, _ := rt.forgedReceive(key)\n\t\t\td := rt.Denom(denom)\n\t\t\tif c := rt.claim(key); c != nil {\n\t\t\t\t// The claim is forged: its unpaid remainder is dropped and\n\t\t\t\t// it leaves its queue, so nothing more of it is paid.\n\t\t\t\trem := c.Remaining()\n\t\t\t\tswitch c.State {\n\t\t\t\tcase Open:\n\t\t\t\t\td.Totals.Owed = mustSub(d.Totals.Owed, rem)\n\t\t\t\t\trt.dequeue(d, c, d.queues.Get(string(c.To)).(*queue))\n\t\t\t\tcase Withheld:\n\t\t\t\t\td.Totals.Withheld = mustSub(d.Totals.Withheld, rem)\n\t\t\t\t}\n\t\t\t\tc.State = Forged\n\t\t\t} else if e := rt.sweep(key); e != nil \u0026\u0026 !e.Paid {\n\t\t\t\t// A claim moved to the sweep queue: its live remainder is\n\t\t\t\t// dropped, so no decision pays it.\n\t\t\t\td.Totals.Swept = mustSub(d.Totals.Swept, e.Amount)\n\t\t\t\te.Voided = true\n\t\t\t}\n\t\t\trt.addLoss(d, Loss{Key: key, Direction: DirRecv, Denom: denom, Amount: it.Amount, Chain: ChainGno, Incident: incident, Cause: CauseForgedReceive})\n\t\t\tcontinue\n\t\t}\n\t\tr := rt.send(key)\n\t\td := rt.Denom(r.Denom)\n\t\tswitch it.Action {\n\t\tcase SettlePay:\n\t\t\tif it.Amount \u003e 0 \u0026\u0026 !plan.parked[it.Seq] {\n\t\t\t\trt.refunded(d, r, it.Amount)\n\t\t\t\tr.Payment = PayReconciled\n\t\t\t\tout = append(out, RefundPayment{Key: key, Denom: r.Denom, To: r.Sender, Amount: it.Amount})\n\t\t\t}\n\t\tcase SettleLoss:\n\t\t\trt.addLoss(d, Loss{Key: key, Direction: DirSend, Denom: r.Denom, Amount: it.Amount, Chain: ChainGno, Incident: incident, Cause: CauseRefundForDelivered})\n\t\t}\n\t\tr.Settlement = it.Action\n\t\tr.Incident = incident\n\t\trt.track(r, was[it.Seq])\n\t}\n\treturn out\n}\n\n// park books a settled genuine refund of an escrowed denom whose free escrow\n// is short. The record is settled, so it no longer blocks a reopening, and\n// its refund is owed and queued (PayQueued, counted in QueuedRefunds), paid\n// later by PayRefunds within the free escrow, the refund meter and any\n// refunds hold, like every queued refund. It is reserved by QueuedRefunds\n// itself: no claim, sweep or receive takes the escrow it waits for. A minted\n// refund is never parked: nothing bounds the vouchers it mints again.\nfunc (rt *Route) park(d *Denom, r *SendRecord, amount uint256) {\n\td.queueRefund(r.Key, amount)\n\tr.Payment = PayQueued\n}\n\n// forgedReceive is the denom and what the receive of key (an inbound\n// sequence of a retired client) paid out: its claim's installments (fee\n// included), or the sweep entry a decision paid. A receive whose forged void\n// booked its loss already, or whose loss is booked, is refused: once.\nfunc (rt *Route) forgedReceive(key string) (string, uint256, error) {\n\tif rt.LossBooked(DirRecv, key) {\n\t\treturn \"\", 0, errors.New(\"the loss of receive \" + key + \" is booked already\")\n\t}\n\tif c := rt.claim(key); c != nil {\n\t\tif c.State == Forged {\n\t\t\treturn \"\", 0, errors.New(\"receive \" + key + \" was voided as forged: its loss is booked\")\n\t\t}\n\t\tif c.Paid == 0 {\n\t\t\treturn \"\", 0, errors.New(\"receive \" + key + \" paid nothing out: void it instead\")\n\t\t}\n\t\treturn c.Denom, c.Paid, nil\n\t}\n\tif e := rt.sweep(key); e != nil \u0026\u0026 !e.Voided {\n\t\tif paid := e.paidOut(); paid \u003e 0 {\n\t\t\treturn e.Denom, paid, nil\n\t\t}\n\t}\n\treturn \"\", 0, errors.New(\"no receive of \" + key + \" paid anything out\")\n}\n\n// ProveOutcome stores the destination's outcome of the send of key, proven by\n// the caller through the route's replacement client at height (ADR pr6375\n// §7.12): packet must be the record's (its commitment), its client one a\n// replacement retired, the record unsettled, and height not below an earlier\n// proof's (a later state of the destination supersedes an earlier one, never\n// the other way round). gen is verifier's trust generation: the outcome holds\n// only while trusted(verifier, gen) (see SettleRecords).\nfunc (rt *Route) ProveOutcome(key string, packet types.Packet, outcome uint8, height uint64, verifier string, gen uint64, trusted func(client string, gen uint64) bool) error {\n\tr := rt.send(key)\n\tif r == nil {\n\t\treturn errors.New(\"controls: no send record for \" + key)\n\t}\n\tif string(types.CommitPacket(packet)) != string(r.Commitment) || key != PacketKey(packet.SourceClient, packet.Sequence) {\n\t\treturn errors.New(\"controls: the packet is not send \" + key + \"'s\")\n\t}\n\tif !rt.retiredHere(packet.SourceClient) {\n\t\treturn errors.New(\"controls: \" + packet.SourceClient + \" is no client a replacement retired from route \" + rt.Key)\n\t}\n\tif r.Settlement != 0 {\n\t\treturn errors.New(\"controls: send \" + key + \" is settled already\")\n\t}\n\tif outcome \u003c ProvenDelivered || outcome \u003e ProvenCollision {\n\t\treturn errors.New(\"controls: unknown outcome\")\n\t}\n\t// A proven outcome is final (an acknowledgement, a receipt, an absence\n\t// past the timeout): it is proven again only once its verifier no longer\n\t// vouches for it (not Active, or a new trust generation), which voids it.\n\tif r.Proven != ProvenNone \u0026\u0026 trusted(r.ProvenBy, r.ProvenGen) {\n\t\treturn ufmt.Errorf(\"controls: send %s's outcome is proven already\", key)\n\t}\n\t// A refund proven owed on a send whose forged success released what it\n\t// held is reserved again at once, until its settlement: another\n\t// settlement, payout or credit in between never takes its backing. A\n\t// proof never releases a reservation, a delivery included: a proof can\n\t// be voided later (its verifier frozen, recovered, replaced) and proven\n\t// otherwise, so only the settlement that applies an outcome valid then\n\t// releases it (ApplySettlement).\n\td := rt.Denom(r.Denom)\n\tif r.ProvenReserve == 0 \u0026\u0026 hiddenRefund(r, d) \u0026\u0026 (outcome == ProvenRefundable || outcome == ProvenCollision) {\n\t\tr.ProvenReserve = mustSub(r.Amount, r.Refunded)\n\t\td.Totals.ProvenOwed = mustAdd(d.Totals.ProvenOwed, r.ProvenReserve)\n\t}\n\tr.Proven, r.ProvenAt, r.ProvenBy, r.ProvenGen = outcome, height, verifier, gen\n\treturn nil\n}\n\n// unqueue takes a record's queued refund out of the queue, unpaid, found by\n// its key in logarithmic work.\nfunc (rt *Route) unqueue(d *Denom, r *SendRecord) {\n\tif v := d.refundIdx.Get(r.Key); v != nil {\n\t\td.refundQueue.Remove(v.(string))\n\t\td.refundIdx.Remove(r.Key)\n\t}\n\td.Totals.QueuedRefunds = mustSub(d.Totals.QueuedRefunds, mustSub(r.Amount, r.Refunded))\n}\n\n// hiddenRefund reports whether r, a record of a retired client, may owe a\n// refund nothing reserves: a success a forged acknowledgement may have\n// released, or an escrowed send still pending (its escrow is reserved by no\n// total; a minted one keeps its supply reservation).\nfunc hiddenRefund(r *SendRecord, d *Denom) bool {\n\treturn r.Outcome == OutcomeSuccess || r.Outcome == OutcomePending \u0026\u0026 d.Config.Kind == KindUnescrow\n}\n\n// Prepared reports whether every client a replacement retired from the\n// route had its records walked by PrepareReplacement: until then no\n// settlement, sweep payment, credit by vote or reopening applies.\nfunc (rt *Route) Prepared() bool { return rt.prepIdx \u003e= len(rt.Replaced) }\n\nfunc (rt *Route) checkPrepared() error {\n\tif !rt.Prepared() {\n\t\treturn errors.New(\"controls: route \" + rt.Key + \"'s replacement is not prepared: its suspect records' refunds are not all reserved yet (PrepareReplacement)\")\n\t}\n\treturn nil\n}\n\n// PrepareReplacement walks up to n (1 to MaxKeys) send records of the\n// clients replacements retired from the route, from where the last call\n// stopped, and reserves every refund one of them may still owe and nothing\n// reserves (ADR pr6375 §7.12): a success processed at or after its client's\n// suspect height (a forged acknowledgement may have released what it held),\n// and an escrowed send still pending. Each is reserved in ProvenOwed until\n// its settlement releases it (a proof, of a delivery too, never does), so no\n// payment for the route takes the backing a sibling's refund needs, whatever\n// the order its outcomes are proven in. Every record visited, and every move\n// to the next retired client, takes a step of n; once n are taken, one more\n// move past a client whose records the batch has just finished may follow.\n// Anyone may call it; it\n// returns how many it reserved and whether the walk is complete.\nfunc (rt *Route) PrepareReplacement(n int) (int, bool, error) {\n\tif n \u003c 1 || n \u003e MaxKeys {\n\t\treturn 0, false, ufmt.Errorf(\"controls: prepare 1 to %d records a call\", MaxKeys)\n\t}\n\treserved, visited := 0, 0\n\tfor visited \u003c n \u0026\u0026 rt.prepIdx \u003c len(rt.Replaced) {\n\t\tclient := rt.Replaced[rt.prepIdx]\n\t\tvar h int64\n\t\tif v := rt.suspect.Get(client); v != nil {\n\t\t\th = v.(int64)\n\t\t}\n\t\tstart := client + \"/\"\n\t\tif rt.prepCursor != \"\" {\n\t\t\tstart = after(rt.prepCursor)\n\t\t}\n\t\tlast := \"\"\n\t\tmore := rt.sends.Iterate(start, client+\"0\", func(k string, v any) bool { // '0' follows '/'\n\t\t\tif visited == n {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tvisited++\n\t\t\tlast = k\n\t\t\tif rt.reserveHidden(v.(*SendRecord), h) {\n\t\t\t\treserved++\n\t\t\t}\n\t\t\treturn false\n\t\t})\n\t\tif more {\n\t\t\trt.prepCursor = last\n\t\t\tbreak\n\t\t}\n\t\t// Moving to the next retired client takes a step of the budget too:\n\t\t// a long run of clients with no records stays bounded per call.\n\t\trt.prepIdx++\n\t\trt.prepCursor = \"\"\n\t\tvisited++\n\t}\n\treturn reserved, rt.Prepared(), nil\n}\n\n// reserveHidden reserves the refund r may owe, if it is a hidden one of the\n// suspect interval starting at h and nothing decided it yet.\nfunc (rt *Route) reserveHidden(r *SendRecord, h int64) bool {\n\td := rt.Denom(r.Denom)\n\tif r.Settlement != 0 || r.ProvenReserve != 0 || !hiddenRefund(r, d) {\n\t\treturn false\n\t}\n\tif r.Outcome == OutcomeSuccess \u0026\u0026 r.ProcessedHeight \u003c h {\n\t\treturn false // processed before the suspect interval: trusted\n\t}\n\towed := mustSub(r.Amount, r.Refunded)\n\tif owed == 0 {\n\t\treturn false\n\t}\n\tr.ProvenReserve = owed\n\td.Totals.ProvenOwed = mustAdd(d.Totals.ProvenOwed, owed)\n\treturn true\n}\n\n// retiredHere reports whether a replacement retired client from the route:\n// an indexed lookup (its suspect height is recorded at the replacement).\nfunc (rt *Route) retiredHere(client string) bool { return rt.suspect.Has(client) }\n\n// HasClient reports whether clientID is or was a client of the route: its\n// current client, or one a replacement retired from it (an indexed lookup,\n// in logarithmic work whatever the number of replacements).\nfunc (rt *Route) HasClient(clientID string) bool {\n\treturn clientID == rt.Client || rt.retiredHere(clientID)\n}\n"},{"name":"route.gno","body":"// Package controls is the state of the safety controls an ICS-20 transfer\n// app keeps for its Ethereum routes (ADR pr6375 §7.1, §7.8, §7.9, §7.12,\n// §7.13): per route and denom, the outflow meters and the claim queue that\n// pays credits from Ethereum, the sweep queue, the send limits, the immutable\n// per-packet send records with the refund meter and the queued refunds, the\n// voids and restores, and the loss ledger with its recapitalizations,\n// reductions and accepted deficits.\n//\n// It holds no coins and makes no call: the transfer implementation moves\n// value as the Payment values returned here say, through the transfer proxy's\n// services, and decides what a refusal means (an error acknowledgement before\n// any state change, or a spent decision). Every function validates first and\n// mutates only once nothing can fail, so an error leaves the state untouched.\n// Clocks are unix seconds and block heights, passed in.\n//\n// A Registry lives in the transfer proxy's storage, allocated there through\n// NewRegistryAny; only the proxy's gated accessor hands out the pointer, to\n// the active implementation. Other realms read it through a Reader, which\n// exposes no mutator.\n//\n// The meter, claim, installment and void algorithms are ported from\n// gnolang/bridges (branch ethereum-draft, commit a58efe81c9eb):\n// solana/realm/p/hlroute/v0/hlroute.gno (Meter, Credit, Claim, void,\n// restore) and ethereum/realm/hlgnot/installments.gno (no slivers),\n// Copyright (C) 2026 NewTendermint, LLC, under the GNO Network General Public\n// License. They are adapted to IBC: claims are keyed by packet, a route holds\n// several denoms with two accountings (unescrow and mint), and refunds have\n// their own meter.\npackage controls\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/aib/encoding/ethaddr/v0\"\n\n\t\"gno.land/p/aib/ibc/host/v0\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nconst (\n\t// Day is a day of block time, in seconds.\n\tDay = int64(86400)\n\t// MaxClaims is the most claims one Claim call pays (ADR pr6375 §7.1).\n\tMaxClaims = 20\n\t// ReopenBatch is how many incidents, then authorizations, one batch of\n\t// a reopening looks at.\n\tReopenBatch = 64\n\t// MaxKeys is the most packet keys one decision names.\n\tMaxKeys = 64\n\t// PageLines is how many entries a page lists.\n\tPageLines = 100\n\t// MaxOpenRecaps is the most unpaid recapitalization authorizations an\n\t// incident holds on a denom at once: a reduction revokes among them.\n\tMaxOpenRecaps = 64\n)\n\n// Claim states.\nconst (\n\tOpen     = \"open\"     // credited, unpaid or paying\n\tWithheld = \"withheld\" // voided as withheld: the remainder stays booked, restorable\n\tForged   = \"forged\"   // voided as forged: the remainder dropped, the paid part lost\n\tDone     = \"done\"     // paid in full\n)\n\n// Who credited a claim.\nconst (\n\tByPacket = \"packet\" // a received packet\n\tByVote   = \"vote\"   // a credit-by-vote decision\n)\n\n// PacketKey is the key of a packet on a client: the client id, \"/\", and the\n// sequence in 20 decimal digits, so keys sort by sequence.\nfunc PacketKey(clientID string, sequence uint64) string {\n\ts := strconv.FormatUint(sequence, 10)\n\treturn clientID + \"/\" + strings.Repeat(\"0\", 20-len(s)) + s\n}\n\n// ParsePacketKey splits a packet key; ok is false for anything else,\n// including a client part that is not a valid client identifier.\nfunc ParsePacketKey(key string) (clientID string, sequence uint64, ok bool) {\n\ti := strings.LastIndex(key, \"/\")\n\tif i \u003c= 0 || len(key)-i-1 != 20 {\n\t\treturn \"\", 0, false\n\t}\n\tif host.ClientIdentifierValidator(key[:i]) != nil {\n\t\treturn \"\", 0, false\n\t}\n\tseq, err := strconv.ParseUint(key[i+1:], 10, 64)\n\tif err != nil || seq == 0 {\n\t\treturn \"\", 0, false\n\t}\n\treturn key[:i], seq, true\n}\n\n// clientOfKey is the client id of key, the key of a stored send record:\n// RecordSend validated it (ParsePacketKey) before writing the record.\nfunc clientOfKey(key string) string { return key[:strings.LastIndex(key, \"/\")] }\n\nfunc seqKey(n int64) string {\n\ts := strconv.FormatInt(n, 10)\n\treturn strings.Repeat(\"0\", 19-len(s)) + s\n}\n\nfunc itoa(n int64) string { return strconv.FormatInt(n, 10) }\n\n// ---------------------------------------------------------------- registry\n\n// Registry holds the controlled routes and the client aliases: a route is\n// keyed by a stable route key (its first client id), and its current client\n// id is an alias, so a route replacement re-points the alias in constant work\n// (ADR pr6375 §7.12, R18).\ntype Registry struct {\n\troutes     avl.Tree // route key -\u003e *Route\n\taliases    avl.Tree // current client id -\u003e route key\n\tretired    avl.Tree // retired client id -\u003e route key\n\tcollectors avl.Tree // fee collector address -\u003e int64: how many routes it collects for\n}\n\n// NewRegistry is an empty registry.\nfunc NewRegistry() *Registry { return \u0026Registry{} }\n\n// NewRegistryAny is NewRegistry for a constructor slot typed func() any.\nfunc NewRegistryAny() any { return NewRegistry() }\n\n// Enroll creates the route key, whose current client is key itself, at block\n// time now and height: the route's activation, before which no receive was\n// booked under its controls.\nfunc (r *Registry) Enroll(key string, collector address, now, height int64) (*Route, error) {\n\tif key == \"\" {\n\t\treturn nil, errors.New(\"controls: empty route key\")\n\t}\n\tif !collector.IsValid() {\n\t\treturn nil, errors.New(\"controls: invalid fee collector \" + string(collector))\n\t}\n\tif r.routes.Has(key) || r.aliases.Has(key) || r.retired.Has(key) {\n\t\treturn nil, errors.New(\"controls: client \" + key + \" is a route or a route's client already\")\n\t}\n\trt := \u0026Route{Key: key, Client: key, Enrolled: now, Activated: height, FeeCollector: collector}\n\tr.routes.Set(key, rt)\n\tr.aliases.Set(key, key)\n\tr.countCollector(collector, 1)\n\treturn rt, nil\n}\n\n// SetFeeCollector replaces route key's fee collector, keeping the index of\n// configured collectors (IsFeeCollector) exact.\nfunc (r *Registry) SetFeeCollector(key string, a address) error {\n\trt := r.Route(key)\n\tif rt == nil {\n\t\treturn errors.New(\"controls: no route \" + key)\n\t}\n\tif !a.IsValid() {\n\t\treturn errors.New(\"controls: invalid fee collector \" + string(a))\n\t}\n\tr.countCollector(rt.FeeCollector, -1)\n\trt.FeeCollector = a\n\tr.countCollector(a, 1)\n\treturn nil\n}\n\n// countCollector moves a's count of routes it collects for by d.\nfunc (r *Registry) countCollector(a address, d int64) {\n\tvar n int64\n\tif v := r.collectors.Get(string(a)); v != nil {\n\t\tn = v.(int64)\n\t}\n\tn += d\n\tif n \u003c= 0 {\n\t\tr.collectors.Remove(string(a))\n\t\treturn\n\t}\n\tr.collectors.Set(string(a), n)\n}\n\n// IsFeeCollector reports whether a is the fee collector of any route: an\n// indexed read, whatever the number of routes. The proxies refuse to\n// register an implementation at such an address, which would turn the\n// collector into a realm of the bridge.\nfunc (r *Registry) IsFeeCollector(a address) bool { return r.collectors.Has(string(a)) }\n\n// Route is the route with this key, or nil.\nfunc (r *Registry) Route(key string) *Route {\n\tv := r.routes.Get(key)\n\tif v == nil {\n\t\treturn nil\n\t}\n\treturn v.(*Route)\n}\n\n// RouteOf is the route whose current client is clientID, or nil.\nfunc (r *Registry) RouteOf(clientID string) *Route {\n\tv := r.aliases.Get(clientID)\n\tif v == nil {\n\t\treturn nil\n\t}\n\treturn r.Route(v.(string))\n}\n\n// RetiredRouteOf is the key of the route a replacement retired clientID from,\n// or \"\".\nfunc (r *Registry) RetiredRouteOf(clientID string) string {\n\tv := r.retired.Get(clientID)\n\tif v == nil {\n\t\treturn \"\"\n\t}\n\treturn v.(string)\n}\n\n// Replace re-points the route key to newClient (ADR pr6375 §7.12): newClient\n// becomes the route's current client and the old one is retired. Nothing per\n// denom moves: escrow, vouchers and controls stay keyed by the route key. The\n// route closes in the same step (no send, credit, claim payout or refund\n// payout) until a governed reopening, once the reconciliation's losses all\n// have a restoring decision (Reopen).\nfunc (r *Registry) Replace(key, newClient string, suspectFrom, height int64) error {\n\trt := r.Route(key)\n\tif rt == nil {\n\t\treturn errors.New(\"controls: no route \" + key)\n\t}\n\tif suspectFrom \u003c 0 {\n\t\treturn errors.New(\"controls: a negative suspect height\")\n\t}\n\t// A replacement replaces the route's current client, open or closed,\n\t// as on Ethereum: each is an incident of its own covering the client it\n\t// retires, and the earlier ones keep their records, suspect intervals\n\t// and counters. A reopening under way is aborted: the route reopens only\n\t// once every retired client is settled.\n\tif rt.Reopening {\n\t\trt.Reopening = false\n\t\trt.reopenIncident, rt.reopenRecap = \"\", \"\"\n\t}\n\t// The old client became current at the previous replacement: its suspect\n\t// interval never starts before it (records before were reconciled then).\n\tif suspectFrom \u003c rt.ReplacedAt {\n\t\tsuspectFrom = rt.ReplacedAt\n\t}\n\tif newClient == \"\" || r.aliases.Has(newClient) || r.retired.Has(newClient) || r.routes.Has(newClient) {\n\t\treturn errors.New(\"controls: client \" + newClient + \" is a route's client already\")\n\t}\n\told := rt.Client\n\tr.aliases.Remove(old)\n\tr.retired.Set(old, key)\n\tr.aliases.Set(newClient, key)\n\trt.Client = newClient\n\trt.Replaced = append(rt.Replaced, old)\n\trt.suspect.Set(old, suspectFrom)\n\trt.reblock(old)\n\trt.ReplacedAt = height\n\trt.Closed = true\n\treturn nil\n}\n\n// Keys lists the route keys.\nfunc (r *Registry) Keys() []string {\n\tvar out []string\n\tr.routes.Iterate(\"\", \"\", func(k string, _ any) bool {\n\t\tout = append(out, k)\n\t\treturn false\n\t})\n\treturn out\n}\n\n// ReadOnly is the registry's read-only view (see Reader).\nfunc (r *Registry) ReadOnly() any { return Reader{r: r} }\n\n// ------------------------------------------------------------------- route\n\n// Route is the controls state of one route.\ntype Route struct {\n\tKey          string\n\tClient       string   // the current client id\n\tReplaced     []string // the client ids replacements retired, oldest first\n\tEnrolled     int64    // block time of the enrolment\n\tActivated    int64    // block height of the enrolment: the controls' cutoff\n\tFeeCollector address\n\tClosed       bool  // closed by a replacement until a governed reopening\n\tReopening    bool  // a reopening decision runs in batches (ContinueReopen)\n\tReplacedAt   int64 // the block height of the last replacement\n\n\trefused   avl.Tree // lower-case Ethereum address -\u003e true: refused send recipients\n\tprotected avl.Tree // lower-case Ethereum address -\u003e its role: the bridge's own contracts, never a recipient\n\tbridged   bool     // the router, ICS20 transfer and IBCAdmin are protected (Protect)\n\tdenoms    avl.Tree // denom -\u003e *Denom\n\tclaims    avl.Tree // packet key -\u003e *Claim\n\tsweeps    avl.Tree // packet key -\u003e *SweepEntry\n\tsends     avl.Tree // packet key -\u003e *SendRecord\n\topenSends avl.Tree // client id -\u003e int64: its open send records\n\tpending   avl.Tree // client id -\u003e int64: its send records with no outcome yet\n\tprocessed avl.Tree // client id -\u003e *processedSet: its processed, unsettled send records by height\n\tsuspect   avl.Tree // retired client id -\u003e int64: the height its suspect interval starts at\n\tblocking  avl.Tree // retired client id -\u003e true while it blocks a reopening (an open record, or an unsettled suspect one)\n\n\tunresolvedDenoms int64 // denoms with an unresolved loss: ExposureClosed in constant work\n\n\treopenIncident, reopenRecap string   // where a batched reopening stopped\n\tprepIdx                     int      // the retired clients (Replaced) whose records PrepareReplacement walked\n\tprepCursor                  string   // where its walk of Replaced[prepIdx] stopped\n\tlossesBooked                int64    // every loss booked: a reopening restarts its scan when it changes\n\treopenLosses                int64    // lossesBooked when the reopening's incident scan started\n\tlosses                      avl.Tree // lossID(direction, packet key) -\u003e *Loss: one per packet and direction\n\tincidents                   avl.Tree // denom|incident -\u003e *IncidentTotals\n\trecaps                      avl.Tree // seqKey(id) -\u003e *Recap\n\topenRecapIdx                avl.Tree // incidentKey(denom, incident) + \"|\" + seqKey(id) -\u003e *Recap, unpaid and unrevoked\n\tnRecaps                     int64\n}\n\n// PendingSends is how many send records of clientID have no outcome yet:\n// their commitments are outstanding in the core.\nfunc (rt *Route) PendingSends(clientID string) int64 {\n\tif v := rt.pending.Get(clientID); v != nil {\n\t\treturn v.(int64)\n\t}\n\treturn 0\n}\n\nfunc (rt *Route) addPending(key string, delta int64) {\n\tclient := clientOfKey(key)\n\tn := rt.PendingSends(client) + delta\n\tif n == 0 {\n\t\trt.pending.Remove(client)\n\t\treturn\n\t}\n\trt.pending.Set(client, n)\n}\n\n// IsClosed reports whether a replacement closed the route until a governed\n// reopening (ADR pr6375 §7.12): nothing moves on it but voucher returns (exit\n// only): no gno-native send, credit, claim payout or refund payout;\n// acknowledgements and timeouts still settle its sends, their refunds queued.\nfunc (rt *Route) IsClosed() bool { return rt.Closed }\n\n// ExposureClosed reports whether the route takes on no new exposure: it is\n// closed, or a loss of any denom has no restoring decision (ADR pr6375 §7.9).\n// A loss stops gno-native sends, credits, claim payouts and sweep payments,\n// whatever the guardians' flags; it never stops voucher returns,\n// acknowledgements, timeouts or refunds, which only the refund meter and the\n// refunds hold govern (§7.3, §7.13).\nfunc (rt *Route) ExposureClosed() bool { return rt.Closed || rt.hasUnresolved() }\n\n// LossClosed reports whether a loss of any denom has no restoring decision.\nfunc (rt *Route) LossClosed() bool { return rt.hasUnresolved() }\n\n// hasUnresolved reports whether any denom of the route has a loss without a\n// restoring decision: denom by denom, never through a sum that could wrap.\nfunc (rt *Route) hasUnresolved() bool { return rt.unresolvedDenoms \u003e 0 }\n\n// noteUnresolved keeps the count of denoms with an unresolved loss after\n// d's unresolved balance changed from was.\nfunc (rt *Route) noteUnresolved(d *Denom, was uint256) {\n\tswitch now := d.Totals.OpenUnresolved; {\n\tcase was == 0 \u0026\u0026 now \u003e 0:\n\t\trt.unresolvedDenoms++\n\tcase was \u003e 0 \u0026\u0026 now == 0:\n\t\trt.unresolvedDenoms--\n\t}\n}\n\n// Reopen reopens a route a replacement closed, by decision (the gno\n// analogue of Ethereum's kind 13, ADR pr6375 §5.4): refused while any send\n// record of a retired client is still open (its commitment outstanding or\n// its refund queued, not settled by reconciliation), or was processed (an\n// acknowledgement or a timeout) at or after the replacement's suspect height\n// and is not settled. Every loss not\n// recapitalized or reduced is recorded as an accepted deficit by the reopening\n// itself (§7.9) and every unpaid recapitalization authorization is revoked,\n// in bounded batches: the route opens when the last completes (see\n// ContinueReopen). It returns the deficit this batch accepted and whether the\n// route is open.\nfunc (rt *Route) Reopen() (uint256, bool, error) {\n\tif !rt.Closed {\n\t\treturn 0, false, errors.New(\"controls: route \" + rt.Key + \" is not closed\")\n\t}\n\tif err := rt.checkPrepared(); err != nil {\n\t\treturn 0, false, err\n\t}\n\tif rt.Reopening {\n\t\treturn 0, false, errors.New(\"controls: route \" + rt.Key + \" is reopening already: continue it\")\n\t}\n\tif err := rt.reopenBlocker(); err != nil {\n\t\treturn 0, false, err\n\t}\n\trt.Reopening = true\n\trt.reopenIncident, rt.reopenRecap = \"\", \"\"\n\trt.reopenLosses = rt.lossesBooked\n\treturn rt.reopenBatch()\n}\n\n// reopenBlocker is why the route cannot open now, or nil: a retired\n// client's record is open or was processed in its suspect interval and is\n// not settled. It reads the index of blocking clients, in logarithmic work\n// whatever the number of replacements.\nfunc (rt *Route) reopenBlocker() error {\n\tc := \"\"\n\trt.blocking.Iterate(\"\", \"\", func(k string, _ any) bool {\n\t\tc = k\n\t\treturn true\n\t})\n\tif c == \"\" {\n\t\treturn nil\n\t}\n\tif n := rt.OpenSends(c); n \u003e 0 {\n\t\treturn ufmt.Errorf(\"controls: %d send records of retired client %s are still open: settle them first\", n, c)\n\t}\n\treturn ufmt.Errorf(\"controls: %d send records retired client %s processed in its suspect interval are not settled: settle them first\", rt.SuspectUnsettled(c), c)\n}\n\n// reblock updates the index of retired clients blocking a reopening after a\n// change to client's records: a retired client blocks while a record of it\n// is open, or one processed in its suspect interval is not settled.\nfunc (rt *Route) reblock(client string) {\n\tif rt.suspect.Has(client) \u0026\u0026 (rt.OpenSends(client) \u003e 0 || rt.SuspectUnsettled(client) \u003e 0) {\n\t\trt.blocking.Set(client, true)\n\t\treturn\n\t}\n\trt.blocking.Remove(client)\n}\n\n// ContinueReopen runs the next bounded batch of a reopening a decision\n// started (ADR pr6375 §5.4 kind 13: a permissionless continuation); the route\n// opens when the last batch completes. It returns the deficit it accepted and\n// whether the route is open.\nfunc (rt *Route) ContinueReopen() (uint256, bool, error) {\n\tif !rt.Reopening {\n\t\treturn 0, false, errors.New(\"controls: route \" + rt.Key + \" is not reopening\")\n\t}\n\treturn rt.reopenBatch()\n}\n\n// reopenBatch records as accepted deficits the losses of at most\n// ReopenBatch incidents, then revokes at most ReopenBatch unpaid\n// recapitalization authorizations, from where the last batch stopped; when\n// none is left the route opens.\nfunc (rt *Route) reopenBatch() (uint256, bool, error) {\n\tvar accepted uint256\n\tn := 0\n\tmore := rt.incidents.Iterate(after(rt.reopenIncident), \"\", func(k string, v any) bool {\n\t\tif n == ReopenBatch {\n\t\t\treturn true\n\t\t}\n\t\tit := v.(*IncidentTotals)\n\t\tif rest := it.OpenUnresolved; rest \u003e 0 {\n\t\t\ti := strings.Index(k, \"|\")\n\t\t\trt.acceptAll(k[:i], k[i+1:])\n\t\t\taccepted = statAdd(accepted, rest)\n\t\t}\n\t\trt.reopenIncident = k\n\t\tn++\n\t\treturn false\n\t})\n\tif more {\n\t\treturn accepted, false, nil\n\t}\n\tn = 0\n\tmore = rt.recaps.Iterate(after(rt.reopenRecap), \"\", func(k string, v any) bool {\n\t\tif n == ReopenBatch {\n\t\t\treturn true\n\t\t}\n\t\tif r := v.(*Recap); !r.Paid \u0026\u0026 !r.Revoked {\n\t\t\trt.closeRecap(r)\n\t\t\tr.Revoked = true\n\t\t}\n\t\trt.reopenRecap = k\n\t\tn++\n\t\treturn false\n\t})\n\tif more {\n\t\treturn accepted, false, nil\n\t}\n\t// A loss booked since the incident scan started may sit behind its\n\t// cursor, on an incident it accepted already: the scan starts over, so\n\t// the route opens only once every incident's losses are resolved or\n\t// accepted (ADR pr6375 §5.4 kind 13, §7.12).\n\tif rt.lossesBooked != rt.reopenLosses {\n\t\trt.reopenIncident, rt.reopenLosses = \"\", rt.lossesBooked\n\t\treturn accepted, false, nil\n\t}\n\trt.Reopening, rt.Closed = false, false\n\treturn accepted, true, nil\n}\n\n// after is the first key an iteration resumes at, past cursor (\"\" from the\n// start).\nfunc after(cursor string) string {\n\tif cursor == \"\" {\n\t\treturn \"\"\n\t}\n\treturn cursor + \"\\x00\"\n}\n\n// Refuse adds lower-case Ethereum addresses to the refused send recipients\n// (the route's bridge contracts, ADR pr6375 §7.8).\nfunc (rt *Route) Refuse(addrs []string) {\n\tfor _, a := range addrs {\n\t\trt.refused.Set(strings.ToLower(a), true)\n\t}\n}\n\n// Unrefuse removes addresses from the refused send recipients.\nfunc (rt *Route) Unrefuse(addrs []string) {\n\tfor _, a := range addrs {\n\t\trt.refused.Remove(strings.ToLower(a))\n\t}\n}\n\n// IsRefused reports whether addr (any case) is a refused send recipient: one\n// a decision refused, or a protected bridge contract.\nfunc (rt *Route) IsRefused(addr string) bool {\n\ta := strings.ToLower(addr)\n\treturn rt.refused.Has(a) || rt.protected.Has(a)\n}\n\n// Bridge roles of the protected recipients.\nconst (\n\tRoleRouter       = \"router\"\n\tRoleICS20        = \"ics20\"\n\tRoleIBCAdmin     = \"ibc admin\"\n\tRoleVoucherToken = \"voucher token\"\n)\n\n// Protect sets the route's mandatory protected recipients, once, at its\n// enrolment (ADR pr6375 R12, §7.8): its Ethereum router, ICS20 transfer\n// contract and IBCAdmin, three distinct addresses (lower-case hex). They\n// are never removed (Unrefuse touches only the decided refusals) and stay\n// through every replacement; a route takes no gno-native send until they\n// are set. Each voucher token a denom names (DenomConfig.Remote) is\n// protected as it is configured.\nfunc (rt *Route) Protect(router, ics20, ibcAdmin string) error {\n\tif err := CheckBridge(router, ics20, ibcAdmin); err != nil {\n\t\treturn err\n\t}\n\tif rt.bridged {\n\t\treturn errors.New(\"controls: the bridge contracts of route \" + rt.Key + \" are set already\")\n\t}\n\trt.protected.Set(strings.ToLower(router), RoleRouter)\n\trt.protected.Set(strings.ToLower(ics20), RoleICS20)\n\trt.protected.Set(strings.ToLower(ibcAdmin), RoleIBCAdmin)\n\trt.bridged = true\n\treturn nil\n}\n\n// CheckBridge is why Protect would refuse the three bridge contracts, or nil.\nfunc CheckBridge(router, ics20, ibcAdmin string) error {\n\tas := []string{strings.ToLower(router), strings.ToLower(ics20), strings.ToLower(ibcAdmin)}\n\tfor i, a := range as {\n\t\tif e, err := ethaddr.Parse(a); err != nil || e.IsZero() {\n\t\t\treturn errors.New(\"controls: a bridge contract is a non-zero Ethereum address, not \" + a)\n\t\t}\n\t\tfor _, b := range as[:i] {\n\t\t\tif a == b {\n\t\t\t\treturn errors.New(\"controls: the router, ICS20 transfer and IBCAdmin are three contracts\")\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// Bridged reports whether the route's bridge contracts are protected.\nfunc (rt *Route) Bridged() bool { return rt.bridged }\n\n// Protected lists the protected recipients as \"address role\".\nfunc (rt *Route) Protected() []string {\n\tvar out []string\n\trt.protected.Iterate(\"\", \"\", func(k string, v any) bool {\n\t\tout = append(out, k+\" \"+v.(string))\n\t\treturn false\n\t})\n\treturn out\n}\n\n// protectToken protects a denom's voucher token on Ethereum.\nfunc (rt *Route) protectToken(remote string) {\n\tif remote != \"\" \u0026\u0026 !rt.protected.Has(strings.ToLower(remote)) {\n\t\trt.protected.Set(strings.ToLower(remote), RoleVoucherToken)\n\t}\n}\n\n// Refused lists the refused send recipients.\nfunc (rt *Route) Refused() []string {\n\tvar out []string\n\trt.refused.Iterate(\"\", \"\", func(k string, _ any) bool {\n\t\tout = append(out, k)\n\t\treturn false\n\t})\n\treturn out\n}\n\n// ------------------------------------------------------------------- denom\n\n// Denom is one denom of a route: its configuration, its meters and queues,\n// and its accounting.\ntype Denom struct {\n\tName           string // the escrow denom (KindUnescrow) or the voucher's IBC denom (KindMint)\n\tConfig         DenomConfig\n\tLaunchCapFloor uint256 // the cap floor it was enabled with: lowering never goes below it\n\n\tmeter       Meter\n\tqueues      avl.Tree // recipient -\u003e *queue\n\twaiting     avl.Tree // recipient -\u003e true while its queue holds an open claim\n\tcursor      string   // the last recipient served\n\trefundMeter Meter\n\trefundQueue avl.Tree // seqKey(n) -\u003e send key, oldest first\n\trefundIdx   avl.Tree // send key -\u003e its seqKey in refundQueue\n\trefundSeq   int64\n\n\tTotals Totals\n}\n\n// Totals is a denom's accounting (ADR pr6375 §7.11's claim, refund and loss\n// terms, kept on chain so monitors read them at a pinned height). Every term\n// is an exact uint256 (see arith.gno): the reservation and loss totals that\n// used to need counters wider than an int64 are plain amounts now, and the\n// JSON shows them whole.\ntype Totals struct {\n\tCredited      uint256 // booked as claims, by packets and votes\n\tOwed          uint256 // open claims' remainders\n\tWithheld      uint256 // withheld claims' remainders\n\tSwept         uint256 // live sweep entries\n\tPaid          uint256 // claim installments paid to recipients\n\tFees          uint256 // claim fee installments paid to the collector\n\tSweepPaid     uint256 // sweep entries paid by decision\n\tOutstanding   uint256 // KindMint: vouchers this route has in circulation\n\tReturning     uint256 // KindMint: vouchers burned by sends not settled yet (in flight, or refund owed), what they still reserve\n\tProvenOwed    uint256 // refunds proven owed (refundable, or a collision) on sends a forged success released, reserved until their settlement\n\tInFlight      uint256 // what the open send records may still owe: their amounts less what was refunded\n\tSent          uint256 // escrowed (KindUnescrow) or burned (KindMint) by recorded sends\n\tSendFees      uint256 // fees charged on sends\n\tRefunded      uint256 // refunds paid\n\tQueuedRefunds uint256 // refunds owed and not paid yet\n\t// Lifetime statistics of the loss ledger: saturating, never read by a\n\t// check.\n\tLost          uint256\n\tRecapitalized uint256\n\tReduced       uint256\n\tAccepted      uint256\n\t// The outstanding balances: losses neither recapitalized nor reduced\n\t// (OpenUnrecovered), and of those, the ones not accepted as a deficit\n\t// either (OpenUnresolved).\n\tOpenUnrecovered uint256\n\tOpenUnresolved  uint256\n}\n\n// Unrecovered is the loss ledger's economic open total, the term of the\n// supply identity (ADR pr6375 §7.11): losses neither recapitalized nor\n// reduced. An accepted deficit restores neither backing nor liabilities, so\n// it stays unrecovered.\nfunc (t Totals) Unrecovered() uint256 { return t.OpenUnrecovered }\n\n// Unresolved is what of the losses still waits for a restoring decision\n// (ADR pr6375 §7.9): the unrecovered losses not accepted as a deficit\n// either. A route reopens only when it is zero.\nfunc (t Totals) Unresolved() uint256 { return t.OpenUnresolved }\n\n// booked is what a minted denom counts against its supply cap (ADR pr6375\n// §6.8): the vouchers in circulation, the vouchers burned by sends that may\n// still be refunded (minted again), and every claim and sweep entry not paid\n// yet.\nfunc (t Totals) booked() uint256 {\n\treturn boundAdd(boundAdd(boundAdd(boundAdd(boundAdd(t.Outstanding, t.Returning), t.ProvenOwed), t.Owed), t.Withheld), t.Swept)\n}\n\n// queue is one recipient's open claims, oldest first, and its meter.\ntype queue struct {\n\titems avl.Tree // seqKey(seq) -\u003e packet key\n\tmeter Meter\n\tnext  int64\n}\n\n// SetDenom enables a denom on the route or replaces its configuration. A new\n// denom's meters start empty as of now in epoch. A new minted denom starts\n// with outstanding, the vouchers of it already in circulation (minted before\n// the route was controlled), counted against its supply cap from the start;\n// outstanding is ignored otherwise. The cap floor never goes below the one the\n// denom was enabled with, and a denom never changes accounting.\nfunc (rt *Route) SetDenom(name string, c DenomConfig, now, epoch int64, outstanding uint256) error {\n\tif err := rt.checkSetDenom(name, c, outstanding); err != nil {\n\t\treturn err\n\t}\n\tif d := rt.Denom(name); d != nil {\n\t\td.Config = c\n\t\trt.protectToken(c.Remote)\n\t\treturn nil\n\t}\n\td := \u0026Denom{Name: name, Config: c, LaunchCapFloor: c.CapFloor}\n\tif c.Kind == KindMint {\n\t\td.Totals.Outstanding = outstanding\n\t}\n\td.meter = NewMeter(now, epoch)\n\td.refundMeter = NewMeter(now, 0)\n\trt.denoms.Set(name, d)\n\trt.protectToken(c.Remote)\n\treturn nil\n}\n\n// checkSetDenom is why SetDenom(name, c, ..., outstanding) would fail, or nil.\nfunc (rt *Route) checkSetDenom(name string, c DenomConfig, outstanding uint256) error {\n\tif name == \"\" {\n\t\treturn errors.New(\"controls: empty denom\")\n\t}\n\tif err := c.Validate(); err != nil {\n\t\treturn err\n\t}\n\tif d := rt.Denom(name); d != nil {\n\t\tif c.Kind != d.Config.Kind {\n\t\t\treturn errors.New(\"controls: a denom never changes accounting\")\n\t\t}\n\t\tif c.CapFloor \u003c d.LaunchCapFloor {\n\t\t\treturn ufmt.Errorf(\"controls: the cap floor (%s) cannot go below its launch value (%s)\", u(c.CapFloor), u(d.LaunchCapFloor))\n\t\t}\n\t}\n\treturn nil\n}\n\n// Reconfigure is SetDenom for a governed change: the denom's bridge meter\n// and refund meter are first refilled up to now at the old caps (the bridge\n// cap derived from lock under the old configuration), then set to the new\n// caps with no time elapsed, so the new caps apply from the change and never\n// to the time before it (ADR pr6375 §7.1; Ethereum settles its buckets the\n// same way). Recipient meters, one per recipient, are not walked: each\n// refills at the cap it last saw up to its next refill (Meter.Refill), which\n// can only grant less. Nothing changes when the change is refused.\nfunc (rt *Route) Reconfigure(name string, c DenomConfig, now, epoch, epochAt int64, outstanding, lock uint256) error {\n\tif err := rt.checkSetDenom(name, c, outstanding); err != nil {\n\t\treturn err\n\t}\n\tif d := rt.Denom(name); d != nil {\n\t\told := d.DayCap(lock)\n\t\td.meter.Refill(now, old, old, epoch, epochAt)\n\t\td.refundMeter.Refill(now, d.Config.RefundCap, d.Config.RefundCap, 0, 0)\n\t}\n\tif err := rt.SetDenom(name, c, now, epoch, outstanding); err != nil {\n\t\treturn err\n\t}\n\td := rt.Denom(name)\n\tnc := d.DayCap(lock)\n\td.meter.Refill(now, nc, nc, epoch, epochAt)\n\td.refundMeter.Refill(now, d.Config.RefundCap, d.Config.RefundCap, 0, 0)\n\treturn nil\n}\n\n// Denom is the denom's state, or nil.\nfunc (rt *Route) Denom(name string) *Denom {\n\tv := rt.denoms.Get(name)\n\tif v == nil {\n\t\treturn nil\n\t}\n\treturn v.(*Denom)\n}\n\n// Denoms lists the route's denoms.\nfunc (rt *Route) Denoms() []string {\n\tvar out []string\n\trt.denoms.Iterate(\"\", \"\", func(k string, _ any) bool {\n\t\tout = append(out, k)\n\t\treturn false\n\t})\n\treturn out\n}\n\n// Unrecovered is the economic open loss total over the route's denoms,\n// saturating at the maximum.\nfunc (rt *Route) Unrecovered() uint256 {\n\tvar total uint256\n\trt.denoms.Iterate(\"\", \"\", func(_ string, v any) bool {\n\t\ttotal = boundAdd(total, v.(*Denom).Totals.Unrecovered())\n\t\treturn false\n\t})\n\treturn total\n}\n\n// Unresolved is the loss total over the route's denoms that still waits for\n// a restoring decision, saturating at the maximum.\nfunc (rt *Route) Unresolved() uint256 {\n\tvar total uint256\n\trt.denoms.Iterate(\"\", \"\", func(_ string, v any) bool {\n\t\ttotal = boundAdd(total, v.(*Denom).Totals.Unresolved())\n\t\treturn false\n\t})\n\treturn total\n}\n\n// DayCap is the denom's day cap: max(CapFloor, lock/10) for an unescrowed\n// denom, CapFloor for a minted one.\nfunc (d *Denom) DayCap(lock uint256) uint256 {\n\tif d.Config.Kind == KindMint {\n\t\treturn d.Config.CapFloor\n\t}\n\tc := lock / 10\n\tif c \u003c d.Config.CapFloor {\n\t\tc = d.Config.CapFloor\n\t}\n\treturn c\n}\n\n// Reserved is the escrow the denom's claims and owed refunds hold: open and\n// withheld claim remainders, and every queued (held or not) refund, so\n// voids, credits and payouts never spend it twice (ADR pr6375 §7.9, §7.12:\n// refund liabilities move with the backing).\nfunc (d *Denom) Reserved() uint256 {\n\treturn boundAdd(boundAdd(boundAdd(d.Totals.Owed, d.Totals.Withheld), d.Totals.QueuedRefunds), d.Totals.ProvenOwed)\n}\n\n// liabilities is what the denom's claims and sweep entries owe, saturating.\nfunc (t Totals) liabilities() uint256 { return boundAdd(boundAdd(t.Owed, t.Withheld), t.Swept) }\n\n// CheckSend is what a send of a gno-native token must satisfy (ADR pr6375\n// §7.8): between the minimum and the maximum. A minted denom's sends are\n// voucher returns and are never limited.\nfunc (d *Denom) CheckSend(amount uint256) error {\n\tif d.Config.Kind != KindUnescrow {\n\t\treturn nil\n\t}\n\tif amount == 0 {\n\t\treturn errors.New(\"controls: the amount must be positive\")\n\t}\n\tif amount \u003c d.Config.SendMin {\n\t\treturn ufmt.Errorf(\"controls: %s is below the minimum send of %s\", u(amount), u(d.Config.SendMin))\n\t}\n\tif amount \u003e d.Config.SendMax {\n\t\treturn ufmt.Errorf(\"controls: %s is above the maximum send of %s\", u(amount), u(d.Config.SendMax))\n\t}\n\treturn nil\n}\n\n// ------------------------------------------------------------------ claims\n\n// Claim is one credit the route owes a recipient, keyed by its packet.\ntype Claim struct {\n\tKey     string // the packet key (receiving client / sequence)\n\tDenom   string\n\tTo      address\n\tAmount  uint256 // what it owes in all, after any reduction\n\tPaid    uint256 // fee included\n\tReduced uint256 // cut by liability reductions\n\t// Fee is the denom's fee when the claim was credited, locked then: the\n\t// claim's first installment and its later minimum installment are the\n\t// lower of it and the denom's fee now (effectiveFee).\n\tFee    uint256\n\tAt     int64 // credited at, unix time\n\tHeight int64 // credited at, block height\n\tBy     string\n\tState  string\n\tSeq    int64 // its place in the recipient's queue\n}\n\n// effectiveFee is the claim's fee: the lower of its fee (locked when it was\n// credited, lowered by every cut seen at a payout since, as PayClaims keeps\n// it) and the denom's fee now. A raise never applies to a claim already\n// credited; a cut does, and stays, so a claim's first installment and its minimum\n// installment never exceed what the current configuration lets a recipient\n// meter (a quarter of a day cap at least the cap floor, the fee at most a\n// quarter of the cap floor) hold.\nfunc (c *Claim) effectiveFee(d *Denom) uint256 {\n\tif d.Config.Fee \u003c c.Fee {\n\t\treturn d.Config.Fee\n\t}\n\treturn c.Fee\n}\n\n// Remaining is what the claim still owes.\nfunc (c *Claim) Remaining() uint256 { return mustSub(c.Amount, c.Paid) }\n\n// SweepEntry is a credit that pays nobody until a decision directs it (ADR\n// pr6375 §7.1): an amount below the fee, above the free lock, or to a bridge\n// realm.\ntype SweepEntry struct {\n\tKey     string\n\tDenom   string\n\tTo      string // the receiver the packet named\n\tAmount  uint256\n\tReason  string\n\tReduced uint256 // cut by liability reductions\n\tHeight  int64\n\tVoided  bool   // voided as forged\n\tPaid    bool   // paid by a sweep decision, or replaced by a credit by vote\n\tPaidTo  string // where a sweep decision paid it\n\t// FromClaim marks an entry an open claim was moved to, its recipient\n\t// having become a bridge realm after the credit; ClaimPaid is what the\n\t// claim had paid out (fee included) before: part of what the receive\n\t// paid out, should it prove forged.\n\tFromClaim bool\n\tClaimPaid uint256\n}\n\n// paidOut is what the receive of a sweep entry paid out: what its claim paid\n// before it was moved here, and what a sweep decision paid (a credit by vote\n// replacing it pays through its claim instead).\nfunc (e *SweepEntry) paidOut() uint256 {\n\tpaid := e.ClaimPaid\n\tif e.Paid \u0026\u0026 e.PaidTo != PaidByVote {\n\t\tpaid = mustAdd(paid, e.Amount)\n\t}\n\treturn paid\n}\n\n// Payment is what a claim payout or a sweep payment moves: Amount to To and\n// Fee to the route's fee collector, out of the denom's escrow (KindUnescrow)\n// or as newly minted vouchers (KindMint).\ntype Payment struct {\n\tKey    string\n\tDenom  string\n\tTo     address\n\tAmount uint256\n\tFee    uint256\n}\n\n// Claim is the claim of a packet key, by value, and whether it exists.\nfunc (rt *Route) Claim(key string) (Claim, bool) {\n\tc := rt.claim(key)\n\tif c == nil {\n\t\treturn Claim{}, false\n\t}\n\treturn *c, true\n}\n\nfunc (rt *Route) claim(key string) *Claim {\n\tv := rt.claims.Get(key)\n\tif v == nil {\n\t\treturn nil\n\t}\n\treturn v.(*Claim)\n}\n\n// Sweep is the sweep entry of a packet key, by value, and whether it exists.\nfunc (rt *Route) Sweep(key string) (SweepEntry, bool) {\n\te := rt.sweep(key)\n\tif e == nil {\n\t\treturn SweepEntry{}, false\n\t}\n\treturn *e, true\n}\n\nfunc (rt *Route) sweep(key string) *SweepEntry {\n\tv := rt.sweeps.Get(key)\n\tif v == nil {\n\t\treturn nil\n\t}\n\treturn v.(*SweepEntry)\n}\n\n// CheckCredit is the refusal test of a receive, run before any state change\n// (ADR pr6375 §7.3): the denom must be enabled on the route, the amount\n// positive and, for a minted denom, within its supply cap (§6.8). A refusal is\n// answered with an error acknowledgement, so the sender is refunded at the\n// source.\nfunc (rt *Route) CheckCredit(denom string, amount uint256) error {\n\td := rt.Denom(denom)\n\tif d == nil {\n\t\treturn errors.New(\"controls: denom \" + denom + \" is not enabled on route \" + rt.Key)\n\t}\n\tif amount == 0 {\n\t\treturn errors.New(\"controls: the amount must be positive\")\n\t}\n\t// Credit's own checked addition, here too: a receive that would\n\t// overflow what the route owes is refused before its acknowledgement.\n\tif _, ok := checkedAdd(d.Totals.liabilities(), amount); !ok {\n\t\treturn errors.New(\"controls: the amount would overflow what is owed\")\n\t}\n\tif d.Config.Kind == KindMint {\n\t\tbooked := d.Totals.booked()\n\t\tif amount \u003e sub0(d.Config.SupplyCap, booked) {\n\t\t\treturn ufmt.Errorf(\"controls: %s would take %s past its supply cap of %s (%s outstanding or booked)\", u(amount), denom, u(d.Config.SupplyCap), u(booked))\n\t\t}\n\t}\n\treturn nil\n}\n\n// Credit books a received transfer under its packet key, in the same\n// transaction as the packet's receipt: a claim in the recipient's queue, or an\n// entry of the sweep queue. sweepReason, when not empty, sweeps it whatever\n// the amount (a bridge realm receiver); otherwise an amount below the fee, or\n// for an unescrowed denom above the free lock (lock minus what claims reserve),\n// is swept. It returns whether it was swept. Run CheckCredit first.\nfunc (rt *Route) Credit(now, height, epoch int64, key, denom string, to address, amount uint256, by, sweepReason string, lock uint256) (bool, error) {\n\tif err := rt.CheckCredit(denom, amount); err != nil {\n\t\treturn false, err\n\t}\n\tif rt.claims.Has(key) || rt.sweeps.Has(key) {\n\t\treturn false, errors.New(\"controls: packet \" + key + \" is credited already\")\n\t}\n\td := rt.Denom(denom)\n\treason := sweepReason\n\tif reason == \"\" {\n\t\tswitch {\n\t\tcase amount \u003c d.Config.Fee:\n\t\t\treason = \"below the fee\"\n\t\tcase d.Config.Kind == KindUnescrow \u0026\u0026 amount \u003e sub0(lock, d.Reserved()):\n\t\t\treason = \"above the free lock\"\n\t\t}\n\t}\n\tif reason != \"\" {\n\t\trt.sweeps.Set(key, \u0026SweepEntry{Key: key, Denom: denom, To: string(to), Amount: amount, Reason: reason, Height: height})\n\t\td.Totals.Swept = mustAdd(d.Totals.Swept, amount)\n\t\treturn true, nil\n\t}\n\tif !to.IsValid() {\n\t\treturn false, errors.New(\"controls: invalid recipient \" + string(to))\n\t}\n\trt.enqueue(d, now, height, epoch, key, to, amount, by)\n\treturn false, nil\n}\n\nfunc (rt *Route) enqueue(d *Denom, now, height, epoch int64, key string, to address, amount uint256, by string) *Claim {\n\tq := d.queue(to, now, epoch)\n\tq.next++\n\tc := \u0026Claim{Key: key, Denom: d.Name, To: to, Amount: amount, Fee: d.Config.Fee, At: now, Height: height, By: by, State: Open, Seq: q.next}\n\trt.claims.Set(key, c)\n\tq.items.Set(seqKey(c.Seq), key)\n\td.waiting.Set(string(to), true)\n\td.Totals.Owed = mustAdd(d.Totals.Owed, amount)\n\td.Totals.Credited = statAdd(d.Totals.Credited, amount)\n\treturn c\n}\n\n// queue is a recipient's queue, made on first use with its meter empty as of\n// now in epoch, at the recipient cap in effect then: a queue made after an\n// epoch's reset starts empty at its first credit, as every meter does.\nfunc (d *Denom) queue(to address, now, epoch int64) *queue {\n\tif v := d.queues.Get(string(to)); v != nil {\n\t\treturn v.(*queue)\n\t}\n\tq := \u0026queue{meter: NewMeter(now, epoch)}\n\t// It starts at the recipient cap in effect now (a quarter of the bridge\n\t// meter's, as of its last refill), so a cap raised before its first\n\t// refill never applies to the time before it.\n\tif d.meter.Set {\n\t\tq.meter.Rate, q.meter.Ceil, q.meter.Set = d.meter.Rate/4, d.meter.Ceil/4, true\n\t}\n\td.queues.Set(string(to), q)\n\treturn q\n}\n\n// oldestOpen is the first open claim of a queue, or nil.\nfunc (rt *Route) oldestOpen(q *queue) *Claim {\n\tvar found *Claim\n\tq.items.Iterate(\"\", \"\", func(_ string, v any) bool {\n\t\tc := rt.claim(v.(string))\n\t\tif c != nil \u0026\u0026 c.State == Open {\n\t\t\tfound = c\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t})\n\treturn found\n}\n\n// nextRecipient is the waiting recipient after the cursor, wrapping.\nfunc (d *Denom) nextRecipient() string {\n\tnext := \"\"\n\td.waiting.Iterate(d.cursor+\"\\x00\", \"\", func(k string, _ any) bool {\n\t\tnext = k\n\t\treturn true\n\t})\n\tif next == \"\" {\n\t\td.waiting.Iterate(\"\", \"\", func(k string, _ any) bool {\n\t\t\tnext = k\n\t\t\treturn true\n\t\t})\n\t}\n\treturn next\n}\n\n// MeterLevels refills and reads the bridge meter and a recipient's meter of\n// a denom under the payouts epoch (a recipient without a queue reads 0).\nfunc (rt *Route) MeterLevels(now, epoch, epochAt int64, denom string, to address, lock uint256) (bridge, recipient uint256) {\n\td := rt.Denom(denom)\n\tif d == nil {\n\t\treturn 0, 0\n\t}\n\tc := d.DayCap(lock)\n\td.meter.Refill(now, c, c, epoch, epochAt)\n\tif v := d.queues.Get(string(to)); v != nil {\n\t\tq := v.(*queue)\n\t\tq.meter.Refill(now, c/4, c/4, epoch, epochAt)\n\t\trecipient = q.meter.Level\n\t}\n\treturn d.meter.Level, recipient\n}\n\n// PayClaims pays up to n claims of a denom (n ≤ MaxClaims), taking recipients\n// round-robin and each recipient's oldest open claim (ADR pr6375 §7.1):\n//\n//   - a turn pays at most min(bridge meter, recipient meter), and for an\n//     unescrowed denom never more than the lock still holds;\n//   - a claim's first installment is its fee, whole, to the fee collector;\n//   - a later installment is paid only when both meters hold at least\n//     min(what the claim still owes, its fee): no slivers;\n//   - a claim's fee is the lower of its own (Claim.Fee: the denom's fee when\n//     it was credited, lowered by every cut a payout turn has seen since)\n//     and the denom's fee now, written back: a later raise never applies to\n//     it, and a cut (with the cap floor, which bounds the fee) never strands\n//     it and is kept.\n//   - a recipient whose meters hold less is passed over, its meter refilling\n//     for a later turn.\n//\n// The bridge meter refills at the day cap a day, the recipient's at a quarter\n// of it; both empty when the payouts epoch changes. It books every payment\n// and returns them for the caller to make; at most n claims are paid and at\n// most 4n recipients looked at.\nfunc (rt *Route) PayClaims(now, epoch, epochAt int64, denom string, n int, lock uint256) ([]Payment, error) {\n\tps, _, err := rt.PayClaimsSweeping(now, epoch, epochAt, denom, n, lock, nil)\n\treturn ps, err\n}\n\n// PayClaimsSweeping is PayClaims checking each claim's recipient at payout\n// time: a claim whose recipient sweep reports (a bridge realm, which became\n// one after the credit) is not paid but moved to the sweep queue, owed until\n// a decision directs it, and its key is returned with the moved ones. A move\n// takes the recipient's turn.\nfunc (rt *Route) PayClaimsSweeping(now, epoch, epochAt int64, denom string, n int, lock uint256, sweep func(address) bool) ([]Payment, []string, error) {\n\td := rt.Denom(denom)\n\tif d == nil {\n\t\treturn nil, nil, errors.New(\"controls: denom \" + denom + \" is not enabled on route \" + rt.Key)\n\t}\n\tif n \u003c 1 {\n\t\treturn nil, nil, errors.New(\"controls: n must be positive\")\n\t}\n\tif n \u003e MaxClaims {\n\t\tn = MaxClaims\n\t}\n\tvar out []Payment\n\tvar moved []string\n\tfor visits := 0; len(out) \u003c n \u0026\u0026 visits \u003c 4*n \u0026\u0026 d.waiting.Size() \u003e 0; visits++ {\n\t\taddr := d.nextRecipient()\n\t\td.cursor = addr\n\t\tq := d.queues.Get(addr).(*queue)\n\t\tc := rt.oldestOpen(q)\n\t\tif c == nil {\n\t\t\td.waiting.Remove(addr)\n\t\t\tcontinue\n\t\t}\n\t\tif sweep != nil \u0026\u0026 sweep(c.To) {\n\t\t\trt.sweepClaim(d, c, q, \"bridge realm\")\n\t\t\tmoved = append(moved, c.Key)\n\t\t\tcontinue\n\t\t}\n\t\tdayCap := d.DayCap(lock)\n\t\td.meter.Refill(now, dayCap, dayCap, epoch, epochAt)\n\t\tq.meter.Refill(now, dayCap/4, dayCap/4, epoch, epochAt)\n\t\tavail := d.meter.Level\n\t\tif q.meter.Level \u003c avail {\n\t\t\tavail = q.meter.Level\n\t\t}\n\t\t// A claim payout never takes the escrow owed refunds reserve,\n\t\t// queued or proven.\n\t\tif free := sub0(lock, boundAdd(d.Totals.QueuedRefunds, d.Totals.ProvenOwed)); d.Config.Kind == KindUnescrow \u0026\u0026 avail \u003e free {\n\t\t\tavail = free\n\t\t}\n\t\trem := c.Remaining()\n\t\t// A cut is kept for good: a later raise never brings it back up.\n\t\tcfee := c.effectiveFee(d)\n\t\tc.Fee = cfee\n\t\tvar fee uint256\n\t\tif c.Paid == 0 {\n\t\t\tfee = cfee\n\t\t\tif fee \u003e rem {\n\t\t\t\tfee = rem\n\t\t\t}\n\t\t\tif avail == 0 || avail \u003c fee {\n\t\t\t\tcontinue // dry: the fee is the first installment, whole\n\t\t\t}\n\t\t} else {\n\t\t\tneed := cfee\n\t\t\tif need \u003e rem {\n\t\t\t\tneed = rem\n\t\t\t}\n\t\t\tif avail == 0 || avail \u003c need {\n\t\t\t\tcontinue // no slivers\n\t\t\t}\n\t\t}\n\t\t// fee is within rem and avail here, so neither subtraction wraps.\n\t\tpay := rem - fee\n\t\tif pay \u003e avail-fee {\n\t\t\tpay = avail - fee\n\t\t}\n\t\tif c.Paid == 0 \u0026\u0026 pay \u003e 0 {\n\t\t\t// No sliver after the fee either: the rest of a first installment\n\t\t\t// is paid only if it is at least min(what remains, the fee).\n\t\t\tneed := cfee\n\t\t\tif need \u003e rem-fee {\n\t\t\t\tneed = rem - fee\n\t\t\t}\n\t\t\tif pay \u003c need {\n\t\t\t\tpay = 0\n\t\t\t}\n\t\t}\n\t\ttotal := fee + pay\n\t\tc.Paid += total // at most the claim's amount\n\t\td.meter.Take(total)\n\t\tq.meter.Take(total)\n\t\td.Totals.Owed = mustSub(d.Totals.Owed, total)\n\t\td.Totals.Paid = statAdd(d.Totals.Paid, pay)\n\t\td.Totals.Fees = statAdd(d.Totals.Fees, fee)\n\t\tif d.Config.Kind == KindMint {\n\t\t\td.Totals.Outstanding = mustAdd(d.Totals.Outstanding, total)\n\t\t} else {\n\t\t\tlock -= total\n\t\t}\n\t\tif c.Remaining() == 0 {\n\t\t\tc.State = Done\n\t\t\trt.dequeue(d, c, q)\n\t\t}\n\t\tout = append(out, Payment{Key: c.Key, Denom: denom, To: c.To, Amount: pay, Fee: fee})\n\t}\n\treturn out, moved, nil\n}\n\n// sweepClaim moves an open claim to the sweep queue under its packet key:\n// its remainder leaves what is owed and becomes a live sweep entry, exactly,\n// carrying what the claim paid out already; the claim leaves its queue and\n// the claims. The credited statistic keeps it: it was credited.\nfunc (rt *Route) sweepClaim(d *Denom, c *Claim, q *queue, reason string) {\n\trem := c.Remaining()\n\trt.dequeue(d, c, q)\n\trt.claims.Remove(c.Key)\n\td.Totals.Owed = mustSub(d.Totals.Owed, rem)\n\td.Totals.Swept = mustAdd(d.Totals.Swept, rem) // within the liabilities already\n\trt.sweeps.Set(c.Key, \u0026SweepEntry{Key: c.Key, Denom: c.Denom, To: string(c.To), Amount: rem, Reason: reason, Reduced: c.Reduced,\n\t\tHeight: c.Height, FromClaim: true, ClaimPaid: c.Paid})\n}\n\n// dequeue takes a claim out of its queue; a queue left with no open claim\n// leaves the ring.\nfunc (rt *Route) dequeue(d *Denom, c *Claim, q *queue) {\n\tq.items.Remove(seqKey(c.Seq))\n\tif q.items.Size() == 0 {\n\t\td.waiting.Remove(string(c.To))\n\t}\n}\n\n// Waiting is how many recipients of a denom have an open claim.\nfunc (rt *Route) Waiting(denom string) int {\n\td := rt.Denom(denom)\n\tif d == nil {\n\t\treturn 0\n\t}\n\treturn d.waiting.Size()\n}\n\n// Send records and refunds are in refunds.gno; voids, credits by vote,\n// sweeps and the loss ledger in voids.gno.\n\nfunc parseKeys(keys []string) error {\n\tif len(keys) == 0 {\n\t\treturn errors.New(\"controls: no packet key\")\n\t}\n\tif len(keys) \u003e MaxKeys {\n\t\treturn ufmt.Errorf(\"controls: more than %d packet keys\", MaxKeys)\n\t}\n\tseen := map[string]bool{}\n\tfor _, k := range keys {\n\t\tif _, _, ok := ParsePacketKey(k); !ok {\n\t\t\treturn errors.New(\"controls: not a packet key: \" + k)\n\t\t}\n\t\tif seen[k] {\n\t\t\treturn errors.New(\"controls: packet key named twice: \" + k)\n\t\t}\n\t\tseen[k] = true\n\t}\n\treturn nil\n}\n"},{"name":"voids.gno","body":"package controls\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// CauseRefundForDelivered is the cause of a settlement's loss: a refund paid\n// for a packet the destination delivered.\nconst CauseRefundForDelivered = \"refund-for-delivered\"\n\n// Loss chains: where a recognized loss happened (ADR pr6375 §7.9).\nconst (\n\tChainGno      = \"gno\"\n\tChainEthereum = \"ethereum\"\n)\n\n// Loss is one entry of the loss ledger: a loss recognized by a decided void\n// or a reconciliation outcome, keyed by its packet (ADR pr6375 §7.9).\n// Monitors and guardians cannot record one.\ntype Loss struct {\n\tKey       string // packet key\n\tDirection string // DirRecv or DirSend: which of the client's packet spaces Key is in\n\tDenom     string\n\tAmount    uint256\n\tChain     string\n\tIncident  string\n\tCause     string // \"forged payout\", or the reconciliation outcome's words\n}\n\n// Recap is a recapitalization a decision authorized: Amount of Denom to be\n// paid into the escrow through the transfer app's Recapitalize, credited to\n// the accounted backing and debited from the unrecovered losses.\ntype Recap struct {\n\tID       int64\n\tDenom    string\n\tAmount   uint256\n\tIncident string\n\tPaid     bool\n\tPaidBy   address\n\tRevoked  bool // revoked by decision, or by an alternative resolution of its loss\n}\n\n// Packet directions of a loss: a packet received on gno.land (its claim or\n// sweep entry), or one gno.land sent (its send record). A client's inbound\n// and outbound sequences are distinct spaces, so the direction is part of a\n// packet's identity.\nconst (\n\tDirRecv = \"recv\"\n\tDirSend = \"send\"\n)\n\n// lossID is a packet's economic-loss identity, and the key of its entry in\n// the ledger: one loss per packet and direction, whatever its cause (kept as\n// the entry's metadata), so no payout is booked twice through two paths (a\n// void, a settlement, a recorded loss), and the two directions of one client\n// and sequence are two entries.\nfunc lossID(dir, key string) string { return dir + \"|\" + key }\n\n// LossBooked reports whether a loss of the packet key in direction dir is in\n// the ledger.\nfunc (rt *Route) LossBooked(dir, key string) bool { return rt.losses.Has(lossID(dir, key)) }\n\n// IncidentTotals is the loss ledger of one incident on one denom: what it\n// lost and how much of that each restoring decision resolved. Resolutions of\n// an incident never exceed its losses, and an authorized recapitalization not\n// yet paid holds its part (ADR pr6375 §7.9).\ntype IncidentTotals struct {\n\t// Lifetime statistics, saturating, never read by a check.\n\tLost          uint256\n\tRecapitalized uint256\n\tReduced       uint256\n\tAccepted      uint256\n\t// The outstanding balances (see Totals).\n\tOpenUnrecovered uint256\n\tOpenUnresolved  uint256\n\t// Mirror marks an incident whose losses were paid out on Ethereum\n\t// (RecordLoss with chain \"ethereum\"): Ethereum's ledger is the\n\t// authoritative one (the payout chain books a loss), and the record here\n\t// is a mirror, resolved only by an accepted deficit, never reduced or\n\t// recapitalized. An incident holds mirrors or authoritative losses, never\n\t// both (Booked: it holds a loss, so its kind is fixed).\n\tMirror bool\n\tBooked bool\n}\n\n// checkIncidentKind is why a loss, a mirror or not, cannot be booked under\n// incident on denom, or nil: an incident holds one kind only.\nfunc (rt *Route) checkIncidentKind(denom, incident string, mirror bool) error {\n\tit := rt.Incident(denom, incident)\n\tif it.Booked \u0026\u0026 it.Mirror != mirror {\n\t\tif it.Mirror {\n\t\t\treturn errors.New(\"controls: incident \" + incident + \" holds mirrors of losses paid on Ethereum: book a loss paid here under another incident\")\n\t\t}\n\t\treturn errors.New(\"controls: incident \" + incident + \" holds losses paid here: book a mirror of a loss paid on Ethereum under another incident\")\n\t}\n\treturn nil\n}\n\n// checkRemedy is why a reduction or a recapitalization cannot act on\n// incident's losses on denom, or nil: a mirror is resolved only by an\n// accepted deficit, the payout chain's ledger carrying the remedies.\nfunc (rt *Route) checkRemedy(denom, incident string) error {\n\tif rt.Incident(denom, incident).Mirror {\n\t\treturn errors.New(\"controls: incident \" + incident + \" mirrors losses paid on Ethereum, whose ledger remedies them: resolve the mirror by an accepted deficit only\")\n\t}\n\treturn nil\n}\n\nfunc incidentKey(denom, incident string) string { return denom + \"|\" + incident }\n\nfunc (rt *Route) incident(denom, incident string) *IncidentTotals {\n\tk := incidentKey(denom, incident)\n\tif v := rt.incidents.Get(k); v != nil {\n\t\treturn v.(*IncidentTotals)\n\t}\n\tit := \u0026IncidentTotals{}\n\trt.incidents.Set(k, it)\n\treturn it\n}\n\n// Incident is the loss ledger of an incident on a denom, by value.\nfunc (rt *Route) Incident(denom, incident string) IncidentTotals {\n\tif v := rt.incidents.Get(incidentKey(denom, incident)); v != nil {\n\t\treturn *v.(*IncidentTotals)\n\t}\n\treturn IncidentTotals{}\n}\n\n// recapitalizable is what of an incident's losses on denom a\n// recapitalization may still restore: its unrecovered losses (accepted\n// deficits included: accepting a deficit restores no backing, so a later\n// recapitalization may), minus the recapitalizations authorized and not\n// paid (except skip, an authorization being paid).\nfunc (rt *Route) recapitalizable(denom, incident string, skip int64) uint256 {\n\treturn sub0(rt.Incident(denom, incident).OpenUnrecovered, rt.openRecaps(denom, incident, skip))\n}\n\n// Voidable reports whether a void as forged (or as withheld) can take the\n// claim or sweep entry of key as it stands: an open claim either way, a\n// withheld or paid claim as forged only, a sweep entry as forged only: a live\n// one, or one a sweep decision paid (its payout becomes the loss), not one a\n// credit by vote replaced (its claim carries the payout).\nfunc (rt *Route) Voidable(key string, forged bool) bool {\n\tif rt.LossBooked(DirRecv, key) {\n\t\treturn false // its loss is booked (a forged-receive settlement or a recorded loss)\n\t}\n\tif c := rt.claim(key); c != nil {\n\t\tswitch c.State {\n\t\tcase Open:\n\t\t\treturn true\n\t\tcase Withheld, Done:\n\t\t\treturn forged\n\t\t}\n\t\treturn false\n\t}\n\tif e := rt.sweep(key); e != nil {\n\t\treturn forged \u0026\u0026 !e.Voided \u0026\u0026 (!e.Paid || e.PaidTo != PaidByVote)\n\t}\n\treturn false\n}\n\n// PaidByVote is a sweep entry's PaidTo when a credit by vote replaced it.\nconst PaidByVote = \"credit by vote\"\n\n// Void voids the claims and sweep entries of keys (ADR pr6375 §7.9). A\n// withheld void keeps a claim's remainder booked and restorable; a forged void\n// drops it and books what was already paid as a loss. A void never creates a\n// refund at the source: the packet was acknowledged as a success. All keys are\n// checked before anything changes.\nfunc (rt *Route) Void(keys []string, forged bool, incident string) error {\n\tif err := parseKeys(keys); err != nil {\n\t\treturn err\n\t}\n\tlosses := map[string]uint256{}\n\tfor _, k := range keys {\n\t\tif !rt.Voidable(k, forged) {\n\t\t\treturn errors.New(\"controls: \" + k + \" cannot be voided \" + voidWord(forged) + \" as it stands\")\n\t\t}\n\t\t// A forged void books a loss paid here: never under a mirror\n\t\t// incident. A withheld one books none, so any incident takes it.\n\t\tif !forged {\n\t\t\tcontinue\n\t\t}\n\t\tvar denom string\n\t\tvar paid uint256\n\t\tif c := rt.claim(k); c != nil {\n\t\t\tdenom, paid = c.Denom, c.Paid\n\t\t} else if e := rt.sweep(k); e != nil {\n\t\t\tdenom, paid = e.Denom, e.paidOut()\n\t\t}\n\t\tif err := rt.checkIncidentKind(denom, incident, false); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !addLossTo(losses, denom, paid) {\n\t\t\treturn errLossOverflow\n\t\t}\n\t}\n\t// The losses the batch books, summed per denom, must fit the ledger's\n\t// balances before anything changes: each is within a paid claim or\n\t// entry, but the balances hold every earlier loss too.\n\tif err := rt.lossesFit(losses, incident); err != nil {\n\t\treturn err\n\t}\n\tfor _, k := range keys {\n\t\tif c := rt.claim(k); c != nil {\n\t\t\trt.voidClaim(c, forged, incident)\n\t\t\tcontinue\n\t\t}\n\t\te := rt.sweep(k)\n\t\te.Voided = true\n\t\td := rt.Denom(e.Denom)\n\t\tif !e.Paid {\n\t\t\td.Totals.Swept = mustSub(d.Totals.Swept, e.Amount)\n\t\t}\n\t\t// A paid entry's payout left the swept total when it was paid: what\n\t\t// the sweep decision paid, and what a claim moved here had paid, is\n\t\t// the loss, booked once.\n\t\tif paid := e.paidOut(); paid \u003e 0 {\n\t\t\trt.addLoss(d, Loss{Key: k, Direction: DirRecv, Denom: e.Denom, Amount: paid, Chain: ChainGno, Incident: incident, Cause: \"forged payout\"})\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc voidWord(forged bool) string {\n\tif forged {\n\t\treturn \"as forged\"\n\t}\n\treturn \"as withheld\"\n}\n\nfunc (rt *Route) voidClaim(c *Claim, forged bool, incident string) {\n\td := rt.Denom(c.Denom)\n\trem := c.Remaining()\n\tswitch c.State {\n\tcase Open:\n\t\td.Totals.Owed = mustSub(d.Totals.Owed, rem)\n\t\trt.dequeue(d, c, d.queues.Get(string(c.To)).(*queue))\n\t\tif forged {\n\t\t\tc.State = Forged\n\t\t} else {\n\t\t\td.Totals.Withheld = mustAdd(d.Totals.Withheld, rem)\n\t\t\tc.State = Withheld\n\t\t}\n\tcase Withheld:\n\t\td.Totals.Withheld = mustSub(d.Totals.Withheld, rem)\n\t\tc.State = Forged\n\tcase Done:\n\t\tc.State = Forged\n\t}\n\tif forged \u0026\u0026 c.Paid \u003e 0 {\n\t\trt.addLoss(d, Loss{Key: c.Key, Direction: DirRecv, Denom: c.Denom, Amount: c.Paid, Chain: ChainGno, Incident: incident, Cause: \"forged payout\"})\n\t}\n}\n\n// The loss ledger's remedies follow one rule (ADR 0004, R13). A remedy that\n// restores backing or lowers liabilities (a paid recapitalization, a\n// reduction) is bounded by the incident's unrecovered losses and lowers the\n// unresolved ones only as far as they remain: it may repair an accepted\n// deficit. An accepted deficit (Accept, or a reopening) is bounded by the\n// unresolved losses and lowers only them: the loss stays unrecovered.\n\n// accept records amount, which its caller checked is within the incident's\n// unresolved losses, as an accepted deficit: off the unresolved balances of\n// the denom and the incident only.\nfunc (rt *Route) accept(denom, incident string, amount uint256) {\n\td := rt.Denom(denom)\n\tt := \u0026d.Totals\n\tit := rt.incident(denom, incident)\n\tif amount \u003e it.OpenUnresolved || amount \u003e t.OpenUnresolved {\n\t\tpanic(\"controls: an acceptance beyond the unresolved losses\")\n\t}\n\tdefer rt.noteUnresolved(d, t.OpenUnresolved)\n\tt.OpenUnresolved -= amount\n\tit.OpenUnresolved -= amount\n\tt.Accepted, it.Accepted = statAdd(t.Accepted, amount), statAdd(it.Accepted, amount)\n}\n\n// acceptAll records the whole of an incident's unresolved losses on denom as\n// an accepted deficit (a reopening's batch).\nfunc (rt *Route) acceptAll(denom, incident string) {\n\td := rt.Denom(denom)\n\tt := \u0026d.Totals\n\tit := rt.incident(denom, incident)\n\tdefer rt.noteUnresolved(d, t.OpenUnresolved)\n\tstat := it.OpenUnresolved\n\tt.OpenUnresolved = mustSub(t.OpenUnresolved, stat) // the incident's part is within the denom's\n\tit.OpenUnresolved = 0\n\tt.Accepted, it.Accepted = statAdd(t.Accepted, stat), statAdd(it.Accepted, stat)\n}\n\n// recover takes a paid recapitalization or a reduction off the outstanding\n// balances: off the unrecovered losses, and off the unresolved ones as far\n// as they go (an accepted deficit it repairs is resolved already). Its\n// caller checked amount is within the incident's unrecovered losses.\nfunc (rt *Route) recover(denom, incident string, amount uint256) {\n\td := rt.Denom(denom)\n\tt := \u0026d.Totals\n\tit := rt.incident(denom, incident)\n\tif amount \u003e it.OpenUnrecovered || amount \u003e t.OpenUnrecovered {\n\t\tpanic(\"controls: a recovery beyond the unrecovered losses\")\n\t}\n\tdefer rt.noteUnresolved(d, t.OpenUnresolved)\n\tx := amount\n\tif x \u003e it.OpenUnresolved {\n\t\tx = it.OpenUnresolved\n\t}\n\tt.OpenUnrecovered -= amount\n\tit.OpenUnrecovered -= amount\n\tt.OpenUnresolved = mustSub(t.OpenUnresolved, x) // the incident's unresolved part is within the denom's\n\tit.OpenUnresolved -= x\n}\n\nvar errLossOverflow = errors.New(\"controls: the loss would overflow the ledger's balances\")\n\n// lossFits refuses a loss the ledger's outstanding balances (the denom's and\n// the incident's unrecovered and unresolved losses) could not hold without\n// passing 2^256 - 1. Lifetime statistics are not checked: they saturate, and\n// a resolved loss frees its capacity.\nfunc (rt *Route) lossFits(denom, incident string, amount uint256) error {\n\td := rt.Denom(denom)\n\tif d == nil {\n\t\treturn errors.New(\"controls: a loss of \" + denom + \" is a positive amount of an enabled denom\")\n\t}\n\tit := rt.Incident(denom, incident)\n\tfor _, b := range []uint256{d.Totals.OpenUnrecovered, d.Totals.OpenUnresolved, it.OpenUnrecovered, it.OpenUnresolved} {\n\t\tif _, ok := checkedAdd(b, amount); !ok {\n\t\t\treturn errLossOverflow\n\t\t}\n\t}\n\treturn nil\n}\n\n// lossesFit is lossFits for the losses a batch books under incident, summed\n// per denom (addLossTo): a forged void of several payouts, a settlement's\n// forged refunds and receives. Each loss is within a paid claim, entry or\n// refund, but the balances hold every earlier loss too, so the batch is\n// checked whole before anything changes.\nfunc (rt *Route) lossesFit(losses map[string]uint256, incident string) error {\n\tfor denom, amount := range losses {\n\t\tif err := rt.lossFits(denom, incident, amount); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// addLossTo adds amount to losses[denom], and whether the sum fits.\nfunc addLossTo(losses map[string]uint256, denom string, amount uint256) bool {\n\ts, ok := checkedAdd(losses[denom], amount)\n\tif ok {\n\t\tlosses[denom] = s\n\t}\n\treturn ok\n}\n\n// addLoss books a loss its caller checked fits (lossFits or lossesFit); an\n// addition that would still overflow is a broken invariant and aborts.\nfunc (rt *Route) addLoss(d *Denom, l Loss) {\n\tit := rt.incident(l.Denom, l.Incident)\n\tif l.Direction != DirRecv \u0026\u0026 l.Direction != DirSend || rt.losses.Has(lossID(l.Direction, l.Key)) {\n\t\tpanic(\"controls: a packet's loss is booked once\")\n\t}\n\trt.losses.Set(lossID(l.Direction, l.Key), \u0026l)\n\tif !it.Booked {\n\t\tit.Booked, it.Mirror = true, l.Chain == ChainEthereum\n\t}\n\twas := d.Totals.OpenUnresolved\n\td.Totals.OpenUnrecovered = mustAdd(d.Totals.OpenUnrecovered, l.Amount)\n\tit.OpenUnrecovered = mustAdd(it.OpenUnrecovered, l.Amount)\n\td.Totals.OpenUnresolved = mustAdd(d.Totals.OpenUnresolved, l.Amount)\n\tit.OpenUnresolved = mustAdd(it.OpenUnresolved, l.Amount)\n\trt.noteUnresolved(d, was)\n\td.Totals.Lost, it.Lost = statAdd(d.Totals.Lost, l.Amount), statAdd(it.Lost, l.Amount)\n\trt.lossesBooked++\n}\n\n// Restore restores withheld claims (ADR pr6375 §7.9: a withheld void can be\n// restored): each goes back into its recipient's queue at its place, opening\n// one for it if it has none (as of now in epoch). A withheld claim's\n// remainder stayed booked (reserved escrow, or counted against the supply\n// cap), so restoring it books nothing new. A forged void and a voided sweep\n// entry are final: their remainders were released, and other credits may\n// hold that escrow or supply since.\nfunc (rt *Route) Restore(keys []string, now, epoch int64) error {\n\tif err := parseKeys(keys); err != nil {\n\t\treturn err\n\t}\n\tfor _, k := range keys {\n\t\tif c := rt.claim(k); c == nil || c.State != Withheld {\n\t\t\treturn errors.New(\"controls: \" + k + \" is no withheld claim (only a withheld void is restored)\")\n\t\t}\n\t}\n\tfor _, k := range keys {\n\t\trt.restoreClaim(rt.claim(k), now, epoch)\n\t}\n\treturn nil\n}\n\nfunc (rt *Route) restoreClaim(c *Claim, now, epoch int64) {\n\td := rt.Denom(c.Denom)\n\trem := c.Remaining()\n\td.Totals.Withheld = mustSub(d.Totals.Withheld, rem)\n\tif rem == 0 {\n\t\tc.State = Done\n\t\treturn\n\t}\n\tc.State = Open\n\td.Totals.Owed = mustAdd(d.Totals.Owed, rem)\n\tq := d.queue(c.To, now, epoch)\n\tq.items.Set(seqKey(c.Seq), c.Key)\n\td.waiting.Set(string(c.To), true)\n}\n\n// CreditByVote credits a packet by decision, the manual fallback for a\n// griefed transfer (ADR pr6375 §7.9). The caller checked that the packet's\n// acknowledgement on this chain is a success: an error-acknowledged packet is\n// refunded at its source and never credited. Credits are deduplicated\n// against every claim and payment of the packet: only a receive these\n// controls booked, a live sweep entry recorded at its receipt after the\n// route's activation, is credited, which replaces the entry. A receive from\n// before the activation was paid at once by impl/v0 and is never credited\n// again. The amount is the sweep entry's whole amount (any haircut goes\n// through Reduce), at least the fee, and for an unescrowed denom never above\n// the free lock.\nfunc (rt *Route) CreditByVote(now, height, epoch int64, key, denom string, to address, amount, lock uint256) error {\n\tif err := rt.checkPrepared(); err != nil {\n\t\treturn err\n\t}\n\tif _, _, ok := ParsePacketKey(key); !ok {\n\t\treturn errors.New(\"controls: not a packet key: \" + key)\n\t}\n\tif rt.claims.Has(key) {\n\t\treturn errors.New(\"controls: packet \" + key + \" has a claim already\")\n\t}\n\td := rt.Denom(denom)\n\tif d == nil {\n\t\treturn errors.New(\"controls: denom \" + denom + \" is not enabled on route \" + rt.Key)\n\t}\n\tif !to.IsValid() {\n\t\treturn errors.New(\"controls: invalid recipient \" + string(to))\n\t}\n\te := rt.sweep(key)\n\tif e == nil || e.Height \u003c rt.Activated {\n\t\treturn errors.New(\"controls: packet \" + key + \" was not received under the route's controls (no sweep entry booked at its receipt): a receive from before the activation was paid already\")\n\t}\n\tif e.Voided || e.Paid || e.Denom != denom {\n\t\treturn errors.New(\"controls: packet \" + key + \"'s sweep entry is voided, paid or of another denom\")\n\t}\n\tif e.FromClaim {\n\t\treturn errors.New(\"controls: packet \" + key + \" was credited as a claim already, moved to the sweep queue: a sweep decision pays it\")\n\t}\n\t// The full payable amount: a credit never drops part of an entitlement\n\t// (a haircut goes through Reduce, against an incident).\n\tif amount != e.Amount || amount \u003c d.Config.Fee {\n\t\treturn ufmt.Errorf(\"controls: a credit by vote is the sweep entry's whole amount (%s), at least the fee (%s), not %s\", u(e.Amount), u(d.Config.Fee), u(amount))\n\t}\n\tif free := sub0(lock, d.Reserved()); d.Config.Kind == KindUnescrow \u0026\u0026 amount \u003e free {\n\t\treturn ufmt.Errorf(\"controls: %s is above the free lock of %s\", u(amount), u(free))\n\t}\n\t// A minted credit replaces its sweep entry within the supply already\n\t// booked for it: amount never exceeds the entry.\n\te.Paid = true\n\te.PaidTo = PaidByVote\n\td.Totals.Swept = mustSub(d.Totals.Swept, e.Amount)\n\trt.enqueue(d, now, height, epoch, key, to, amount, ByVote)\n\treturn nil\n}\n\n// CreditPending credits by decision a packet received on the route whose\n// asynchronous acknowledgement is still unwritten (ADR pr6375 §7.9): the\n// caller checked that the core received it after the route's activation and\n// writes its success acknowledgement in the same transaction, so the packet\n// can never be refunded at its source. Nothing was booked at its receipt, so\n// it has no claim or sweep entry; carried is what the packet carried, which\n// the amount must be (a haircut goes through Reduce), at least the fee; for\n// an unescrowed denom it is never above the free lock, for a minted one never\n// past the supply cap.\nfunc (rt *Route) CreditPending(now, height, epoch int64, key, denom string, to address, amount, carried, lock uint256) error {\n\tif _, _, ok := ParsePacketKey(key); !ok {\n\t\treturn errors.New(\"controls: not a packet key: \" + key)\n\t}\n\tif rt.claims.Has(key) || rt.sweeps.Has(key) {\n\t\treturn errors.New(\"controls: packet \" + key + \" is credited already\")\n\t}\n\td := rt.Denom(denom)\n\tif d == nil {\n\t\treturn errors.New(\"controls: denom \" + denom + \" is not enabled on route \" + rt.Key)\n\t}\n\tif !to.IsValid() {\n\t\treturn errors.New(\"controls: invalid recipient \" + string(to))\n\t}\n\t// The full payable amount: the success acknowledgement written with it\n\t// ends the packet's entitlement at the source.\n\tif amount != carried || amount \u003c d.Config.Fee {\n\t\treturn ufmt.Errorf(\"controls: a credit by vote is what the packet carried (%s), at least the fee (%s), not %s\", u(carried), u(d.Config.Fee), u(amount))\n\t}\n\tif free := sub0(lock, d.Reserved()); d.Config.Kind == KindUnescrow \u0026\u0026 amount \u003e free {\n\t\treturn ufmt.Errorf(\"controls: %s is above the free lock of %s\", u(amount), u(free))\n\t}\n\tif _, ok := checkedAdd(d.Totals.liabilities(), amount); !ok {\n\t\treturn errors.New(\"controls: the amount would overflow what is owed\")\n\t}\n\tif d.Config.Kind == KindMint \u0026\u0026 amount \u003e sub0(d.Config.SupplyCap, d.Totals.booked()) {\n\t\treturn ufmt.Errorf(\"controls: %s would take %s past its supply cap\", u(amount), denom)\n\t}\n\trt.enqueue(d, now, height, epoch, key, to, amount, ByVote)\n\treturn nil\n}\n\n// PaySweeps pays live sweep entries of one denom to an address by decision\n// (ADR pr6375 §7.1: owed, paid only by a 7-day decision). For an unescrowed\n// denom the total never exceeds the free lock (what open claims do not\n// reserve). It books them and returns the\n// payments.\nfunc (rt *Route) PaySweeps(keys []string, to address, lock uint256) ([]Payment, error) {\n\tif err := rt.checkPrepared(); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := parseKeys(keys); err != nil {\n\t\treturn nil, err\n\t}\n\tif !to.IsValid() {\n\t\treturn nil, errors.New(\"controls: invalid recipient \" + string(to))\n\t}\n\tdenom := \"\"\n\tvar total uint256\n\tfor _, k := range keys {\n\t\te := rt.sweep(k)\n\t\tif e == nil || e.Voided || e.Paid {\n\t\t\treturn nil, errors.New(\"controls: \" + k + \" is no live sweep entry\")\n\t\t}\n\t\tif denom == \"\" {\n\t\t\tdenom = e.Denom\n\t\t} else if e.Denom != denom {\n\t\t\treturn nil, errors.New(\"controls: a sweep payment names entries of one denom\")\n\t\t}\n\t\tvar ok bool\n\t\t// A payment past 2^256 - 1 could never be made at once: refused, a\n\t\t// smaller decision pays.\n\t\tif total, ok = checkedAdd(total, e.Amount); !ok {\n\t\t\treturn nil, errors.New(\"controls: the sweep payment's total would overflow\")\n\t\t}\n\t}\n\td := rt.Denom(denom)\n\tif free := sub0(lock, d.Reserved()); d.Config.Kind == KindUnescrow \u0026\u0026 total \u003e free {\n\t\treturn nil, ufmt.Errorf(\"controls: %s is above the free lock of %s\", u(total), u(free))\n\t}\n\t// A minted payment adds to the outstanding supply: refused whole, the\n\t// entries kept, when it would overflow it.\n\tif _, ok := checkedAdd(d.Totals.Outstanding, total); d.Config.Kind == KindMint \u0026\u0026 !ok {\n\t\treturn nil, ufmt.Errorf(\"controls: %s of %s would overflow its outstanding supply\", u(total), denom)\n\t}\n\tvar out []Payment\n\tfor _, k := range keys {\n\t\te := rt.sweep(k)\n\t\te.Paid, e.PaidTo = true, string(to)\n\t\td.Totals.Swept = mustSub(d.Totals.Swept, e.Amount)\n\t\td.Totals.SweepPaid = statAdd(d.Totals.SweepPaid, e.Amount)\n\t\tif d.Config.Kind == KindMint {\n\t\t\td.Totals.Outstanding = mustAdd(d.Totals.Outstanding, e.Amount)\n\t\t}\n\t\tout = append(out, Payment{Key: k, Denom: denom, To: to, Amount: e.Amount})\n\t}\n\treturn out, nil\n}\n\n// RecordLoss records a loss a reconciliation recognized (ADR pr6375 §7.12):\n// a refund paid for a delivered packet, a forged delivery, a collision. cause\n// says which, in words; one entry per packet and cause.\nfunc (rt *Route) RecordLoss(key, denom string, amount uint256, chain, incident, cause string) error {\n\tif _, _, ok := ParsePacketKey(key); !ok {\n\t\treturn errors.New(\"controls: not a packet key: \" + key)\n\t}\n\td := rt.Denom(denom)\n\tif d == nil {\n\t\treturn errors.New(\"controls: denom \" + denom + \" is not enabled on route \" + rt.Key)\n\t}\n\tif amount == 0 {\n\t\treturn errors.New(\"controls: a loss is positive\")\n\t}\n\tif chain != ChainGno \u0026\u0026 chain != ChainEthereum {\n\t\treturn errors.New(\"controls: a loss happens on gno or ethereum\")\n\t}\n\tif incident == \"\" || cause == \"\" || cause == \"forged payout\" {\n\t\treturn errors.New(\"controls: a loss names its incident and a reconciliation cause\")\n\t}\n\t// A refund paid for a delivered packet, and a forged receive, are booked\n\t// by the settlement of their record (kind 12 actions 4 and 5), which\n\t// also settles it: recording one on its own would leave the record\n\t// unsettled and block the reopening.\n\tif cause == CauseRefundForDelivered || cause == CauseForgedReceive {\n\t\treturn errors.New(\"controls: a \" + cause + \" loss is booked by settling its record\")\n\t}\n\t// A loss on gno.land is of a packet gno.land received; one on Ethereum,\n\t// of a sequence gno.land sent (a collision with an honest send, or a\n\t// delivery of a sequence it never sent).\n\tdir := DirSend\n\tif chain == ChainGno {\n\t\tdir = DirRecv\n\t\t// A receive represented here (a claim or a sweep entry) books its\n\t\t// loss by its own operation: a void as forged, or a forged-receive\n\t\t// settlement, which also settle the claim or entry.\n\t\tif rt.claim(key) != nil || rt.sweep(key) != nil {\n\t\t\treturn errors.New(\"controls: receive \" + key + \" is represented here: void it as forged or settle it (action 5)\")\n\t\t}\n\t}\n\tif rt.LossBooked(dir, key) {\n\t\treturn errors.New(\"controls: a loss of packet \" + key + \" is recorded already\")\n\t}\n\tif err := rt.lossFits(denom, incident, amount); err != nil {\n\t\treturn err\n\t}\n\tif err := rt.checkIncidentKind(denom, incident, chain == ChainEthereum); err != nil {\n\t\treturn err\n\t}\n\trt.addLoss(d, Loss{Key: key, Direction: dir, Denom: denom, Amount: amount, Chain: chain, Incident: incident, Cause: cause})\n\treturn nil\n}\n\n// AuthorizeRecap records a recapitalization a decision authorized (ADR\n// pr6375 §7.9): amount, at most what of the incident's losses on the denom no\n// decision resolved or reserved yet, to be paid into the escrow. It returns\n// the authorization's id.\nfunc (rt *Route) AuthorizeRecap(denom string, amount uint256, incident string) (int64, error) {\n\td := rt.Denom(denom)\n\tif d == nil {\n\t\treturn 0, errors.New(\"controls: denom \" + denom + \" is not enabled on route \" + rt.Key)\n\t}\n\tif d.Config.Kind != KindUnescrow {\n\t\treturn 0, errors.New(\"controls: only an escrowed denom is recapitalized on gno.land\")\n\t}\n\tif incident == \"\" {\n\t\treturn 0, errors.New(\"controls: a recapitalization names its incident\")\n\t}\n\tif err := rt.checkRemedy(denom, incident); err != nil {\n\t\treturn 0, err\n\t}\n\tif amount == 0 || amount \u003e rt.recapitalizable(denom, incident, 0) {\n\t\treturn 0, ufmt.Errorf(\"controls: %s is not within incident %s's unrecovered losses not yet authorized\", u(amount), incident)\n\t}\n\tp := openRecapPrefix(denom, incident)\n\topen := 0\n\trt.openRecapIdx.Iterate(p, p+\"~\", func(string, any) bool {\n\t\topen++\n\t\treturn open == MaxOpenRecaps\n\t})\n\tif open \u003e= MaxOpenRecaps {\n\t\treturn 0, ufmt.Errorf(\"controls: incident %s holds %d unpaid authorizations on %s already: pay or revoke one first\", incident, MaxOpenRecaps, denom)\n\t}\n\trt.nRecaps++\n\tr := \u0026Recap{ID: rt.nRecaps, Denom: denom, Amount: amount, Incident: incident}\n\trt.recaps.Set(seqKey(rt.nRecaps), r)\n\trt.openRecapIdx.Set(openRecapKey(r), r)\n\treturn rt.nRecaps, nil\n}\n\n// openRecaps is what the unpaid recapitalizations of an incident on denom\n// hold, the authorization skip aside: a walk of that incident's open\n// authorizations only (at most MaxOpenRecaps).\nfunc (rt *Route) openRecaps(denom, incident string, skip int64) uint256 {\n\tvar n uint256\n\tp := openRecapPrefix(denom, incident)\n\trt.openRecapIdx.Iterate(p, p+\"~\", func(_ string, v any) bool {\n\t\tif r := v.(*Recap); r.ID != skip {\n\t\t\tn = mustAdd(n, r.Amount) // each within the incident's losses\n\t\t}\n\t\treturn false\n\t})\n\treturn n\n}\n\nfunc openRecapPrefix(denom, incident string) string { return incidentKey(denom, incident) + \"|\" }\n\nfunc openRecapKey(r *Recap) string { return openRecapPrefix(r.Denom, r.Incident) + seqKey(r.ID) }\n\n// closeRecap takes an authorization out of the open index, as it is paid or\n// revoked.\nfunc (rt *Route) closeRecap(r *Recap) { rt.openRecapIdx.Remove(openRecapKey(r)) }\n\n// unresolvedOf is what of an incident's losses on denom no decision resolved\n// (recapitalized, reduced or accepted), unpaid authorizations aside: what an\n// acceptance may take.\nfunc (rt *Route) unresolvedOf(denom, incident string) uint256 {\n\treturn rt.Incident(denom, incident).OpenUnresolved\n}\n\n// unrecoveredOf is what of an incident's losses on denom is not recovered\n// (recapitalized or reduced), accepted deficits included: what a reduction\n// may take.\nfunc (rt *Route) unrecoveredOf(denom, incident string) uint256 {\n\treturn rt.Incident(denom, incident).OpenUnrecovered\n}\n\n// revokeConflicting revokes the newest unpaid recapitalization authorizations\n// of an incident until a reduction of amount leaves them within what the\n// incident still has unrecovered: a reduction decided later supersedes them\n// (ADR pr6375 §7.9). An accepted deficit supersedes none: a recapitalization\n// may still restore it. The caller checked amount fits unrecoveredOf. One\n// pass over the incident's open authorizations (at most MaxOpenRecaps): their\n// total once, then the newest revoked first while it exceeds what remains.\nfunc (rt *Route) revokeConflicting(denom, incident string, amount uint256) {\n\tvar open []*Recap\n\tvar total uint256\n\tp := openRecapPrefix(denom, incident)\n\trt.openRecapIdx.Iterate(p, p+\"~\", func(_ string, v any) bool {\n\t\tr := v.(*Recap)\n\t\topen = append(open, r)\n\t\ttotal = mustAdd(total, r.Amount)\n\t\treturn false\n\t})\n\t// What remains after the reduction (the caller checked amount fits).\n\troom := mustSub(rt.Incident(denom, incident).OpenUnrecovered, amount)\n\tfor i := len(open) - 1; i \u003e= 0 \u0026\u0026 room \u003c total; i-- {\n\t\ttotal -= open[i].Amount\n\t\trt.closeRecap(open[i])\n\t\topen[i].Revoked = true\n\t}\n}\n\n// RevokeRecap revokes an unpaid recapitalization authorization by decision,\n// one whose funding fell through: its part of the incident's losses is free\n// for another restoring decision.\nfunc (rt *Route) RevokeRecap(id int64) error {\n\tv := rt.recaps.Get(seqKey(id))\n\tif v == nil {\n\t\treturn ufmt.Errorf(\"controls: no recapitalization %d\", id)\n\t}\n\tr := v.(*Recap)\n\tif r.Paid || r.Revoked {\n\t\treturn ufmt.Errorf(\"controls: recapitalization %d is paid or revoked already\", id)\n\t}\n\trt.closeRecap(r)\n\tr.Revoked = true\n\treturn nil\n}\n\n// Recap is a recapitalization authorization by id, by value.\nfunc (rt *Route) Recap(id int64) (Recap, bool) {\n\tv := rt.recaps.Get(seqKey(id))\n\tif v == nil {\n\t\treturn Recap{}, false\n\t}\n\treturn *v.(*Recap), true\n}\n\n// Recapitalize books an authorized recapitalization as paid: the caller moved\n// exactly its amount into the escrow, which now backs it, and the loss\n// ledger's unrecovered total falls by it.\nfunc (rt *Route) Recapitalize(id int64, denom string, amount uint256, by address) error {\n\tv := rt.recaps.Get(seqKey(id))\n\tif v == nil {\n\t\treturn ufmt.Errorf(\"controls: no recapitalization %d\", id)\n\t}\n\tr := v.(*Recap)\n\tif r.Paid || r.Revoked {\n\t\treturn ufmt.Errorf(\"controls: recapitalization %d is paid or revoked\", id)\n\t}\n\tif r.Denom != denom || r.Amount != amount {\n\t\treturn ufmt.Errorf(\"controls: recapitalization %d is %s %s\", id, u(r.Amount), r.Denom)\n\t}\n\t// Re-checked at payment: the incident's losses must still hold it.\n\tif amount \u003e rt.recapitalizable(denom, r.Incident, id) {\n\t\treturn ufmt.Errorf(\"controls: recapitalization %d is above what incident %s still has unrecovered\", id, r.Incident)\n\t}\n\trt.closeRecap(r)\n\tr.Paid, r.PaidBy = true, by\n\trt.recover(denom, r.Incident, amount)\n\tt := \u0026rt.Denom(denom).Totals\n\tit := rt.incident(denom, r.Incident)\n\tt.Recapitalized, it.Recapitalized = statAdd(t.Recapitalized, amount), statAdd(it.Recapitalized, amount)\n\treturn nil\n}\n\n// Reduce cuts an open or withheld claim, or an unpaid owed sweep entry, by\n// amount against an incident: an explicit liability reduction (ADR pr6375\n// §7.9, the claims term of §7.11 includes the sweep entries owed). The claims\n// term and the unrecovered losses fall by the same amount; refunds are never\n// reduced.\nfunc (rt *Route) Reduce(key string, amount uint256, incident string) error {\n\tc := rt.claim(key)\n\tif c == nil {\n\t\treturn rt.reduceSweep(key, amount, incident)\n\t}\n\tif c.State != Open \u0026\u0026 c.State != Withheld {\n\t\treturn errors.New(\"controls: claim \" + key + \" is \" + c.State)\n\t}\n\td := rt.Denom(c.Denom)\n\tif incident == \"\" {\n\t\treturn errors.New(\"controls: a reduction names its incident\")\n\t}\n\tif err := rt.checkRemedy(c.Denom, incident); err != nil {\n\t\treturn err\n\t}\n\tif amount == 0 || amount \u003e c.Remaining() || amount \u003e rt.unrecoveredOf(c.Denom, incident) {\n\t\treturn ufmt.Errorf(\"controls: %s is not within the claim's remainder and incident %s's unrecovered losses\", u(amount), incident)\n\t}\n\trt.revokeConflicting(c.Denom, incident, amount)\n\tc.Amount -= amount\n\tc.Reduced += amount // at most the claim's amount\n\trt.recover(c.Denom, incident, amount)\n\tit := rt.incident(c.Denom, incident)\n\td.Totals.Reduced, it.Reduced = statAdd(d.Totals.Reduced, amount), statAdd(it.Reduced, amount)\n\tif c.State == Open {\n\t\td.Totals.Owed = mustSub(d.Totals.Owed, amount)\n\t\tif c.Remaining() == 0 {\n\t\t\tc.State = Done\n\t\t\trt.dequeue(d, c, d.queues.Get(string(c.To)).(*queue))\n\t\t}\n\t} else {\n\t\td.Totals.Withheld = mustSub(d.Totals.Withheld, amount)\n\t}\n\treturn nil\n}\n\n// reduceSweep cuts a live sweep entry: its payable amount and the swept\n// term fall, and an entry cut to nothing is closed as paid by the reduction.\nfunc (rt *Route) reduceSweep(key string, amount uint256, incident string) error {\n\te := rt.sweep(key)\n\tif e == nil {\n\t\treturn errors.New(\"controls: no claim or sweep entry \" + key)\n\t}\n\tif e.Voided || e.Paid {\n\t\treturn errors.New(\"controls: sweep entry \" + key + \" is voided or paid\")\n\t}\n\tif incident == \"\" {\n\t\treturn errors.New(\"controls: a reduction names its incident\")\n\t}\n\tif err := rt.checkRemedy(e.Denom, incident); err != nil {\n\t\treturn err\n\t}\n\tif amount == 0 || amount \u003e e.Amount || amount \u003e rt.unrecoveredOf(e.Denom, incident) {\n\t\treturn ufmt.Errorf(\"controls: %s is not within the sweep entry's amount and incident %s's unrecovered losses\", u(amount), incident)\n\t}\n\trt.revokeConflicting(e.Denom, incident, amount)\n\td := rt.Denom(e.Denom)\n\te.Amount -= amount\n\te.Reduced += amount // at most the entry's amount\n\td.Totals.Swept = mustSub(d.Totals.Swept, amount)\n\trt.recover(e.Denom, incident, amount)\n\tit := rt.incident(e.Denom, incident)\n\td.Totals.Reduced, it.Reduced = statAdd(d.Totals.Reduced, amount), statAdd(it.Reduced, amount)\n\tif e.Amount == 0 {\n\t\te.Paid, e.PaidTo = true, \"reduced\"\n\t}\n\treturn nil\n}\n\n// Accept records an accepted deficit (ADR pr6375 §7.9): governance reopens\n// with amount of the losses unrecovered, stated in the trust statement. The\n// loss stays unrecovered (the supply identity still carries it); it only no\n// longer waits for a restoring decision.\nfunc (rt *Route) Accept(denom string, amount uint256, incident string) error {\n\td := rt.Denom(denom)\n\tif d == nil {\n\t\treturn errors.New(\"controls: denom \" + denom + \" is not enabled on route \" + rt.Key)\n\t}\n\tif incident == \"\" {\n\t\treturn errors.New(\"controls: an accepted deficit names its incident\")\n\t}\n\tif amount == 0 || amount \u003e rt.unresolvedOf(denom, incident) {\n\t\treturn ufmt.Errorf(\"controls: %s is not within incident %s's unresolved losses\", u(amount), incident)\n\t}\n\trt.accept(denom, incident, amount)\n\treturn nil\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"Vz6CD+/uHuRQDnfYqoiY/6PCQSgW0xtlczzt/+MaETM9E2JWLW3VQwIrUDNuuv0e/pxz/3oGWMN+a2Yz95Sf2Q=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"extproof","path":"gno.land/p/aib/ibc/lightclient/extproof/v0","files":[{"name":"extproof.gno","body":"// Package extproof lets light clients whose state proofs are not ICS23\n// proofs (an Ethereum client proves storage with Merkle-Patricia proofs) take\n// part in packet verification without changing the frozen message types.\n//\n// MsgRecvPacket, MsgAcknowledgement and MsgTimeout carry their proof in an\n// []ics23.CommitmentProof field, which such a client cannot use, and an Ext\n// field reserved for data a later core implementation needs. A relayer for\n// such a client leaves the ICS23 field empty and sets Ext to a Proof; the core\n// implementation then verifies through the client's Verifier methods.\npackage extproof\n\nimport \"gno.land/p/aib/ibc/types/v0\"\n\n// Proof is the value a relayer puts in a message's Ext field to carry a\n// client-specific proof. Data is whatever proof type the light client of the\n// verifying side expects; the client rejects any other type.\ntype Proof struct {\n\tData any\n}\n\n// Verifier is implemented by light clients that verify Proof.Data.\ntype Verifier interface {\n\t// VerifyMembershipProof verifies that value is stored at path at height.\n\tVerifyMembershipProof(height types.Height, proof any, path types.MerklePath, value []byte) error\n\t// VerifyNonMembershipProof verifies that nothing is stored at path at\n\t// height.\n\tVerifyNonMembershipProof(height types.Height, proof any, path types.MerklePath) error\n}\n\n// FromExt returns the Proof a message's Ext field carries, if any.\nfunc FromExt(ext any) (Proof, bool) {\n\tswitch p := ext.(type) {\n\tcase Proof:\n\t\treturn p, true\n\tcase *Proof:\n\t\tif p != nil {\n\t\t\treturn *p, true\n\t\t}\n\t}\n\treturn Proof{}, false\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/aib/ibc/lightclient/extproof/v0\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"I+RfdLIZuMn3J7SdZGAMl6oIztvnvaHHlOdHoSYPHYNNDXZ8KTXLpMmSMtAQbWhc7dCOlDfDs3aToMYG7GRYJA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"lightclient","path":"gno.land/p/aib/ibc/lightclient/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/aib/ibc/lightclient/v0\"\ngno = \"0.9\"\n"},{"name":"lightclient.gno","body":"package lightclient\n\nimport (\n\t\"gno.land/p/aib/ibc/types/v0\"\n\t\"gno.land/p/aib/ics23/v0\"\n)\n\n// Status represents the status of a client\ntype Status = string\n\nconst (\n\t// Solomachine is used to indicate that the light client is a solo machine.\n\tSolomachine string = \"06-solomachine\"\n\n\t// Tendermint is used to indicate that the client uses the Tendermint Consensus Algorithm.\n\tTendermint string = \"07-tendermint\"\n\n\t// Localhost is the client type for the localhost client.\n\tLocalhost string = \"09-localhost\"\n\n\t// LocalhostClientID is the sentinel client ID for the localhost client.\n\tLocalhostClientID string = Localhost\n\n\t// Active is a status type of a client. An active client is allowed to be used.\n\tActive Status = \"Active\"\n\n\t// Frozen is a status type of a client. A frozen client is not allowed to be used.\n\tFrozen Status = \"Frozen\"\n\n\t// Expired is a status type of a client. An expired client is not allowed to be used.\n\tExpired Status = \"Expired\"\n\n\t// Unknown indicates there was an error in determining the status of a client.\n\tUnknown Status = \"Unknown\"\n\n\t// Unauthorized indicates that the client type is not registered as an allowed client type.\n\tUnauthorized Status = \"Unauthorized\"\n)\n\n// Interface is an interface which core IBC uses to interact with light\n// client modules.  Light client modules must implement this interface to\n// integrate with core IBC.\ntype Interface interface {\n\t// Initialize is called upon client creation, it allows the client to perform\n\t// validation on the client state and initial consensus state.  The light\n\t// client module is responsible for setting any client-specific data in the\n\t// store. This includes the client state, initial consensus state and any\n\t// associated metadata.\n\tInitialize(ClientState, ConsensusState) error\n\n\t// VerifyClientMessage must verify a ClientMessage. A ClientMessage could be\n\t// a Header, Misbehaviour, or batch update.  It must handle each type of\n\t// ClientMessage appropriately. Calls to CheckForMisbehaviour, UpdateState,\n\t// and UpdateStateOnMisbehaviour will assume that the content of the\n\t// ClientMessage has been verified and can be trusted. An error should be\n\t// returned if the ClientMessage fails to verify.\n\tVerifyClientMessage(ClientMessage) error\n\n\t// Checks for evidence of a misbehaviour in Header or Misbehaviour type. It\n\t// assumes the ClientMessage has already been verified.\n\tCheckForMisbehaviour(ClientMessage) bool\n\n\t// UpdateStateOnMisbehaviour should perform appropriate state changes on a\n\t// client state given that misbehaviour has been detected and verified.\n\tUpdateStateOnMisbehaviour(ClientMessage)\n\n\t// UpdateState updates and stores as necessary any associated information for\n\t// an IBC client, such as the ClientState and corresponding ConsensusState.\n\t// Upon successful update, a list of consensus heights is returned. It\n\t// assumes the ClientMessage has already been verified.\n\tUpdateState(ClientMessage) []types.Height\n\n\t// VerifyMembership is a generic proof verification method which verifies a\n\t// proof of the existence of a value at a given CommitmentPath at the\n\t// specified height.  The caller is expected to construct the full\n\t// CommitmentPath from a CommitmentPrefix and a standardized path (as defined\n\t// in ICS 24).\n\tVerifyMembership(height types.Height, proofs []ics23.CommitmentProof,\n\t\tpath types.MerklePath, value []byte) error\n\n\t// VerifyNonMembership is a generic proof verification method which verifies\n\t// the absence of a given CommitmentPath at a specified height.  The caller\n\t// is expected to construct the full CommitmentPath from a CommitmentPrefix\n\t// and a standardized path (as defined in ICS 24).\n\tVerifyNonMembership(height types.Height,\n\t\tproofs []ics23.CommitmentProof, path types.MerklePath) error\n\n\t// Status must return the status of the client. Only Active clients are\n\t// allowed to process packets.\n\tStatus() Status\n\n\t// LatestHeight returns the latest height of the client. If no client is\n\t// present for the provided client identifier a zero value height may be\n\t// returned.\n\tLatestHeight() types.Height\n\n\t// TimestampAtHeight must return the timestamp in seconds for the consensus\n\t// state associated with the provided height.\n\tTimestampAtHeight(types.Height) (uint64, error)\n\n\t// RecoverClient must verify that the provided substitute may be used to\n\t// recover the subject (receiver) client, and copy the substitute's updated\n\t// client and consensus state into the subject. The caller is responsible\n\t// for verifying Status() on both sides and authorization beforehand.\n\tRecoverClient(substitute Interface) error\n\n\t// Upgrade functions NOTE: proof heights are not included as upgrade to a new\n\t// revision is expected to pass only on the last height committed by the\n\t// current revision. Clients are responsible for ensuring that the planned\n\t// last height of the current revision is somehow encoded in the proof\n\t// verification process.  This is to ensure that no premature upgrades occur,\n\t// since upgrade plans committed to by the counterparty may be cancelled or\n\t// modified before the last planned height.  If the upgrade is verified, the\n\t// upgraded client and consensus states must be set in the client store.\n\tVerifyUpgradeAndUpdateState(newClient, newConsState, upgradeClientProof,\n\t\tupgradeConsensusStateProof any) error\n}\n\n// ClientState defines the required common functions for light clients.\ntype ClientState interface {\n\tClientType() string\n\tValidateBasic() error\n}\n\n// ConsensusState is the state of the consensus process\ntype ConsensusState interface {\n\tClientType() string\n\tValidateBasic() error\n}\n\n// ClientMessage is an interface used to update an IBC client.\n// The update may be done by a single header, a batch of headers, misbehaviour,\n// or any type which when verified produces a change to state of the IBC client\ntype ClientMessage interface {\n\tClientType() string\n\tValidateBasic() error\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"ef3i9NSBRkGDv5Z9Ob3d75d4TjUBr3x6YuvmQ05uCs08Rg65GPwrVCeOCG3lOBiDztKhhIeJ0/mdVCeto00amw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"ethereum","path":"gno.land/p/aib/ibc/lightclient/ethereum/v0","files":[{"name":"ethereum.gno","body":"// Package ethereum is an IBC light client of Ethereum for gno.land. It\n// follows the Altair light-client sync protocol: it trusts the sync\n// committee, verifies its BLS aggregate signature over each update with the\n// crypto/bls12381 native, tracks finalized execution state roots, and proves\n// ICS26 router commitments with Merkle-Patricia storage proofs.\n//\n// It ports the Ethereum light client of cosmos/solidity-ibc-eureka\n// (packages/ethereum/light-client), and supports Electra and later forks.\n//\n// Its storage proofs are not ICS23 proofs, so it implements\n// extproof.Verifier and relayers pass a MembershipProof in the Ext field of\n// packet messages.\npackage ethereum\n\nimport (\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"crypto/bls12381\"\n\n\t\"gno.land/p/aib/ibc/lightclient/extproof/v0\"\n\t\"gno.land/p/aib/ibc/lightclient/v0\"\n\t\"gno.land/p/aib/ibc/types/v0\"\n\t\"gno.land/p/aib/ics23/v0\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// EthLightClient is the light client. Consensus states are keyed by the\n// height {0, finalized slot}.\ntype EthLightClient struct {\n\tClientState            *ClientState\n\tConsensusStateByHeight *bptree.BPTree // height:*ConsensusState\n\t// TrustedFromSlot is the slot of the consensus state a recovery copied:\n\t// the states below it were trusted by the verifier the recovery replaced,\n\t// so no proof, timestamp or update is ever read from them again.\n\tTrustedFromSlot uint64\n\t// history is the verifiers replacements retired before this one\n\t// (NewFrom), read only, keyed by the latest slot each had when it was\n\t// retired (zero-padded): a persistent tree, shared with the verifiers it\n\t// came from and never written through, so a height none of the newer\n\t// stores holds is found by one ordered lookup, in O(log replacements),\n\t// however many replacements there were (no chain of delegations).\n\thistory *avl.Node\n\t// overrides is the consensus states a retired verifier rewrote at slots\n\t// an earlier store's interval already covers (a next-committee-only\n\t// update of the head it inherited), keyed by historyKey(slot), consulted\n\t// before history: a persistent tree, copy-on-write like history, holding\n\t// at most the few heads each replacement carried.\n\toverrides *avl.Node\n}\n\nvar (\n\t_ lightclient.Interface = (*EthLightClient)(nil)\n\t_ extproof.Verifier     = (*EthLightClient)(nil)\n)\n\nfunc NewEthLightClient() *EthLightClient {\n\treturn \u0026EthLightClient{ConsensusStateByHeight: bptree.NewBPTree32()}\n}\n\n// NewLightClient is NewEthLightClient typed as the generic interface, for the\n// IBC core's client constructor table.\nfunc NewLightClient() lightclient.Interface {\n\treturn NewEthLightClient()\n}\n\nfunc heightAtSlot(slot uint64) types.Height {\n\treturn types.NewHeight(0, slot)\n}\n\n// Version names this verifier package and version, which a governed verifier\n// replacement converts from (core.RegisterVerifier).\nconst Version = \"ethereum/v0\"\n\n// VerifierVersion is Version: the core reads it to tell which verifier a\n// client holds.\nfunc (c *EthLightClient) VerifierVersion() string { return Version }\n\n// Ethereum is the v0 verifier itself: a later verifier version that wraps it\n// keeps exposing its state, so renders and recoveries read it whatever wraps\n// it.\nfunc (c *EthLightClient) Ethereum() *EthLightClient { return c }\n\n// RouteIdentity is the history this verifier authenticates (ADR pr6375\n// §7.12, \"Route identity is preserved\"): the Ethereum chain (its chain id and\n// genesis validators root), the ICS26 router and its commitments slot. router\n// is the router's address in lower case with \"0x\". Every field is fixed at\n// creation: no update, recovery or verifier replacement changes it.\nfunc (c *EthLightClient) RouteIdentity() (identity, router string) {\n\tcs := c.ClientState\n\trouter = \"0x\" + hex.EncodeToString(cs.IBCContractAddress)\n\tidentity = \"chain \" + strconv.FormatUint(cs.ChainID, 10) +\n\t\t\", genesis validators root 0x\" + hex.EncodeToString(cs.GenesisValidatorsRoot) +\n\t\t\", router \" + router +\n\t\t\", commitment slot 0x\" + hex.EncodeToString(cs.IBCCommitmentSlot)\n\treturn identity, router\n}\n\n// ethState is a verifier exposing ethereum/v0 state: this one, or a later\n// version wrapping it.\ntype ethState interface {\n\tEthereum() *EthLightClient\n}\n\n// Retire is a read-only view of c, the verifier a replacement is about to\n// retire: what a conversion is handed, so it reads c and can never write to\n// it. The view serves the consensus states c trusts at or below its latest\n// slot.\nfunc Retire(c *EthLightClient) *Retired {\n\treturn \u0026Retired{c: c, upTo: c.ClientState.LatestSlot}\n}\n\n// NewFrom is a verifier that continues the retired one r views, for a\n// verifier replacement (ADR pr6375 R16), copying no consensus-state history\n// (O(log previous stores), the persistent trees' insertions copying their\n// search paths): a deep copy of its client state, and r, from which it serves every\n// consensus state at or below the retired verifier's latest slot, so every\n// height it trusted stays provable. Its own consensus states, those it stores\n// from now on, are kept apart (an overlay), and nothing writes to the retired\n// verifier: r is the only reference, and it only reads.\nfunc NewFrom(r *Retired) *EthLightClient {\n\tcs := r.ClientState()\n\t// Each store in the history owns the interval of slots above the\n\t// previous one's key and up to its own: non-overlapping. The retired\n\t// verifier's own store joins it only when it holds a slot above every\n\t// key (its finality advanced since it was created). Any state it holds\n\t// at or below the highest key (at most the head it inherited, rewritten\n\t// by a next-committee-only update) is an override of an earlier store's\n\t// interval: it is copied into the overrides, consulted first, so it is\n\t// never lost to that interval whether or not finality advanced after it.\n\t// Both trees are persistent: the retired verifier's own are not changed.\n\thist, ovr := r.c.history, r.c.overrides\n\tif r.c.ConsensusStateByHeight.Size() \u003e 0 {\n\t\tvar top uint64\n\t\thasTop := false\n\t\thist.ReverseIterate(\"\", \"\", func(n *avl.Node) bool {\n\t\t\ttop, _ = strconv.ParseUint(n.Key(), 10, 64)\n\t\t\thasTop = true\n\t\t\treturn true\n\t\t})\n\t\tif hasTop {\n\t\t\t// Bounded: a verifier's own store holds no slot below the head\n\t\t\t// it was created with, so this visits at most that head.\n\t\t\tr.c.ConsensusStateByHeight.Iterate(\"\", heightAtSlot(top+1).StringNatSort(), func(_ string, v any) bool {\n\t\t\t\tcopied := copyConsensusState(*v.(*ConsensusState))\n\t\t\t\tovr, _ = ovr.Set(historyKey(copied.Slot), \u0026copied)\n\t\t\t\treturn false\n\t\t\t})\n\t\t}\n\t\tif !hasTop || r.upTo \u003e top {\n\t\t\thist, _ = hist.Set(historyKey(r.upTo), r.c)\n\t\t}\n\t}\n\treturn \u0026EthLightClient{\n\t\tClientState:            \u0026cs,\n\t\tConsensusStateByHeight: bptree.NewBPTree32(),\n\t\tTrustedFromSlot:        r.c.TrustedFromSlot,\n\t\thistory:                hist,\n\t\toverrides:              ovr,\n\t}\n}\n\n// historyKey orders the retired verifiers by the latest slot they had.\nfunc historyKey(slot uint64) string {\n\ts := strconv.FormatUint(slot, 10)\n\treturn strings.Repeat(\"0\", 20-len(s)) + s\n}\n\n// fromHistory is the consensus state at height a retired verifier stores\n// itself: the first one retired at a latest slot at or above it holds it,\n// if any does (their own stores cover disjoint ranges of slots).\nfunc (c *EthLightClient) fromHistory(height types.Height) (*ConsensusState, bool) {\n\tif _, v, ok := c.overrides.Get(historyKey(height.RevisionHeight)); ok {\n\t\treturn v.(*ConsensusState), true\n\t}\n\tvar owner *EthLightClient\n\tc.history.Iterate(historyKey(height.RevisionHeight), \"\", func(n *avl.Node) bool {\n\t\towner = n.Value().(*EthLightClient)\n\t\treturn true\n\t})\n\tif owner == nil {\n\t\treturn nil, false\n\t}\n\tv := owner.ConsensusStateByHeight.Get(height.StringNatSort())\n\tif v == nil {\n\t\treturn nil, false\n\t}\n\treturn v.(*ConsensusState), true\n}\n\n// HistoryDepth is how many retired verifiers' stores this one reads through\n// its history: each lookup is one ordered search of them.\nfunc (c *EthLightClient) HistoryDepth() int { return c.history.Size() }\n\n// Retired is a read-only view of a verifier a replacement retires: the\n// consensus states it trusted, at or below upTo, and nothing to write with.\ntype Retired struct {\n\tc    *EthLightClient\n\tupTo uint64\n}\n\n// ClientState is a deep copy of the retired verifier's client state.\nfunc (r *Retired) ClientState() ClientState {\n\tcs := *r.c.ClientState\n\tcs.GenesisValidatorsRoot = cloneBytes(cs.GenesisValidatorsRoot)\n\tcs.IBCContractAddress = cloneBytes(cs.IBCContractAddress)\n\tcs.IBCCommitmentSlot = cloneBytes(cs.IBCCommitmentSlot)\n\treturn cs\n}\n\n// LatestHeight is the retired verifier's latest height.\nfunc (r *Retired) LatestHeight() types.Height { return r.c.LatestHeight() }\n\n// ConsensusState is a copy of the retired verifier's consensus state at\n// height, if it trusted one there.\nfunc (r *Retired) ConsensusState(height types.Height) (ConsensusState, bool) {\n\tcons, ok := r.consensusState(height)\n\tif !ok {\n\t\treturn ConsensusState{}, false\n\t}\n\treturn copyConsensusState(*cons), true\n}\n\n// consensusState is the retired verifier's consensus state at height, if it\n// trusted one there.\nfunc (r *Retired) consensusState(height types.Height) (*ConsensusState, bool) {\n\tif r == nil || height.RevisionHeight \u003e r.upTo {\n\t\treturn nil, false\n\t}\n\treturn r.c.GetConsensusState(height)\n}\n\n// Implements lightclient.Interface. It runs once, on a client not yet\n// initialized: a stored client's trusted state is never replaced through it.\nfunc (c *EthLightClient) Initialize(clientState lightclient.ClientState, consensusState lightclient.ConsensusState) error {\n\tif c.ClientState != nil {\n\t\treturn errors.New(\"ethereum client initialized already\")\n\t}\n\tcs, ok := clientState.(ClientState)\n\tif !ok {\n\t\treturn ufmt.Errorf(\"expected ethereum ClientState, got %T\", clientState)\n\t}\n\tin, ok := consensusState.(ConsensusState)\n\tif !ok {\n\t\treturn ufmt.Errorf(\"expected ethereum ConsensusState, got %T\", consensusState)\n\t}\n\tif in.Slot != cs.LatestSlot {\n\t\treturn ufmt.Errorf(\"consensus state slot %d differs from client latest slot %d\", in.Slot, cs.LatestSlot)\n\t}\n\t// Copy into values allocated here, see the tendermint client's Initialize.\n\tcs.GenesisValidatorsRoot = cloneBytes(cs.GenesisValidatorsRoot)\n\tcs.IBCContractAddress = cloneBytes(cs.IBCContractAddress)\n\tcs.IBCCommitmentSlot = cloneBytes(cs.IBCCommitmentSlot)\n\tc.ClientState = \u0026cs\n\tcons := copyConsensusState(in)\n\tc.setConsensusState(\u0026cons)\n\treturn nil\n}\n\n// cloneBytes copies b into an array allocated by the running realm. Every\n// byte slice the client keeps goes through it: a slice taken from the caller\n// stays owned by the caller's realm, which could change it after a verified\n// update and so rewrite a trusted state root or committee.\nfunc cloneBytes(b []byte) []byte {\n\tif b == nil {\n\t\treturn nil\n\t}\n\treturn append([]byte{}, b...)\n}\n\nfunc cloneSummary(in SummarizedSyncCommittee) SummarizedSyncCommittee {\n\treturn SummarizedSyncCommittee{\n\t\tPubkeysHash:     cloneBytes(in.PubkeysHash),\n\t\tAggregatePubkey: cloneBytes(in.AggregatePubkey),\n\t}\n}\n\n// copyConsensusState returns a deep copy of in: no byte slice is shared.\nfunc copyConsensusState(in ConsensusState) ConsensusState {\n\tout := ConsensusState{\n\t\tSlot:                 in.Slot,\n\t\tStateRoot:            cloneBytes(in.StateRoot),\n\t\tTimestamp:            in.Timestamp,\n\t\tCurrentSyncCommittee: cloneSummary(in.CurrentSyncCommittee),\n\t}\n\tif in.NextSyncCommittee != nil {\n\t\tnext := cloneSummary(*in.NextSyncCommittee)\n\t\tout.NextSyncCommittee = \u0026next\n\t}\n\treturn out\n}\n\n// Implements lightclient.Interface.\nfunc (c *EthLightClient) VerifyClientMessage(clientMsg lightclient.ClientMessage) error {\n\tswitch msg := clientMsg.(type) {\n\tcase *Header:\n\t\treturn c.verifyHeader(msg)\n\tcase *Misbehaviour:\n\t\treturn c.verifyMisbehaviour(msg)\n\t}\n\treturn errors.New(\"unknown client message type\")\n}\n\nfunc now() uint64 { return uint64(time.Now().Unix()) }\n\nfunc (c *EthLightClient) currentSlot() (uint64, error) {\n\tslot, ok := c.ClientState.slotAtTimestamp(now())\n\tif !ok {\n\t\treturn 0, errors.New(\"current time precedes the tracked chain's genesis\")\n\t}\n\treturn slot, nil\n}\n\n// trustedState returns the consensus state at slot and the summary of its\n// current sync committee, or of its next one if isNext.\nfunc (c *EthLightClient) trustedState(slot uint64, isNext bool) (*ConsensusState, SummarizedSyncCommittee, error) {\n\tcons, ok := c.GetConsensusState(heightAtSlot(slot))\n\tif !ok {\n\t\treturn nil, SummarizedSyncCommittee{}, ufmt.Errorf(\"no consensus state at trusted slot %d\", slot)\n\t}\n\tif !isNext {\n\t\treturn cons, cons.CurrentSyncCommittee, nil\n\t}\n\tif cons.NextSyncCommittee == nil {\n\t\treturn nil, SummarizedSyncCommittee{}, errors.New(\"next sync committee is not known\")\n\t}\n\treturn cons, *cons.NextSyncCommittee, nil\n}\n\n// verifyHeader implements verify_header.\nfunc (c *EthLightClient) verifyHeader(h *Header) error {\n\tcs := c.ClientState\n\t// Also checked by core before it calls VerifyClientMessage; repeated here so\n\t// no caller of the client can skip the size checks.\n\tif err := h.ValidateBasic(); err != nil {\n\t\treturn err\n\t}\n\tif err := c.checkLatestAnchor(h.TrustedSlot); err != nil {\n\t\treturn err\n\t}\n\ttrusted, active, err := c.trustedState(h.TrustedSlot, h.ActiveIsNext)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcurrent, err := c.currentSlot()\n\tif err != nil {\n\t\treturn err\n\t}\n\tu := h.ConsensusUpdate\n\tif err := c.validateUpdate(trusted, active, h.ActiveIsNext, u, current); err != nil {\n\t\treturn err\n\t}\n\t// Only a 2/3 supermajority may move the client.\n\tif !hasSupermajority(u) {\n\t\treturn errors.New(\"not enough sync committee signatures\")\n\t}\n\tif u.FinalizedHeader.Beacon.Slot \u003c= trusted.Slot {\n\t\t// As in the sync protocol's apply_light_client_update, an update that\n\t\t// does not advance finality is still useful when it reveals the next\n\t\t// sync committee: without it a client bootstrapped late in a period\n\t\t// could never cross into the next one. validateUpdate checked it is\n\t\t// such an update.\n\t\tif !isNextCommitteeOnly(cs, trusted, u) {\n\t\t\treturn ufmt.Errorf(\"finalized slot %d is not after trusted slot %d\", u.FinalizedHeader.Beacon.Slot, trusted.Slot)\n\t\t}\n\t\treturn nil\n\t}\n\t// An update may cross into the next period without the committee after\n\t// it, as a finality update does at every rotation: as in the sync\n\t// protocol's apply_light_client_update, the new state then does not know\n\t// its next committee until a later update reveals it.\n\t_, err = c.finalizedExecution(u)\n\treturn err\n}\n\n// validateUpdate implements validate_light_client_update, active being the\n// summary of the signing committee.\nfunc (c *EthLightClient) validateUpdate(trusted *ConsensusState, active SummarizedSyncCommittee, committeeIsNext bool, u LightClientUpdate, currentSlot uint64) error {\n\tcs := c.ClientState\n\tif u.SyncAggregate.participants() \u003c cs.MinSyncCommitteeParticipants {\n\t\treturn ufmt.Errorf(\"insufficient sync committee participants: %d\", u.SyncAggregate.participants())\n\t}\n\tif err := c.validateLightClientHeader(u.AttestedHeader); err != nil {\n\t\treturn ufmt.Errorf(\"attested header: %v\", err)\n\t}\n\tattestedSlot := u.AttestedHeader.Beacon.Slot\n\tfinalizedSlot := u.FinalizedHeader.Beacon.Slot\n\tif finalizedSlot == cs.GenesisSlot {\n\t\treturn errors.New(\"finalized slot is the genesis slot\")\n\t}\n\tif currentSlot \u003c u.SignatureSlot {\n\t\treturn ufmt.Errorf(\"signature slot %d is after the current slot %d\", u.SignatureSlot, currentSlot)\n\t}\n\tif !(u.SignatureSlot \u003e attestedSlot \u0026\u0026 attestedSlot \u003e= finalizedSlot) {\n\t\treturn errors.New(\"slots must satisfy signature \u003e attested \u003e= finalized\")\n\t}\n\n\tstoredPeriod := cs.periodAtSlot(trusted.Slot)\n\tsignaturePeriod := cs.periodAtSlot(u.SignatureSlot)\n\t// The full next committee is only available when the relayer passed it\n\t// as the active one; see TrustedConsensusState in the Rust client.\n\tnextKnown := committeeIsNext\n\tif nextKnown {\n\t\tif signaturePeriod != storedPeriod \u0026\u0026 signaturePeriod != storedPeriod+1 {\n\t\t\treturn ufmt.Errorf(\"signature period %d is not the stored period %d or the next\", signaturePeriod, storedPeriod)\n\t\t}\n\t} else if signaturePeriod != storedPeriod {\n\t\treturn ufmt.Errorf(\"signature period %d is not the stored period %d\", signaturePeriod, storedPeriod)\n\t}\n\n\tattestedPeriod := cs.periodAtSlot(attestedSlot)\n\tisNextCommitteeUpdate := u.NextSyncCommittee != nil\n\thasNextCommittee := !nextKnown \u0026\u0026 isNextCommitteeUpdate \u0026\u0026 attestedPeriod == storedPeriod\n\tif !(attestedSlot \u003e trusted.Slot || hasNextCommittee) {\n\t\treturn errors.New(\"irrelevant update\")\n\t}\n\n\tif err := c.validateLightClientHeader(u.FinalizedHeader); err != nil {\n\t\treturn ufmt.Errorf(\"finalized header: %v\", err)\n\t}\n\tif err := cs.verifySupportedFork(attestedSlot); err != nil {\n\t\treturn err\n\t}\n\tif !isValidMerkleBranch(u.FinalizedHeader.Beacon.HashTreeRoot(), u.FinalityBranch,\n\t\tcs.finalizedRootGindex(attestedSlot), u.AttestedHeader.Beacon.StateRoot) {\n\t\treturn errors.New(\"invalid finality branch\")\n\t}\n\n\tif isNextCommitteeUpdate {\n\t\tif attestedPeriod == storedPeriod \u0026\u0026 nextKnown \u0026\u0026 !u.NextSyncCommittee.equal(*trusted.NextSyncCommittee) {\n\t\t\treturn errors.New(\"next sync committee does not match the trusted one\")\n\t\t}\n\t\tif !isValidMerkleBranch(u.NextSyncCommittee.committeeRoot(), u.NextSyncCommitteeBranch,\n\t\t\tcs.nextSyncCommitteeGindex(attestedSlot), u.AttestedHeader.Beacon.StateRoot) {\n\t\t\treturn errors.New(\"invalid next sync committee branch\")\n\t\t}\n\t}\n\n\t// The signing committee: the current one within the stored period, the\n\t// next one across it.\n\tif signaturePeriod == storedPeriod \u0026\u0026 committeeIsNext {\n\t\treturn errors.New(\"update signed in the stored period needs the current sync committee\")\n\t}\n\tif signaturePeriod != storedPeriod \u0026\u0026 !committeeIsNext {\n\t\treturn errors.New(\"update signed in the next period needs the next sync committee\")\n\t}\n\texcluded, err := c.nonSignerKeys(active, u)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tforkVersionSlot := u.SignatureSlot\n\tif forkVersionSlot \u003e 0 {\n\t\tforkVersionSlot--\n\t}\n\tforkVersion := cs.ForkParameters.forkVersion(cs.epochAtSlot(forkVersionSlot))\n\tdomain := computeDomain(domainSyncCommittee, forkVersion, cs.GenesisValidatorsRoot)\n\tsigningRoot := computeSigningRoot(u.AttestedHeader.Beacon.HashTreeRoot(), domain)\n\t// The committee commits to its aggregate key next to its keys, so the\n\t// signers' aggregate is it minus the non-signers': a few keys to decode\n\t// instead of hundreds.\n\tif !bls12381.FastAggregateVerifyExcluding(active.AggregatePubkey, excluded,\n\t\tsigningRoot, u.SyncAggregate.SyncCommitteeSignature) {\n\t\treturn errors.New(\"invalid sync committee signature\")\n\t}\n\treturn nil\n}\n\n// nonSignerKeys checks that u.NonSigners are exactly the members of the\n// committee summarized by active whose participation bit is unset, each proven\n// at its index against the committee's pubkeys root, and returns their keys\n// concatenated.\nfunc (c *EthLightClient) nonSignerKeys(active SummarizedSyncCommittee, u LightClientUpdate) ([]byte, error) {\n\tsize := c.ClientState.SyncCommitteeSize\n\tbits := u.SyncAggregate.SyncCommitteeBits\n\tif uint64(len(bits))*8 != size {\n\t\treturn nil, ufmt.Errorf(\"sync committee bits cover %d members, committee has %d\", len(bits)*8, size)\n\t}\n\tvar excluded []byte\n\tnext := 0\n\tfor i := uint64(0); i \u003c size; i++ {\n\t\tif bits[i/8]\u0026(1\u003c\u003c(i%8)) != 0 {\n\t\t\tcontinue\n\t\t}\n\t\tif next == len(u.NonSigners) {\n\t\t\treturn nil, ufmt.Errorf(\"non-signer %d is missing\", i)\n\t\t}\n\t\tns := u.NonSigners[next]\n\t\tnext++\n\t\tif !isValidMerkleBranch(pubkeyLeaf(ns.Pubkey), ns.Branch, size+i, active.PubkeysHash) {\n\t\t\treturn nil, ufmt.Errorf(\"invalid branch for non-signer %d\", i)\n\t\t}\n\t\texcluded = append(excluded, ns.Pubkey...)\n\t}\n\tif next != len(u.NonSigners) {\n\t\treturn nil, errors.New(\"more non-signers than unset participation bits\")\n\t}\n\treturn excluded, nil\n}\n\n// validateLightClientHeader implements is_valid_light_client_header for\n// Electra and later: the execution payload header, or from Gloas on the\n// execution block hash, is in the beacon block body.\nfunc (c *EthLightClient) validateLightClientHeader(h LightClientHeader) error {\n\tif err := c.ClientState.verifySupportedFork(h.Beacon.Slot); err != nil {\n\t\treturn err\n\t}\n\t// SSZ hashes fixed-size fields as chunks laid side by side: a field of the\n\t// wrong length could shift bytes into its neighbour and keep the root, so\n\t// every fixed size is checked before hashing.\n\tif err := h.Beacon.validateSizes(); err != nil {\n\t\treturn err\n\t}\n\tif err := validateBranch(h.ExecutionBranch); err != nil {\n\t\treturn ufmt.Errorf(\"execution branch: %v\", err)\n\t}\n\tleaf, gindex := []byte(nil), uint64(executionPayloadGindex)\n\tif c.ClientState.isGloas(h.Beacon.Slot) {\n\t\tif len(h.ExecutionBlockHash) != hashSize {\n\t\t\treturn errors.New(\"execution block hash must be 32 bytes\")\n\t\t}\n\t\tleaf, gindex = h.ExecutionBlockHash, executionBlockHashGindexGloas\n\t} else {\n\t\tif err := h.Execution.validateSizes(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tleaf = h.Execution.HashTreeRoot()\n\t}\n\tif !isValidMerkleBranch(leaf, h.ExecutionBranch, gindex, h.Beacon.BodyRoot) {\n\t\treturn errors.New(\"invalid execution branch\")\n\t}\n\treturn nil\n}\n\n// executionData is what the client keeps of an execution block.\ntype executionData struct {\n\tstateRoot   []byte\n\ttimestamp   uint64\n\tblockNumber uint64\n}\n\n// finalizedExecution returns the execution data of the update's finalized\n// header: its execution payload header before Gloas, the execution block\n// header whose hash the beacon block commits to from Gloas on.\nfunc (c *EthLightClient) finalizedExecution(u LightClientUpdate) (executionData, error) {\n\th := u.FinalizedHeader\n\tif !c.ClientState.isGloas(h.Beacon.Slot) {\n\t\treturn executionData{h.Execution.StateRoot, h.Execution.Timestamp, h.Execution.BlockNumber}, nil\n\t}\n\tif !bytesEqual(keccak(u.FinalizedExecutionHeader), h.ExecutionBlockHash) {\n\t\treturn executionData{}, errors.New(\"finalized execution header does not match the execution block hash\")\n\t}\n\treturn decodeExecutionHeader(u.FinalizedExecutionHeader)\n}\n\n// verifyMisbehaviour accepts two valid updates that finalize different beacon\n// headers at the same slot: conflicting finality, which an honest chain never\n// produces. Different attested heads alone are not misbehaviour, since sync\n// committees sign their fork-choice head and an ordinary reorg yields two.\n// Like an update, the evidence must build on the latest trusted state of a\n// client that has not expired, so a past committee cannot freeze it.\nfunc (c *EthLightClient) verifyMisbehaviour(m *Misbehaviour) error {\n\tif err := m.ValidateBasic(); err != nil {\n\t\treturn err\n\t}\n\tif err := c.checkLatestAnchor(m.TrustedSlot); err != nil {\n\t\treturn err\n\t}\n\ttrusted, active, err := c.trustedState(m.TrustedSlot, m.ActiveIsNext)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif m.Update1.FinalizedHeader.Beacon.Slot != m.Update2.FinalizedHeader.Beacon.Slot {\n\t\treturn errors.New(\"misbehaviour updates finalize different slots\")\n\t}\n\tif bytesEqual(m.Update1.FinalizedHeader.Beacon.HashTreeRoot(), m.Update2.FinalizedHeader.Beacon.HashTreeRoot()) {\n\t\treturn errors.New(\"misbehaviour updates finalize the same beacon header\")\n\t}\n\t// Evidence needs the same 2/3 supermajority as an update moving the client,\n\t// not only the configured minimum participation.\n\tif !hasSupermajority(m.Update1) || !hasSupermajority(m.Update2) {\n\t\treturn errors.New(\"not enough sync committee signatures\")\n\t}\n\tcurrent, err := c.currentSlot()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := c.validateUpdate(trusted, active, m.ActiveIsNext, m.Update1, current); err != nil {\n\t\treturn ufmt.Errorf(\"update 1: %v\", err)\n\t}\n\tif err := c.validateUpdate(trusted, active, m.ActiveIsNext, m.Update2, current); err != nil {\n\t\treturn ufmt.Errorf(\"update 2: %v\", err)\n\t}\n\treturn nil\n}\n\n// Implements lightclient.Interface. A header that would store a different\n// state root at a slot the client already holds is misbehaviour too.\nfunc (c *EthLightClient) CheckForMisbehaviour(clientMsg lightclient.ClientMessage) bool {\n\tswitch msg := clientMsg.(type) {\n\tcase *Misbehaviour:\n\t\treturn true\n\tcase *Header:\n\t\tu := msg.ConsensusUpdate\n\t\texisting, ok := c.GetConsensusState(heightAtSlot(u.FinalizedHeader.Beacon.Slot))\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\t\texec, err := c.finalizedExecution(u)\n\t\treturn err == nil \u0026\u0026 !bytesEqual(existing.StateRoot, exec.stateRoot)\n\t}\n\treturn false\n}\n\n// Implements lightclient.Interface.\nfunc (c *EthLightClient) UpdateStateOnMisbehaviour(lightclient.ClientMessage) {\n\tcs := *c.ClientState\n\tcs.IsFrozen = true\n\tc.ClientState = \u0026cs\n}\n\n// isNextCommitteeOnly reports whether u only reveals the next sync committee\n// of the trusted state's period, which the trusted state does not know yet.\nfunc isNextCommitteeOnly(cs *ClientState, trusted *ConsensusState, u LightClientUpdate) bool {\n\treturn trusted.NextSyncCommittee == nil \u0026\u0026 u.NextSyncCommittee != nil \u0026\u0026\n\t\tcs.periodAtSlot(u.AttestedHeader.Beacon.Slot) == cs.periodAtSlot(trusted.Slot)\n}\n\n// Implements lightclient.Interface: update_consensus_state.\nfunc (c *EthLightClient) UpdateState(clientMsg lightclient.ClientMessage) []types.Height {\n\th, ok := clientMsg.(*Header)\n\tif !ok {\n\t\tpanic(ufmt.Sprintf(\"expected *ethereum.Header, got %T\", clientMsg))\n\t}\n\tcs := c.ClientState\n\ttrusted, ok := c.GetConsensusState(heightAtSlot(h.TrustedSlot))\n\tif !ok {\n\t\tpanic(\"trusted consensus state disappeared\")\n\t}\n\tu := h.ConsensusUpdate\n\tif u.FinalizedHeader.Beacon.Slot \u003c= trusted.Slot {\n\t\t// A next-committee-only update (see verifyHeader): record the\n\t\t// committee in the trusted state and change nothing else.\n\t\tnext := copyConsensusState(*trusted)\n\t\ts := cloneSummary(*u.NextSyncCommittee)\n\t\tnext.NextSyncCommittee = \u0026s\n\t\tc.setConsensusState(\u0026next)\n\t\treturn []types.Height{heightAtSlot(trusted.Slot)}\n\t}\n\tstorePeriod := cs.periodAtSlot(trusted.Slot)\n\tfinalizedSlot := u.FinalizedHeader.Beacon.Slot\n\tfinalizedPeriod := cs.periodAtSlot(finalizedSlot)\n\n\tnext := copyConsensusState(*trusted)\n\tvar updateNext *SummarizedSyncCommittee\n\tif u.NextSyncCommittee != nil {\n\t\ts := cloneSummary(*u.NextSyncCommittee)\n\t\tupdateNext = \u0026s\n\t}\n\tif trusted.NextSyncCommittee != nil {\n\t\tif finalizedPeriod == storePeriod+1 {\n\t\t\tnext.CurrentSyncCommittee = cloneSummary(*trusted.NextSyncCommittee)\n\t\t\tnext.NextSyncCommittee = updateNext\n\t\t}\n\t} else {\n\t\tif finalizedPeriod != storePeriod {\n\t\t\tpanic(\"store period must equal the finalized period\")\n\t\t}\n\t\tnext.NextSyncCommittee = updateNext\n\t}\n\texec, err := c.finalizedExecution(u)\n\tif err != nil {\n\t\tpanic(err) // checked by VerifyClientMessage\n\t}\n\tnext.Slot = finalizedSlot\n\tnext.StateRoot = cloneBytes(exec.stateRoot)\n\tnext.Timestamp = exec.timestamp\n\tc.setConsensusState(\u0026next)\n\n\tif finalizedSlot \u003e cs.LatestSlot {\n\t\tupdated := *cs\n\t\tupdated.LatestSlot = finalizedSlot\n\t\tupdated.LatestExecutionBlockNumber = exec.blockNumber\n\t\tc.ClientState = \u0026updated\n\t}\n\treturn []types.Height{heightAtSlot(finalizedSlot)}\n}\n\n// VerifyMembership implements lightclient.Interface. This client's proofs are\n// not ICS23 proofs: relayers must pass a MembershipProof in the message's Ext\n// field, which reaches VerifyMembershipProof.\nfunc (c *EthLightClient) VerifyMembership(types.Height, []ics23.CommitmentProof, types.MerklePath, []byte) error {\n\treturn errors.New(\"ethereum light client proofs must be passed as an extproof.Proof\")\n}\n\n// VerifyNonMembership implements lightclient.Interface; see VerifyMembership.\nfunc (c *EthLightClient) VerifyNonMembership(types.Height, []ics23.CommitmentProof, types.MerklePath) error {\n\treturn errors.New(\"ethereum light client proofs must be passed as an extproof.Proof\")\n}\n\n// VerifyMembershipProof implements extproof.Verifier: value must be the\n// 32-byte value of the ICS26 commitment at path.\nfunc (c *EthLightClient) VerifyMembershipProof(height types.Height, proof any, path types.MerklePath, value []byte) error {\n\tif len(value) != hashSize {\n\t\treturn ufmt.Errorf(\"commitment value must be 32 bytes, got %d\", len(value))\n\t}\n\tstored, err := c.provenCommitment(height, proof, path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !bytesEqual(stored, value) {\n\t\treturn errors.New(\"stored commitment differs from the expected value\")\n\t}\n\treturn nil\n}\n\n// VerifyNonMembershipProof implements extproof.Verifier.\nfunc (c *EthLightClient) VerifyNonMembershipProof(height types.Height, proof any, path types.MerklePath) error {\n\tstored, err := c.provenCommitment(height, proof, path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !bytesEqual(stored, make([]byte, hashSize)) {\n\t\treturn errors.New(\"a commitment is stored at the path\")\n\t}\n\treturn nil\n}\n\n// provenCommitment returns the value of the router's commitments[keccak256(path)]\n// at height, zero if unset, as established by proof.\nfunc (c *EthLightClient) provenCommitment(height types.Height, proof any, path types.MerklePath) ([]byte, error) {\n\tvar mp MembershipProof\n\tswitch p := proof.(type) {\n\tcase MembershipProof:\n\t\tmp = p\n\tcase *MembershipProof:\n\t\tif p == nil {\n\t\t\treturn nil, errors.New(\"nil membership proof\")\n\t\t}\n\t\tmp = *p\n\tdefault:\n\t\treturn nil, ufmt.Errorf(\"expected ethereum.MembershipProof, got %T\", proof)\n\t}\n\tif height.RevisionNumber != 0 {\n\t\treturn nil, ufmt.Errorf(\"revision number must be 0, got %d\", height.RevisionNumber)\n\t}\n\tif c.ClientState.LatestSlot \u003c height.RevisionHeight {\n\t\treturn nil, ufmt.Errorf(\"proof slot %d is after the latest slot %d\", height.RevisionHeight, c.ClientState.LatestSlot)\n\t}\n\tcons, ok := c.GetConsensusState(height)\n\tif !ok {\n\t\treturn nil, ufmt.Errorf(\"no consensus state at slot %d\", height.RevisionHeight)\n\t}\n\tif len(path.KeyPath) != 1 {\n\t\treturn nil, ufmt.Errorf(\"path must have exactly one element, got %d\", len(path.KeyPath))\n\t}\n\tstorageRoot, err := accountStorageRoot(cons.StateRoot, c.ClientState.IBCContractAddress, mp.AccountProof)\n\tif err != nil {\n\t\treturn nil, ufmt.Errorf(\"account proof: %v\", err)\n\t}\n\tslot := commitmentStorageSlot(path.KeyPath[0], c.ClientState.IBCCommitmentSlot)\n\tv, err := storageValue(storageRoot, slot, mp.StorageProof)\n\tif err != nil {\n\t\treturn nil, ufmt.Errorf(\"storage proof: %v\", err)\n\t}\n\treturn v, nil\n}\n\n// commitmentStorageSlot is the storage slot of commitments[keccak256(path)]\n// in the router, whose commitments mapping is at mappingSlot.\nfunc commitmentStorageSlot(path, mappingSlot []byte) []byte {\n\treturn keccak(append(keccak(path), mappingSlot...))\n}\n\n// TrustingPeriods is how many sync committee periods (about 27 hours each on\n// mainnet) the latest trusted state stays usable. Past that the client\n// expires: its committees are too old to trust, so only a governed recovery\n// can bring it back.\nconst TrustingPeriods = 8\n\n// hasSupermajority reports whether at least 2/3 of the sync committee signed u.\nfunc hasSupermajority(u LightClientUpdate) bool {\n\treturn u.SyncAggregate.participants()*3 \u003e= u.SyncAggregate.size()*2\n}\n\n// expired reports whether more than TrustingPeriods sync committee periods'\n// worth of slots have passed since the latest trusted slot. It counts slots,\n// not period numbers, so the window is the same wherever in a period the\n// latest trusted slot falls.\nfunc (c *EthLightClient) expired() bool {\n\tcurrent, err := c.currentSlot()\n\tif err != nil {\n\t\treturn false\n\t}\n\tcs := c.ClientState\n\twindow := TrustingPeriods * cs.EpochsPerSyncCommitteePeriod * cs.SlotsPerEpoch\n\treturn current \u003e cs.LatestSlot+window\n}\n\n// checkLatestAnchor requires a client message to build on the latest trusted\n// state, as the spec's single store does, of a client that has not expired.\n// Anchoring at an older stored state would let a past sync committee, long\n// rotated out and never slashable, insert state roots below the latest slot\n// that packet proofs would accept, or freeze the client.\nfunc (c *EthLightClient) checkLatestAnchor(trustedSlot uint64) error {\n\tcs := c.ClientState\n\tif trustedSlot != cs.LatestSlot {\n\t\treturn ufmt.Errorf(\"updates must build on the latest trusted slot %d, not %d\", cs.LatestSlot, trustedSlot)\n\t}\n\tif c.expired() {\n\t\treturn ufmt.Errorf(\"client expired: its latest trusted slot %d is more than %d sync committee periods old\", cs.LatestSlot, TrustingPeriods)\n\t}\n\treturn nil\n}\n\n// Implements lightclient.Interface. The client expires TrustingPeriods sync\n// committee periods after its latest trusted slot and must then be recovered.\nfunc (c *EthLightClient) Status() lightclient.Status {\n\tif c.ClientState.IsFrozen {\n\t\treturn lightclient.Frozen\n\t}\n\tif _, ok := c.GetConsensusState(c.LatestHeight()); !ok {\n\t\treturn lightclient.Expired\n\t}\n\tif c.expired() {\n\t\treturn lightclient.Expired\n\t}\n\treturn lightclient.Active\n}\n\n// Implements lightclient.Interface.\nfunc (c *EthLightClient) LatestHeight() types.Height {\n\treturn heightAtSlot(c.ClientState.LatestSlot)\n}\n\n// Implements lightclient.Interface: the execution block time, in seconds.\nfunc (c *EthLightClient) TimestampAtHeight(height types.Height) (uint64, error) {\n\tcons, ok := c.GetConsensusState(height)\n\tif !ok {\n\t\treturn 0, ufmt.Errorf(\"no consensus state at height %s\", height.String())\n\t}\n\treturn cons.Timestamp, nil\n}\n\n// Implements lightclient.Interface. The substitute must track the same chain\n// and router; its latest consensus state is copied and the subject unfrozen.\nfunc (c *EthLightClient) RecoverClient(substitute lightclient.Interface) error {\n\tif err := c.CheckRecovery(substitute); err != nil {\n\t\treturn err\n\t}\n\tsub := substitute.(ethState).Ethereum()\n\ta, b := c.ClientState, sub.ClientState\n\tcons, _ := sub.GetConsensusState(sub.LatestHeight())\n\tcopied := copyConsensusState(*cons)\n\tc.setConsensusState(\u0026copied)\n\tupdated := *a\n\tupdated.LatestSlot = b.LatestSlot\n\tupdated.LatestExecutionBlockNumber = b.LatestExecutionBlockNumber\n\tupdated.MinSyncCommitteeParticipants = b.MinSyncCommitteeParticipants\n\tupdated.IsFrozen = false\n\tc.ClientState = \u0026updated\n\tc.TrustedFromSlot = copied.Slot\n\treturn nil\n}\n\n// CheckRecovery is why RecoverClient would refuse substitute, or nil; it\n// changes nothing, so a governed recovery is refused deterministically\n// before it runs.\nfunc (c *EthLightClient) CheckRecovery(substitute lightclient.Interface) error {\n\t// The v0 verifier, or a later version wrapping it.\n\ts, ok := substitute.(ethState)\n\tif !ok {\n\t\treturn ufmt.Errorf(\"substitute must be an ethereum client, got %T\", substitute)\n\t}\n\tsub := s.Ethereum()\n\ta, b := c.ClientState, sub.ClientState\n\tif a.ChainID != b.ChainID || !bytesEqual(a.GenesisValidatorsRoot, b.GenesisValidatorsRoot) ||\n\t\ta.ForkParameters != b.ForkParameters || a.SyncCommitteeSize != b.SyncCommitteeSize ||\n\t\ta.GenesisTime != b.GenesisTime || a.GenesisSlot != b.GenesisSlot ||\n\t\ta.SecondsPerSlot != b.SecondsPerSlot || a.SlotsPerEpoch != b.SlotsPerEpoch ||\n\t\ta.EpochsPerSyncCommitteePeriod != b.EpochsPerSyncCommitteePeriod ||\n\t\t!bytesEqual(a.IBCContractAddress, b.IBCContractAddress) ||\n\t\t!bytesEqual(a.IBCCommitmentSlot, b.IBCCommitmentSlot) {\n\t\treturn errors.New(\"substitute client tracks a different chain or router\")\n\t}\n\tif b.LatestSlot \u003c= a.LatestSlot {\n\t\treturn errors.New(\"substitute must be ahead of the subject\")\n\t}\n\tif _, ok := sub.GetConsensusState(sub.LatestHeight()); !ok {\n\t\treturn errors.New(\"substitute has no latest consensus state\")\n\t}\n\treturn nil\n}\n\n// Implements lightclient.Interface. Ethereum has no IBC upgrade path; replace\n// the client with RecoverClient instead.\nfunc (c *EthLightClient) VerifyUpgradeAndUpdateState(_, _, _, _ any) error {\n\treturn errors.New(\"the ethereum light client does not support upgrades\")\n}\n\nfunc (c *EthLightClient) setConsensusState(cons *ConsensusState) {\n\tc.ConsensusStateByHeight.Set(heightAtSlot(cons.Slot).StringNatSort(), cons)\n}\n\n// GetConsensusState returns the consensus state at height, if any and still\n// trusted: one below TrustedFromSlot, from before a recovery, is not. An\n// Ethereum height is revision 0 and a slot: any other revision holds none,\n// whatever store (the client's own, the overrides, the history) would be\n// read for its slot.\nfunc (c *EthLightClient) GetConsensusState(height types.Height) (*ConsensusState, bool) {\n\tif height.RevisionNumber != 0 || height.RevisionHeight \u003c c.TrustedFromSlot {\n\t\treturn nil, false\n\t}\n\tv := c.ConsensusStateByHeight.Get(height.StringNatSort())\n\tif v == nil {\n\t\t// A height the retired verifier trusted before a replacement: a\n\t\t// copy, so no pointer into the retired verifier ever escapes, to a\n\t\t// later conversion least of all.\n\t\tcons, ok := c.fromHistory(height)\n\t\tif !ok {\n\t\t\treturn nil, false\n\t\t}\n\t\tcopied := copyConsensusState(*cons)\n\t\treturn \u0026copied, true\n\t}\n\treturn v.(*ConsensusState), true\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/aib/ibc/lightclient/ethereum/v0\"\ngno = \"0.9\"\n"},{"name":"migrate.gno","body":"package ethereum\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/aib/encoding/wire/v0\"\n\t\"gno.land/p/aib/ibc/lightclient/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// MigrateForkParameters replaces the fork schedule of a live client in place,\n// keeping its client state otherwise, its consensus states and (in the IBC\n// core) its client id and packet state: the migration R16 of ADR pr6375 needs\n// before an Ethereum fork that this package version already verifies (its\n// containers and generalized indices are known here, only the activation\n// epoch was not). A fork whose containers change needs a new package version\n// and a conversion through the core's Client.SetLightClient instead.\n//\n// The new schedule may only change forks that are not active yet: every fork\n// whose epoch is at or before the current epoch (the later of the latest\n// trusted slot's and the block time's) must be unchanged, version and epoch,\n// and every changed fork must activate after the current epoch. The genesis\n// fork version and slot never change, the fork epochs stay in order, and the\n// schedule must differ from the current one. The client must be Active.\n//\n// Call it on the stored client (the core's verifier): it writes the new state\n// under the receiver's storage realm.\nfunc (c *EthLightClient) MigrateForkParameters(fp ForkParameters) error {\n\tif err := c.CheckForkParameters(fp); err != nil {\n\t\treturn err\n\t}\n\tupdated := *c.ClientState\n\tupdated.ForkParameters = fp\n\tc.ClientState = \u0026updated\n\treturn nil\n}\n\n// CheckForkParameters is why MigrateForkParameters would refuse fp, or nil;\n// it changes nothing, so a governed migration is refused deterministically\n// before it runs.\nfunc (c *EthLightClient) CheckForkParameters(fp ForkParameters) error {\n\tif status := c.Status(); status != lightclient.Active {\n\t\treturn ufmt.Errorf(\"cannot migrate a client with status %s\", status)\n\t}\n\tcs := c.ClientState\n\told := cs.ForkParameters\n\tif fp == old {\n\t\treturn errors.New(\"the fork schedule is unchanged\")\n\t}\n\tif fp.GenesisForkVersion != old.GenesisForkVersion || fp.GenesisSlot != old.GenesisSlot {\n\t\treturn errors.New(\"the genesis fork version and slot cannot change\")\n\t}\n\tcurrent := cs.LatestSlot\n\tif s, err := c.currentSlot(); err == nil \u0026\u0026 s \u003e current {\n\t\tcurrent = s\n\t}\n\tepoch := cs.epochAtSlot(current)\n\tolds, news := old.forks(), fp.forks()\n\tfor i := range olds {\n\t\to, n := olds[i], news[i]\n\t\tif o.Epoch \u003c= epoch {\n\t\t\tif n != o {\n\t\t\t\treturn ufmt.Errorf(\"fork %s is active since epoch %d and cannot change\", forkNames[i], o.Epoch)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif n != o \u0026\u0026 n.Epoch \u003c= epoch {\n\t\t\treturn ufmt.Errorf(\"fork %s cannot activate at epoch %d, at or before the current epoch %d\", forkNames[i], n.Epoch, epoch)\n\t\t}\n\t\tif i \u003e 0 \u0026\u0026 n.Epoch \u003c news[i-1].Epoch {\n\t\t\treturn ufmt.Errorf(\"fork %s at epoch %d precedes fork %s at epoch %d\", forkNames[i], n.Epoch, forkNames[i-1], news[i-1].Epoch)\n\t\t}\n\t}\n\treturn nil\n}\n\nvar forkNames = []string{\"altair\", \"bellatrix\", \"capella\", \"deneb\", \"electra\", \"fulu\", \"gloas\"}\n\n// forks lists the forks in activation order.\nfunc (p ForkParameters) forks() []Fork {\n\treturn []Fork{p.Altair, p.Bellatrix, p.Capella, p.Deneb, p.Electra, p.Fulu, p.Gloas}\n}\n\n// EncodeForkParameters writes p in the wire format: Blob(genesis fork\n// version, 4 bytes), Uint genesis slot, then for each fork from Altair to\n// Gloas Blob(version, 4 bytes) and Uint epoch.\nfunc EncodeForkParameters(p ForkParameters) []byte {\n\tw := wire.NewWriter()\n\tw.Blob(p.GenesisForkVersion[:]).Uint(p.GenesisSlot)\n\tfor _, f := range p.forks() {\n\t\tw.Blob(f.Version[:]).Uint(f.Epoch)\n\t}\n\treturn w.Bytes()\n}\n\n// DecodeForkParameters reads what EncodeForkParameters writes, refusing\n// trailing bytes.\nfunc DecodeForkParameters(bz []byte) (ForkParameters, error) {\n\td := newDecoder(bz)\n\tp := ForkParameters{GenesisForkVersion: d.version(), GenesisSlot: d.Uint()}\n\tfork := func() Fork { return Fork{Version: d.version(), Epoch: d.Uint()} }\n\tp.Altair = fork()\n\tp.Bellatrix = fork()\n\tp.Capella = fork()\n\tp.Deneb = fork()\n\tp.Electra = fork()\n\tp.Fulu = fork()\n\tp.Gloas = fork()\n\tif err := d.done(); err != nil {\n\t\treturn ForkParameters{}, ufmt.Errorf(\"fork parameters: %v\", err)\n\t}\n\treturn p, nil\n}\n\n// version reads a 4-byte fork version blob.\nfunc (d *decoder) version() [4]byte {\n\tvar v [4]byte\n\tb := d.Blob()\n\tif d.ok() \u0026\u0026 len(b) != 4 {\n\t\td.err = errors.New(\"wire: a fork version is 4 bytes\")\n\t}\n\tcopy(v[:], b)\n\treturn v\n}\n"},{"name":"mpt.gno","body":"package ethereum\n\nimport (\n\t\"crypto/keccak256\"\n\t\"encoding/hex\"\n\t\"errors\"\n)\n\n// Merkle-Patricia trie proof verification, as produced by eth_getProof.\n// See https://ethereum.org/developers/docs/data-structures-and-encoding/patricia-merkle-trie\n\nvar errMissingProofNode = errors.New(\"mpt: proof is missing a node\")\n\nfunc keccak(b []byte) []byte {\n\th := keccak256.Sum256(b)\n\treturn h[:]\n}\n\n// mptGet returns the value stored under keccak256(key) in the trie with the\n// given root, using proof (the list of RLP-encoded nodes on the path). found\n// is false when the proof shows that no value is stored there. An error\n// means the proof does not establish either.\nfunc mptGet(root, key []byte, proof [][]byte) (value []byte, found bool, err error) {\n\tnodes := make(map[string][]byte, len(proof))\n\tfor _, n := range proof {\n\t\tnodes[hex.EncodeToString(keccak(n))] = n\n\t}\n\tpath := toNibbles(keccak(key))\n\n\tnode, ok := nodes[hex.EncodeToString(root)]\n\tif !ok {\n\t\treturn nil, false, errMissingProofNode\n\t}\n\tfor {\n\t\titem, err := rlpDecode(node)\n\t\tif err != nil {\n\t\t\treturn nil, false, err\n\t\t}\n\t\telems, err := rlpList(item)\n\t\tif err != nil {\n\t\t\treturn nil, false, err\n\t\t}\n\n\t\tvar child rlpItem\n\t\tswitch len(elems) {\n\t\tcase 17: // branch\n\t\t\tif len(path) == 0 {\n\t\t\t\tv := elems[16]\n\t\t\t\tif v.isList {\n\t\t\t\t\treturn nil, false, errRLP\n\t\t\t\t}\n\t\t\t\treturn v.payload, len(v.payload) \u003e 0, nil\n\t\t\t}\n\t\t\tchild = elems[path[0]]\n\t\t\tpath = path[1:]\n\t\tcase 2: // extension or leaf\n\t\t\tif elems[0].isList {\n\t\t\t\treturn nil, false, errRLP\n\t\t\t}\n\t\t\tnodePath, isLeaf, err := decodeCompactPath(elems[0].payload)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, false, err\n\t\t\t}\n\t\t\tif isLeaf {\n\t\t\t\tif !nibblesEqual(nodePath, path) {\n\t\t\t\t\treturn nil, false, nil // diverges: absent\n\t\t\t\t}\n\t\t\t\tif elems[1].isList {\n\t\t\t\t\treturn nil, false, errRLP\n\t\t\t\t}\n\t\t\t\treturn elems[1].payload, true, nil\n\t\t\t}\n\t\t\tif len(path) \u003c len(nodePath) || !nibblesEqual(nodePath, path[:len(nodePath)]) {\n\t\t\t\treturn nil, false, nil // diverges: absent\n\t\t\t}\n\t\t\tpath = path[len(nodePath):]\n\t\t\tchild = elems[1]\n\t\tdefault:\n\t\t\treturn nil, false, errRLP\n\t\t}\n\n\t\t// Resolve the child reference: empty means absent, a list is an\n\t\t// embedded node (encoded in under 32 bytes), 32 bytes is a hash.\n\t\tswitch {\n\t\tcase child.isList:\n\t\t\tnode = child.raw\n\t\tcase len(child.payload) == 0:\n\t\t\treturn nil, false, nil\n\t\tcase len(child.payload) == 32:\n\t\t\tnode, ok = nodes[hex.EncodeToString(child.payload)]\n\t\t\tif !ok {\n\t\t\t\treturn nil, false, errMissingProofNode\n\t\t\t}\n\t\tdefault:\n\t\t\treturn nil, false, errRLP\n\t\t}\n\t}\n}\n\nfunc toNibbles(b []byte) []byte {\n\tn := make([]byte, 2*len(b))\n\tfor i, c := range b {\n\t\tn[2*i] = c \u003e\u003e 4\n\t\tn[2*i+1] = c \u0026 0x0f\n\t}\n\treturn n\n}\n\n// decodeCompactPath decodes a hex-prefix encoded node path.\nfunc decodeCompactPath(b []byte) (nibbles []byte, isLeaf bool, err error) {\n\tif len(b) == 0 {\n\t\treturn nil, false, errRLP\n\t}\n\tflag := b[0] \u003e\u003e 4\n\tif flag \u003e 3 {\n\t\treturn nil, false, errRLP\n\t}\n\tisLeaf = flag \u003e= 2\n\tall := toNibbles(b)\n\tif flag%2 == 1 { // odd: the first nibble is part of the path\n\t\treturn all[1:], isLeaf, nil\n\t}\n\tif b[0]\u00260x0f != 0 {\n\t\treturn nil, false, errRLP\n\t}\n\treturn all[2:], isLeaf, nil\n}\n\nfunc nibblesEqual(a, b []byte) bool {\n\treturn bytesEqual(a, b)\n}\n\n// verifyAccountStorageRoot returns the storage root of the account at\n// address in the state trie with root stateRoot.\nfunc accountStorageRoot(stateRoot, address []byte, proof [][]byte) ([]byte, error) {\n\tv, found, err := mptGet(stateRoot, address, proof)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !found {\n\t\treturn nil, errors.New(\"mpt: account not found\")\n\t}\n\titem, err := rlpDecode(v)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfields, err := rlpList(item)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// [nonce, balance, storageRoot, codeHash]\n\tif len(fields) != 4 || fields[2].isList || len(fields[2].payload) != 32 {\n\t\treturn nil, errRLP\n\t}\n\treturn fields[2].payload, nil\n}\n\n// storageValue returns the 32-byte value of slot key in the storage trie with\n// the given root, zero when the proof shows it is unset.\nfunc storageValue(storageRoot, key []byte, proof [][]byte) ([]byte, error) {\n\tv, found, err := mptGet(storageRoot, key, proof)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tout := make([]byte, 32)\n\tif !found {\n\t\treturn out, nil\n\t}\n\t// The leaf holds rlp(value) with leading zero bytes stripped.\n\titem, err := rlpDecode(v)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif item.isList || len(item.payload) \u003e 32 || len(item.payload) == 0 || item.payload[0] == 0 {\n\t\treturn nil, errRLP\n\t}\n\tcopy(out[32-len(item.payload):], item.payload)\n\treturn out, nil\n}\n\n// decodeExecutionHeader reads the state root, block number and timestamp of\n// an RLP-encoded execution block header. Later forks only append fields.\nfunc decodeExecutionHeader(rlp []byte) (executionData, error) {\n\titem, err := rlpDecode(rlp)\n\tif err != nil {\n\t\treturn executionData{}, err\n\t}\n\tfields, err := rlpList(item)\n\tif err != nil {\n\t\treturn executionData{}, err\n\t}\n\t// [parentHash, ommersHash, coinbase, stateRoot, txRoot, receiptsRoot,\n\t//  bloom, difficulty, number, gasLimit, gasUsed, timestamp, ...]\n\tif len(fields) \u003c 12 || fields[3].isList || len(fields[3].payload) != 32 {\n\t\treturn executionData{}, errors.New(\"invalid execution header\")\n\t}\n\tnumber, err := rlpUint64(fields[8])\n\tif err != nil {\n\t\treturn executionData{}, err\n\t}\n\ttimestamp, err := rlpUint64(fields[11])\n\tif err != nil {\n\t\treturn executionData{}, err\n\t}\n\treturn executionData{stateRoot: fields[3].payload, timestamp: timestamp, blockNumber: number}, nil\n}\n\n// rlpUint64 reads a canonical RLP unsigned integer of at most 8 bytes.\nfunc rlpUint64(it rlpItem) (uint64, error) {\n\tif it.isList || len(it.payload) \u003e 8 || (len(it.payload) \u003e 0 \u0026\u0026 it.payload[0] == 0) {\n\t\treturn 0, errRLP\n\t}\n\tvar v uint64\n\tfor _, c := range it.payload {\n\t\tv = v\u003c\u003c8 | uint64(c)\n\t}\n\treturn v, nil\n}\n"},{"name":"receipt.gno","body":"package ethereum\n\nimport (\n\t\"gno.land/p/aib/encoding/abi/v0\"\n\t\"gno.land/p/aib/ibc/types/v0\"\n)\n\n// PacketReceiptCommitment is the value an Ethereum ICS26 router\n// (solidity-ibc-eureka) stores at a received packet's ICS24 receipt path:\n// keccak256(abi.encode(packet)), the packet as the router's Packet struct\n// (sequence, sourceClient, destClient, timeoutTimestamp, payloads with\n// sourcePort, destPort, version, encoding, value). A reconciliation\n// recomputes it to tell the packet's receipt from another's (ADR pr6375\n// §7.12).\nfunc PacketReceiptCommitment(p types.Packet) []byte {\n\tpayloads := make([]abi.Value, len(p.Payloads))\n\tfor i, pl := range p.Payloads {\n\t\tpayloads[i] = abi.Tuple(abi.String(pl.SourcePort), abi.String(pl.DestinationPort), abi.String(pl.Version),\n\t\t\tabi.String(pl.Encoding), abi.Bytes(pl.Value))\n\t}\n\treturn keccak(abi.Encode(abi.Tuple(abi.Uint64(p.Sequence), abi.String(p.SourceClient), abi.String(p.DestinationClient),\n\t\tabi.Uint64(p.TimeoutTimestamp), abi.Array(payloads...))))\n}\n\n// ProvenValue is the value proof establishes for the router's commitment at\n// path at height, zero (32 zero bytes) if none is stored: what a\n// reconciliation reads to tell a delivered, refundable or collided packet\n// apart.\nfunc (c *EthLightClient) ProvenValue(height types.Height, proof any, path types.MerklePath) ([]byte, error) {\n\treturn c.provenCommitment(height, proof, path)\n}\n"},{"name":"rlp.gno","body":"package ethereum\n\nimport \"errors\"\n\n// Minimal RLP decoding, enough to walk Merkle-Patricia trie nodes and read\n// an account. See https://ethereum.org/developers/docs/data-structures-and-encoding/rlp\n\nvar errRLP = errors.New(\"invalid rlp\")\n\n// rlpItem is one decoded RLP item. For a string, payload is its bytes; for a\n// list, payload is the concatenated encoding of its elements. raw is the\n// item's full encoding.\ntype rlpItem struct {\n\tisList  bool\n\tpayload []byte\n\traw     []byte\n}\n\n// rlpSplit decodes the first item of b and returns it with the bytes that\n// follow it.\nfunc rlpSplit(b []byte) (rlpItem, []byte, error) {\n\tif len(b) == 0 {\n\t\treturn rlpItem{}, nil, errRLP\n\t}\n\tprefix := b[0]\n\tvar (\n\t\tisList         bool\n\t\toffset, length int\n\t)\n\tswitch {\n\tcase prefix \u003c 0x80:\n\t\treturn rlpItem{payload: b[:1], raw: b[:1]}, b[1:], nil\n\tcase prefix \u003c= 0xb7:\n\t\toffset, length = 1, int(prefix-0x80)\n\t\tif length == 1 \u0026\u0026 len(b) \u003e 1 \u0026\u0026 b[1] \u003c 0x80 {\n\t\t\treturn rlpItem{}, nil, errRLP // single byte must be encoded as itself\n\t\t}\n\tcase prefix \u003c= 0xbf:\n\t\tn := int(prefix - 0xb7)\n\t\tl, err := rlpLength(b, n)\n\t\tif err != nil {\n\t\t\treturn rlpItem{}, nil, err\n\t\t}\n\t\toffset, length = 1+n, l\n\tcase prefix \u003c= 0xf7:\n\t\tisList = true\n\t\toffset, length = 1, int(prefix-0xc0)\n\tdefault:\n\t\tisList = true\n\t\tn := int(prefix - 0xf7)\n\t\tl, err := rlpLength(b, n)\n\t\tif err != nil {\n\t\t\treturn rlpItem{}, nil, err\n\t\t}\n\t\toffset, length = 1+n, l\n\t}\n\tend := offset + length\n\tif end \u003c offset || end \u003e len(b) {\n\t\treturn rlpItem{}, nil, errRLP\n\t}\n\treturn rlpItem{isList: isList, payload: b[offset:end], raw: b[:end]}, b[end:], nil\n}\n\n// rlpLength reads the n-byte big-endian length following the prefix byte of\n// a long string or list, rejecting non-canonical encodings.\nfunc rlpLength(b []byte, n int) (int, error) {\n\tif n \u003e 4 || len(b) \u003c 1+n || b[1] == 0 {\n\t\treturn 0, errRLP\n\t}\n\tl := 0\n\tfor i := 1; i \u003c= n; i++ {\n\t\tl = l\u003c\u003c8 | int(b[i])\n\t}\n\tif l \u003c 56 {\n\t\treturn 0, errRLP\n\t}\n\treturn l, nil\n}\n\n// rlpDecode decodes b, which must hold exactly one item.\nfunc rlpDecode(b []byte) (rlpItem, error) {\n\titem, rest, err := rlpSplit(b)\n\tif err != nil {\n\t\treturn rlpItem{}, err\n\t}\n\tif len(rest) != 0 {\n\t\treturn rlpItem{}, errRLP\n\t}\n\treturn item, nil\n}\n\n// rlpList returns the elements of a list item.\nfunc rlpList(item rlpItem) ([]rlpItem, error) {\n\tif !item.isList {\n\t\treturn nil, errRLP\n\t}\n\tvar items []rlpItem\n\trest := item.payload\n\tfor len(rest) \u003e 0 {\n\t\tvar (\n\t\t\tit  rlpItem\n\t\t\terr error\n\t\t)\n\t\tit, rest, err = rlpSplit(rest)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\titems = append(items, it)\n\t}\n\treturn items, nil\n}\n"},{"name":"ssz.gno","body":"package ethereum\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/binary\"\n)\n\n// SSZ hash_tree_root helpers, restricted to the containers the light client\n// hashes. See https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md\n\nconst chunkSize = 32\n\n// maxZeroHashDepth bounds the merkleization depth used here: the largest\n// tree is a 512-key sync committee (depth 9).\nconst maxZeroHashDepth = 16\n\n// zeroHashes[i] is the root of a tree of 2^i zero chunks.\nvar zeroHashes [maxZeroHashDepth + 1][]byte\n\nfunc init() {\n\tzeroHashes[0] = make([]byte, chunkSize)\n\tfor i := 1; i \u003c= maxZeroHashDepth; i++ {\n\t\tzeroHashes[i] = hash2(zeroHashes[i-1], zeroHashes[i-1])\n\t}\n}\n\n// hash2 returns sha256(a || b).\nfunc hash2(a, b []byte) []byte {\n\tbuf := make([]byte, 0, len(a)+len(b))\n\tbuf = append(buf, a...)\n\tbuf = append(buf, b...)\n\th := sha256.Sum256(buf)\n\treturn h[:]\n}\n\n// merkleize returns the root of chunks padded with zero chunks to the next\n// power of two of limit leaves. len(chunks) must not exceed limit.\nfunc merkleize(chunks [][]byte, limit int) []byte {\n\tif len(chunks) \u003e limit {\n\t\tpanic(\"ssz: more chunks than the limit\")\n\t}\n\tdepth := 0\n\tfor (1 \u003c\u003c depth) \u003c limit {\n\t\tdepth++\n\t}\n\tif depth \u003e maxZeroHashDepth {\n\t\tpanic(\"ssz: tree too deep\")\n\t}\n\tif len(chunks) == 0 {\n\t\treturn zeroHashes[depth]\n\t}\n\tlayer := chunks\n\tfor d := 0; d \u003c depth; d++ {\n\t\tnext := make([][]byte, (len(layer)+1)/2)\n\t\tfor i := 0; i \u003c len(layer); i += 2 {\n\t\t\tright := zeroHashes[d]\n\t\t\tif i+1 \u003c len(layer) {\n\t\t\t\tright = layer[i+1]\n\t\t\t}\n\t\t\tnext[i/2] = hash2(layer[i], right)\n\t\t}\n\t\tlayer = next\n\t}\n\treturn layer[0]\n}\n\n// uint64Chunk is the SSZ chunk of a uint64: little-endian, zero-padded.\nfunc uint64Chunk(v uint64) []byte {\n\tc := make([]byte, chunkSize)\n\tbinary.LittleEndian.PutUint64(c, v)\n\treturn c\n}\n\n// padChunk right-pads b (at most 32 bytes) to a chunk.\nfunc padChunk(b []byte) []byte {\n\tif len(b) \u003e chunkSize {\n\t\tpanic(\"ssz: value longer than a chunk\")\n\t}\n\tc := make([]byte, chunkSize)\n\tcopy(c, b)\n\treturn c\n}\n\n// pack splits b into chunks, the last one zero-padded.\nfunc pack(b []byte) [][]byte {\n\tvar chunks [][]byte\n\tfor i := 0; i \u003c len(b); i += chunkSize {\n\t\tend := i + chunkSize\n\t\tif end \u003e len(b) {\n\t\t\tend = len(b)\n\t\t}\n\t\tchunks = append(chunks, padChunk(b[i:end]))\n\t}\n\treturn chunks\n}\n\nfunc mixInLength(root []byte, length uint64) []byte {\n\treturn hash2(root, uint64Chunk(length))\n}\n\n// HashTreeRoot of BeaconBlockHeader.\nfunc (h BeaconBlockHeader) HashTreeRoot() []byte {\n\treturn merkleize([][]byte{\n\t\tuint64Chunk(h.Slot),\n\t\tuint64Chunk(h.ProposerIndex),\n\t\th.ParentRoot,\n\t\th.StateRoot,\n\t\th.BodyRoot,\n\t}, 5)\n}\n\nconst (\n\tlogsBloomSize   = 256\n\tmaxExtraDataLen = 32\n)\n\n// HashTreeRoot of the Deneb-and-later ExecutionPayloadHeader. Electra and\n// Fulu did not change this container.\nfunc (h ExecutionPayloadHeader) HashTreeRoot() []byte {\n\t// base_fee_per_gas is a uint256: little-endian in its chunk.\n\tbaseFee := make([]byte, chunkSize)\n\tfor i := 0; i \u003c len(h.BaseFeePerGas) \u0026\u0026 i \u003c chunkSize; i++ {\n\t\tbaseFee[i] = h.BaseFeePerGas[len(h.BaseFeePerGas)-1-i]\n\t}\n\treturn merkleize([][]byte{\n\t\th.ParentHash,\n\t\tpadChunk(h.FeeRecipient),\n\t\th.StateRoot,\n\t\th.ReceiptsRoot,\n\t\tmerkleize(pack(h.LogsBloom), logsBloomSize/chunkSize),\n\t\th.PrevRandao,\n\t\tuint64Chunk(h.BlockNumber),\n\t\tuint64Chunk(h.GasLimit),\n\t\tuint64Chunk(h.GasUsed),\n\t\tuint64Chunk(h.Timestamp),\n\t\tmixInLength(merkleize(pack(h.ExtraData), (maxExtraDataLen+chunkSize-1)/chunkSize), uint64(len(h.ExtraData))),\n\t\tbaseFee,\n\t\th.BlockHash,\n\t\th.TransactionsRoot,\n\t\th.WithdrawalsRoot,\n\t\tuint64Chunk(h.BlobGasUsed),\n\t\tuint64Chunk(h.ExcessBlobGas),\n\t}, 17)\n}\n\n// pubkeyLeaf is the hash_tree_root of a 48-byte BLS public key, its leaf in\n// a sync committee's pubkeys vector.\nfunc pubkeyLeaf(pk []byte) []byte {\n\treturn hash2(pk[:chunkSize], padChunk(pk[chunkSize:]))\n}\n\n// computeDomain implements compute_domain for the given domain type.\nfunc computeDomain(domainType [4]byte, forkVersion [4]byte, genesisValidatorsRoot []byte) []byte {\n\tforkDataRoot := hash2(padChunk(forkVersion[:]), genesisValidatorsRoot)\n\tdomain := make([]byte, chunkSize)\n\tcopy(domain, domainType[:])\n\tcopy(domain[4:], forkDataRoot[:28])\n\treturn domain\n}\n\n// computeSigningRoot implements compute_signing_root.\nfunc computeSigningRoot(objectRoot, domain []byte) []byte {\n\treturn hash2(objectRoot, domain)\n}\n\n// floorLog2 returns floor(log2(n)) for n \u003e 0.\nfunc floorLog2(n uint64) int {\n\td := -1\n\tfor n \u003e 0 {\n\t\tn \u003e\u003e= 1\n\t\td++\n\t}\n\treturn d\n}\n\n// isValidMerkleBranch implements is_valid_normalized_merkle_branch for a\n// generalized index: leaf is at gindex in the tree whose root is root. A\n// branch longer than the index's depth must start with zero hashes.\nfunc isValidMerkleBranch(leaf []byte, branch [][]byte, gindex uint64, root []byte) bool {\n\tdepth := floorLog2(gindex)\n\tif len(branch) \u003c depth {\n\t\treturn false\n\t}\n\tfor _, extra := range branch[:len(branch)-depth] {\n\t\tif !bytesEqual(extra, zeroHashes[0]) {\n\t\t\treturn false\n\t\t}\n\t}\n\tbranch = branch[len(branch)-depth:]\n\tindex := gindex - (1 \u003c\u003c uint(depth))\n\tvalue := leaf\n\tfor i := 0; i \u003c depth; i++ {\n\t\tif len(branch[i]) != chunkSize {\n\t\t\treturn false\n\t\t}\n\t\tif (index\u003e\u003euint(i))\u00261 == 1 {\n\t\t\tvalue = hash2(branch[i], value)\n\t\t} else {\n\t\t\tvalue = hash2(value, branch[i])\n\t\t}\n\t}\n\treturn bytesEqual(value, root)\n}\n\nfunc bytesEqual(a, b []byte) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i := range a {\n\t\tif a[i] != b[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n"},{"name":"types.gno","body":"package ethereum\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// ClientType is the IBC client type of this light client; client IDs are\n// ethereum-\u003cn\u003e.\nconst ClientType = \"ethereum\"\n\n// Generalized indices of the light-client proofs. Electra's hold through\n// Fulu; Gloas (EIP-7688, EIP-7732) moved them. See the light-client sync\n// protocols of both forks.\nconst (\n\tfinalizedRootGindexElectra     = 169\n\tnextSyncCommitteeGindexElectra = 87\n\texecutionPayloadGindex         = 25\n\n\tfinalizedRootGindexGloas      = 735\n\tnextSyncCommitteeGindexGloas  = 2946\n\texecutionBlockHashGindexGloas = 2856\n)\n\n// FarFutureEpoch marks a fork that is not scheduled.\nconst FarFutureEpoch = ^uint64(0)\n\nvar domainSyncCommittee = [4]byte{0x07, 0x00, 0x00, 0x00}\n\nconst (\n\tblsPubKeySize    = 48\n\tblsSignatureSize = 96\n\thashSize         = 32\n\taddressSize      = 20\n)\n\n// Fork is a consensus fork version and its activation epoch.\ntype Fork struct {\n\tVersion [4]byte\n\tEpoch   uint64\n}\n\n// ForkParameters are the forks of the tracked chain.\ntype ForkParameters struct {\n\tGenesisForkVersion [4]byte\n\tGenesisSlot        uint64\n\tAltair             Fork\n\tBellatrix          Fork\n\tCapella            Fork\n\tDeneb              Fork\n\tElectra            Fork\n\tFulu               Fork\n\t// Gloas is the ePBS fork. Set its epoch to FarFutureEpoch while it is\n\t// not scheduled.\n\tGloas Fork\n}\n\n// forkVersion implements compute_fork_version.\nfunc (p ForkParameters) forkVersion(epoch uint64) [4]byte {\n\tswitch {\n\tcase epoch \u003e= p.Gloas.Epoch:\n\t\treturn p.Gloas.Version\n\tcase epoch \u003e= p.Fulu.Epoch:\n\t\treturn p.Fulu.Version\n\tcase epoch \u003e= p.Electra.Epoch:\n\t\treturn p.Electra.Version\n\tcase epoch \u003e= p.Deneb.Epoch:\n\t\treturn p.Deneb.Version\n\tcase epoch \u003e= p.Capella.Epoch:\n\t\treturn p.Capella.Version\n\tcase epoch \u003e= p.Bellatrix.Epoch:\n\t\treturn p.Bellatrix.Version\n\tcase epoch \u003e= p.Altair.Epoch:\n\t\treturn p.Altair.Version\n\t}\n\treturn p.GenesisForkVersion\n}\n\n// ClientState is the state of an Ethereum light client. It mirrors the\n// client state of the Ethereum light client in cosmos/solidity-ibc-eureka.\ntype ClientState struct {\n\tChainID                      uint64\n\tGenesisValidatorsRoot        []byte\n\tMinSyncCommitteeParticipants uint64\n\tSyncCommitteeSize            uint64\n\tGenesisTime                  uint64\n\tGenesisSlot                  uint64\n\tForkParameters               ForkParameters\n\tSecondsPerSlot               uint64\n\tSlotsPerEpoch                uint64\n\tEpochsPerSyncCommitteePeriod uint64\n\tLatestSlot                   uint64\n\tLatestExecutionBlockNumber   uint64\n\tIsFrozen                     bool\n\t// IBCContractAddress is the ICS26 router whose storage holds the\n\t// commitments, and IBCCommitmentSlot the storage slot of its commitments\n\t// mapping.\n\tIBCContractAddress []byte\n\tIBCCommitmentSlot  []byte\n}\n\nfunc (ClientState) ClientType() string { return ClientType }\n\nfunc (cs ClientState) ValidateBasic() error {\n\tswitch {\n\tcase len(cs.GenesisValidatorsRoot) != hashSize:\n\t\treturn errors.New(\"genesis validators root must be 32 bytes\")\n\tcase cs.SyncCommitteeSize \u003c 8 || cs.SyncCommitteeSize\u0026(cs.SyncCommitteeSize-1) != 0 || cs.SyncCommitteeSize \u003e 1\u003c\u003cmaxZeroHashDepth:\n\t\treturn ufmt.Errorf(\"invalid sync committee size %d\", cs.SyncCommitteeSize)\n\tcase cs.MinSyncCommitteeParticipants == 0 || cs.MinSyncCommitteeParticipants \u003e cs.SyncCommitteeSize:\n\t\treturn ufmt.Errorf(\"invalid min sync committee participants %d\", cs.MinSyncCommitteeParticipants)\n\tcase cs.SecondsPerSlot == 0 || cs.SlotsPerEpoch == 0 || cs.EpochsPerSyncCommitteePeriod == 0:\n\t\treturn errors.New(\"seconds per slot, slots per epoch and epochs per period must be positive\")\n\tcase cs.LatestSlot \u003c cs.GenesisSlot:\n\t\treturn errors.New(\"latest slot precedes genesis slot\")\n\tcase cs.IsFrozen:\n\t\treturn errors.New(\"cannot create a frozen client\")\n\tcase len(cs.IBCContractAddress) != addressSize:\n\t\treturn errors.New(\"ibc contract address must be 20 bytes\")\n\tcase len(cs.IBCCommitmentSlot) != hashSize:\n\t\treturn errors.New(\"ibc commitment slot must be 32 bytes\")\n\t}\n\treturn nil\n}\n\nfunc (cs ClientState) epochAtSlot(slot uint64) uint64 { return slot / cs.SlotsPerEpoch }\n\nfunc (cs ClientState) periodAtSlot(slot uint64) uint64 {\n\treturn cs.epochAtSlot(slot) / cs.EpochsPerSyncCommitteePeriod\n}\n\n// slotAtTimestamp returns the slot at unix time t, and false before genesis.\nfunc (cs ClientState) slotAtTimestamp(t uint64) (uint64, bool) {\n\tif t \u003c cs.GenesisTime {\n\t\treturn 0, false\n\t}\n\treturn (t-cs.GenesisTime)/cs.SecondsPerSlot + cs.GenesisSlot, true\n}\n\nfunc (cs ClientState) isGloas(slot uint64) bool {\n\treturn cs.epochAtSlot(slot) \u003e= cs.ForkParameters.Gloas.Epoch\n}\n\nfunc (cs ClientState) finalizedRootGindex(slot uint64) uint64 {\n\tif cs.isGloas(slot) {\n\t\treturn finalizedRootGindexGloas\n\t}\n\treturn finalizedRootGindexElectra\n}\n\nfunc (cs ClientState) nextSyncCommitteeGindex(slot uint64) uint64 {\n\tif cs.isGloas(slot) {\n\t\treturn nextSyncCommitteeGindexGloas\n\t}\n\treturn nextSyncCommitteeGindexElectra\n}\n\n// verifySupportedFork rejects slots before Electra, whose generalized\n// indices differ from the ones this client uses.\nfunc (cs ClientState) verifySupportedFork(slot uint64) error {\n\tif cs.epochAtSlot(slot) \u003c cs.ForkParameters.Electra.Epoch {\n\t\treturn ufmt.Errorf(\"slot %d is before Electra, which is not supported\", slot)\n\t}\n\treturn nil\n}\n\n// SummarizedSyncCommittee commits to a sync committee: PubkeysHash is the\n// hash_tree_root of its pubkeys vector.\ntype SummarizedSyncCommittee struct {\n\tPubkeysHash     []byte\n\tAggregatePubkey []byte\n}\n\nfunc (s SummarizedSyncCommittee) equal(o SummarizedSyncCommittee) bool {\n\treturn bytesEqual(s.PubkeysHash, o.PubkeysHash) \u0026\u0026 bytesEqual(s.AggregatePubkey, o.AggregatePubkey)\n}\n\n// ConsensusState is the state trusted at a finalized beacon slot. StateRoot is\n// the execution state root, against which storage proofs are verified, and\n// Timestamp the execution block time in seconds.\ntype ConsensusState struct {\n\tSlot                 uint64\n\tStateRoot            []byte\n\tTimestamp            uint64\n\tCurrentSyncCommittee SummarizedSyncCommittee\n\tNextSyncCommittee    *SummarizedSyncCommittee\n}\n\nfunc (ConsensusState) ClientType() string { return ClientType }\n\nfunc (cs ConsensusState) ValidateBasic() error {\n\tif len(cs.StateRoot) != hashSize {\n\t\treturn errors.New(\"state root must be 32 bytes\")\n\t}\n\tif err := validateSummary(cs.CurrentSyncCommittee); err != nil {\n\t\treturn ufmt.Errorf(\"current sync committee: %v\", err)\n\t}\n\tif cs.NextSyncCommittee != nil {\n\t\tif err := validateSummary(*cs.NextSyncCommittee); err != nil {\n\t\t\treturn ufmt.Errorf(\"next sync committee: %v\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc validateSummary(s SummarizedSyncCommittee) error {\n\tif len(s.PubkeysHash) != hashSize {\n\t\treturn errors.New(\"pubkeys hash must be 32 bytes\")\n\t}\n\tif len(s.AggregatePubkey) != blsPubKeySize {\n\t\treturn errors.New(\"aggregate pubkey must be 48 bytes\")\n\t}\n\treturn nil\n}\n\n// validateBranch requires every node of a Merkle branch to be one 32-byte\n// chunk: a longer or shorter node would change where its bytes fall in the\n// parent hash.\nfunc validateBranch(branch [][]byte) error {\n\tfor i, node := range branch {\n\t\tif len(node) != hashSize {\n\t\t\treturn ufmt.Errorf(\"node %d is %d bytes, not 32\", i, len(node))\n\t\t}\n\t}\n\treturn nil\n}\n\n// validateSizes checks the fixed-size fields SSZ hashes as whole chunks.\nfunc (h BeaconBlockHeader) validateSizes() error {\n\tif len(h.ParentRoot) != hashSize || len(h.StateRoot) != hashSize || len(h.BodyRoot) != hashSize {\n\t\treturn errors.New(\"beacon header roots must be 32 bytes\")\n\t}\n\treturn nil\n}\n\n// validateSizes checks every field of the execution payload header against its\n// SSZ size: fixed-size fields exactly, bounded ones within their bound.\nfunc (h ExecutionPayloadHeader) validateSizes() error {\n\tfor _, f := range []struct {\n\t\tname string\n\t\tb    []byte\n\t\tsize int\n\t}{\n\t\t{\"parent hash\", h.ParentHash, hashSize},\n\t\t{\"fee recipient\", h.FeeRecipient, 20},\n\t\t{\"state root\", h.StateRoot, hashSize},\n\t\t{\"receipts root\", h.ReceiptsRoot, hashSize},\n\t\t{\"logs bloom\", h.LogsBloom, logsBloomSize},\n\t\t{\"prev randao\", h.PrevRandao, hashSize},\n\t\t{\"block hash\", h.BlockHash, hashSize},\n\t\t{\"transactions root\", h.TransactionsRoot, hashSize},\n\t\t{\"withdrawals root\", h.WithdrawalsRoot, hashSize},\n\t} {\n\t\tif len(f.b) != f.size {\n\t\t\treturn ufmt.Errorf(\"execution header %s must be %d bytes, got %d\", f.name, f.size, len(f.b))\n\t\t}\n\t}\n\tif len(h.ExtraData) \u003e maxExtraDataLen {\n\t\treturn ufmt.Errorf(\"execution header extra data exceeds %d bytes\", maxExtraDataLen)\n\t}\n\tif len(h.BaseFeePerGas) \u003e chunkSize {\n\t\treturn errors.New(\"execution header base fee exceeds 32 bytes\")\n\t}\n\treturn nil\n}\n\n// BeaconBlockHeader is the consensus-layer block header.\ntype BeaconBlockHeader struct {\n\tSlot          uint64\n\tProposerIndex uint64\n\tParentRoot    []byte\n\tStateRoot     []byte\n\tBodyRoot      []byte\n}\n\n// ExecutionPayloadHeader is the Deneb-and-later execution payload header.\n// BaseFeePerGas is a big-endian uint256 of at most 32 bytes.\ntype ExecutionPayloadHeader struct {\n\tParentHash       []byte\n\tFeeRecipient     []byte\n\tStateRoot        []byte\n\tReceiptsRoot     []byte\n\tLogsBloom        []byte\n\tPrevRandao       []byte\n\tBlockNumber      uint64\n\tGasLimit         uint64\n\tGasUsed          uint64\n\tTimestamp        uint64\n\tExtraData        []byte\n\tBaseFeePerGas    []byte\n\tBlockHash        []byte\n\tTransactionsRoot []byte\n\tWithdrawalsRoot  []byte\n\tBlobGasUsed      uint64\n\tExcessBlobGas    uint64\n}\n\n// LightClientHeader is a beacon header with the execution data it commits to\n// and the branch proving it in the beacon block body. Before Gloas that is\n// the execution payload header (Execution); from Gloas on, it is only the\n// hash of the latest execution block (ExecutionBlockHash).\ntype LightClientHeader struct {\n\tBeacon             BeaconBlockHeader\n\tExecution          ExecutionPayloadHeader\n\tExecutionBlockHash []byte\n\tExecutionBranch    [][]byte\n}\n\n// committeeRoot is the hash_tree_root of the SyncCommittee container the\n// summary commits to.\nfunc (s SummarizedSyncCommittee) committeeRoot() []byte {\n\tagg := hash2(s.AggregatePubkey[:chunkSize], padChunk(s.AggregatePubkey[chunkSize:]))\n\treturn hash2(s.PubkeysHash, agg)\n}\n\n// NonSigner is a sync committee member whose participation bit is unset: its\n// public key and the branch proving it at its index in the committee's\n// pubkeys vector.\ntype NonSigner struct {\n\tPubkey []byte\n\tBranch [][]byte\n}\n\n// SyncAggregate is the sync committee's signature over an attested header.\ntype SyncAggregate struct {\n\tSyncCommitteeBits      []byte\n\tSyncCommitteeSignature []byte\n}\n\nfunc (a SyncAggregate) participants() uint64 {\n\tvar n uint64\n\tfor _, b := range a.SyncCommitteeBits {\n\t\tfor ; b != 0; b \u0026= b - 1 {\n\t\t\tn++\n\t\t}\n\t}\n\treturn n\n}\n\nfunc (a SyncAggregate) size() uint64 { return uint64(len(a.SyncCommitteeBits)) * 8 }\n\n// LightClientUpdate is a light-client update as served by the beacon API,\n// with the sync committees reduced to what the client verifies: the next\n// committee as its summary (its pubkeys root and aggregate key, which is all\n// its branch commits to), and of the signing committee only the members that\n// did not sign. NextSyncCommittee and NextSyncCommitteeBranch are both set or\n// both unset.\ntype LightClientUpdate struct {\n\tAttestedHeader          LightClientHeader\n\tNextSyncCommittee       *SummarizedSyncCommittee\n\tNextSyncCommitteeBranch [][]byte\n\tFinalizedHeader         LightClientHeader\n\tFinalityBranch          [][]byte\n\tSyncAggregate           SyncAggregate\n\tSignatureSlot           uint64\n\t// FinalizedExecutionHeader is the RLP-encoded execution block header\n\t// whose hash is FinalizedHeader.ExecutionBlockHash, from which a Gloas\n\t// update takes the execution state root. Unused before Gloas.\n\tFinalizedExecutionHeader []byte\n\t// NonSigners are the members of the signing committee whose bit in\n\t// SyncAggregate is unset, in committee order.\n\tNonSigners []NonSigner\n}\n\nfunc (u LightClientUpdate) validateBasic() error {\n\tif (u.NextSyncCommittee == nil) != (len(u.NextSyncCommitteeBranch) == 0) {\n\t\treturn errors.New(\"next sync committee and its branch must be set together\")\n\t}\n\tif u.NextSyncCommittee != nil {\n\t\tif err := validateSummary(*u.NextSyncCommittee); err != nil {\n\t\t\treturn ufmt.Errorf(\"next sync committee: %v\", err)\n\t\t}\n\t}\n\tif err := validateBranch(u.NextSyncCommitteeBranch); err != nil {\n\t\treturn ufmt.Errorf(\"next sync committee branch: %v\", err)\n\t}\n\tif err := validateBranch(u.FinalityBranch); err != nil {\n\t\treturn ufmt.Errorf(\"finality branch: %v\", err)\n\t}\n\tfor i, ns := range u.NonSigners {\n\t\tif len(ns.Pubkey) != blsPubKeySize {\n\t\t\treturn ufmt.Errorf(\"non-signer %d: pubkey is not 48 bytes\", i)\n\t\t}\n\t\tif err := validateBranch(ns.Branch); err != nil {\n\t\t\treturn ufmt.Errorf(\"non-signer %d branch: %v\", i, err)\n\t\t}\n\t}\n\tif len(u.SyncAggregate.SyncCommitteeSignature) != blsSignatureSize {\n\t\treturn errors.New(\"sync committee signature must be 96 bytes\")\n\t}\n\treturn nil\n}\n\n// Header is the client message that updates the client from the trusted\n// consensus state at TrustedSlot. The update is signed by that state's\n// current sync committee, or by its next one if ActiveIsNext.\ntype Header struct {\n\tActiveIsNext    bool\n\tConsensusUpdate LightClientUpdate\n\tTrustedSlot     uint64\n}\n\nfunc (*Header) ClientType() string { return ClientType }\n\nfunc (h *Header) ValidateBasic() error {\n\treturn h.ConsensusUpdate.validateBasic()\n}\n\n// Misbehaviour is two valid updates attesting different execution state\n// roots at the same slot.\ntype Misbehaviour struct {\n\tActiveIsNext bool\n\tUpdate1      LightClientUpdate\n\tUpdate2      LightClientUpdate\n\tTrustedSlot  uint64\n}\n\nfunc (*Misbehaviour) ClientType() string { return ClientType }\n\nfunc (m *Misbehaviour) ValidateBasic() error {\n\tif err := m.Update1.validateBasic(); err != nil {\n\t\treturn err\n\t}\n\treturn m.Update2.validateBasic()\n}\n\n// MembershipProof proves a storage slot of the ICS26 router: AccountProof is\n// eth_getProof's accountProof for the router and StorageProof the proof of\n// the slot in its storage trie.\ntype MembershipProof struct {\n\tAccountProof [][]byte\n\tStorageProof [][]byte\n}\n"},{"name":"wire.gno","body":"package ethereum\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/aib/encoding/wire/v0\"\n)\n\n// Binary encodings of the client messages and proofs, in the positional wire\n// format of gno.land/p/aib/encoding/wire: relayers pass them as []byte MsgCall\n// arguments, which the VM base64-decodes natively, so a message costs neither\n// MsgRun preprocessing nor interpreted parsing beyond slicing its fields.\n//\n// Schema (field order is the format; \"list\" is a Uvarint count followed by\n// the elements, \"blob\" a Uvarint length followed by the bytes):\n//\n//\tHeader:          Uint trusted_slot, Bool active_is_next, Update\n//\tMisbehaviour:    Uint trusted_slot, Bool active_is_next, Update, Update\n//\tUpdate:          LCHeader attested, Bool has_next,\n//\t                 [has_next: blob pubkeys_hash, blob aggregate_pubkey, list\u003cblob\u003e next_branch],\n//\t                 LCHeader finalized, list\u003cblob\u003e finality_branch,\n//\t                 blob bits, blob signature, Uint signature_slot,\n//\t                 blob finalized_execution_header,\n//\t                 list\u003cNonSigner\u003e\n//\tNonSigner:       blob pubkey, list\u003cblob\u003e branch\n//\tLCHeader:        Uint slot, Uint proposer_index, blob parent_root, blob state_root,\n//\t                 blob body_root, Byte kind (0 payload header, 1 block hash),\n//\t                 [kind 0: ExecutionPayloadHeader] [kind 1: blob block_hash],\n//\t                 list\u003cblob\u003e execution_branch\n//\tExecutionPayloadHeader: blob parent_hash, blob fee_recipient, blob state_root,\n//\t                 blob receipts_root, blob logs_bloom, blob prev_randao,\n//\t                 Uint block_number, Uint gas_limit, Uint gas_used, Uint timestamp,\n//\t                 blob extra_data, blob base_fee_per_gas (big-endian),\n//\t                 blob block_hash, blob transactions_root, blob withdrawals_root,\n//\t                 Uint blob_gas_used, Uint excess_blob_gas\n//\tMembershipProof: list\u003cblob\u003e account_proof, list\u003cblob\u003e storage_proof\n\nvar (\n\terrTrailing   = errors.New(\"wire: trailing bytes\")\n\terrListLength = errors.New(\"wire: list longer than the remaining bytes\")\n\terrHeaderKind = errors.New(\"wire: unknown light-client header kind\")\n)\n\n// decoder is a wire.Reader that also records errors of its own, such as a\n// list count the remaining bytes cannot hold.\ntype decoder struct {\n\t*wire.Reader\n\terr error\n}\n\nfunc newDecoder(bz []byte) *decoder { return \u0026decoder{Reader: wire.NewReader(bz)} }\n\nfunc (d *decoder) ok() bool { return d.err == nil \u0026\u0026 d.Err() == nil }\n\n// done returns the first decoding error, or rejects trailing bytes.\nfunc (d *decoder) done() error {\n\tif d.err != nil {\n\t\treturn d.err\n\t}\n\tif err := d.Err(); err != nil {\n\t\treturn err\n\t}\n\tif !d.Done() {\n\t\treturn errTrailing\n\t}\n\treturn nil\n}\n\n// DecodeHeader decodes a Header.\nfunc DecodeHeader(bz []byte) (*Header, error) {\n\td := newDecoder(bz)\n\th := \u0026Header{TrustedSlot: d.Uint(), ActiveIsNext: d.Bool()}\n\th.ConsensusUpdate = d.update()\n\treturn h, d.done()\n}\n\n// DecodeMisbehaviour decodes a Misbehaviour.\nfunc DecodeMisbehaviour(bz []byte) (*Misbehaviour, error) {\n\td := newDecoder(bz)\n\tm := \u0026Misbehaviour{TrustedSlot: d.Uint(), ActiveIsNext: d.Bool()}\n\tm.Update1 = d.update()\n\tm.Update2 = d.update()\n\treturn m, d.done()\n}\n\n// DecodeMembershipProof decodes a MembershipProof.\nfunc DecodeMembershipProof(bz []byte) (MembershipProof, error) {\n\td := newDecoder(bz)\n\tp := MembershipProof{AccountProof: d.blobs(), StorageProof: d.blobs()}\n\treturn p, d.done()\n}\n\n// count reads a list length, rejecting one the remaining bytes cannot hold\n// (every element takes at least one byte) before anything is allocated.\nfunc (d *decoder) count() int {\n\tn := d.Uvarint()\n\tif !d.ok() {\n\t\treturn 0\n\t}\n\tif n \u003e uint64(d.Remaining()) {\n\t\td.err = errListLength\n\t\treturn 0\n\t}\n\treturn int(n)\n}\n\nfunc (d *decoder) blobs() [][]byte {\n\tn := d.count()\n\tout := make([][]byte, n)\n\tfor i := 0; i \u003c n \u0026\u0026 d.ok(); i++ {\n\t\tout[i] = d.Blob()\n\t}\n\treturn out\n}\n\nfunc (d *decoder) update() LightClientUpdate {\n\tvar u LightClientUpdate\n\tu.AttestedHeader = d.lcHeader()\n\tif d.Bool() {\n\t\tu.NextSyncCommittee = \u0026SummarizedSyncCommittee{PubkeysHash: d.Blob(), AggregatePubkey: d.Blob()}\n\t\tu.NextSyncCommitteeBranch = d.blobs()\n\t}\n\tu.FinalizedHeader = d.lcHeader()\n\tu.FinalityBranch = d.blobs()\n\tu.SyncAggregate = SyncAggregate{SyncCommitteeBits: d.Blob(), SyncCommitteeSignature: d.Blob()}\n\tu.SignatureSlot = d.Uint()\n\tu.FinalizedExecutionHeader = d.Blob()\n\tn := d.count()\n\tu.NonSigners = make([]NonSigner, n)\n\tfor i := 0; i \u003c n \u0026\u0026 d.ok(); i++ {\n\t\tu.NonSigners[i] = NonSigner{Pubkey: d.Blob(), Branch: d.blobs()}\n\t}\n\treturn u\n}\n\nfunc (d *decoder) lcHeader() LightClientHeader {\n\tvar h LightClientHeader\n\th.Beacon = BeaconBlockHeader{\n\t\tSlot: d.Uint(), ProposerIndex: d.Uint(),\n\t\tParentRoot: d.Blob(), StateRoot: d.Blob(), BodyRoot: d.Blob(),\n\t}\n\tswitch kind := d.Byte(); {\n\tcase !d.ok():\n\tcase kind == 0:\n\t\th.Execution = ExecutionPayloadHeader{\n\t\t\tParentHash: d.Blob(), FeeRecipient: d.Blob(), StateRoot: d.Blob(), ReceiptsRoot: d.Blob(),\n\t\t\tLogsBloom: d.Blob(), PrevRandao: d.Blob(),\n\t\t\tBlockNumber: d.Uint(), GasLimit: d.Uint(), GasUsed: d.Uint(), Timestamp: d.Uint(),\n\t\t\tExtraData: d.Blob(), BaseFeePerGas: d.Blob(), BlockHash: d.Blob(),\n\t\t\tTransactionsRoot: d.Blob(), WithdrawalsRoot: d.Blob(),\n\t\t\tBlobGasUsed: d.Uint(), ExcessBlobGas: d.Uint(),\n\t\t}\n\tcase kind == 1:\n\t\th.ExecutionBlockHash = d.Blob()\n\tdefault:\n\t\td.err = errHeaderKind\n\t}\n\th.ExecutionBranch = d.blobs()\n\treturn h\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"BD2r2vpON4N63hYvtuYpjYHRfkeIpbYcU2HxeM5HaAoP7VZcHXDnA/UkLxOG7p+Fid+YT6rjwpqlY9Rzx+2LIA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"merkle","path":"gno.land/p/aib/merkle/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/aib/merkle/v0\"\ngno = \"0.9\"\n"},{"name":"merkle.gno","body":"package merkle\n\nimport (\n\t\"crypto/sha256\"\n)\n\n// HashFromByteSlices computes a Merkle tree where the leaves are the byte\n// slice, in the provided order. It follows RFC-6962.\nfunc HashFromByteSlices(items [][]byte) []byte {\n\tswitch len(items) {\n\tcase 0:\n\t\treturn emptyHash()\n\tcase 1:\n\t\treturn leafHash(items[0])\n\tdefault:\n\t\t// Find the split point (largest power of 2 \u003c= len(items)).\n\t\tlength := len(items)\n\t\tvar k int\n\t\tfor k = 1; k \u003c length; k \u003c\u003c= 1 {\n\t\t}\n\t\tk \u003e\u003e= 1\n\n\t\tleft := HashFromByteSlices(items[:k])\n\t\tright := HashFromByteSlices(items[k:])\n\t\treturn innerHash(left, right)\n\t}\n}\n\nvar (\n\tleafPrefix  = []byte{0x00}\n\tinnerPrefix = []byte{0x01}\n)\n\n// emptyHash returns sha256(\u003cempty\u003e)\nfunc emptyHash() []byte {\n\tbz := sha256.Sum256([]byte{})\n\treturn bz[:]\n}\n\n// leafHash returns sha256(0x00 || leaf)\nfunc leafHash(leaf []byte) []byte {\n\tbz := sha256.Sum256(append(leafPrefix, leaf...))\n\treturn bz[:]\n}\n\n// innerHash returns sha256(0x01 || left || right)\nfunc innerHash(left, right []byte) []byte {\n\tbz := sha256.Sum256(append(innerPrefix, append(left, right...)...))\n\treturn bz[:]\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"Kr8KPQtz/j+fYHdk1MvZaccXFkS5n6G0jDxeI2rRIIZJsHimvlaK/wnnOeXquGNGvXn4NMPGRPfbreT5RiobHQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"tendermint","path":"gno.land/p/aib/ibc/lightclient/tendermint/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/aib/ibc/lightclient/tendermint/v0\"\ngno = \"0.9\"\n"},{"name":"header.gno","body":"package tendermint\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"time\"\n\n\t\"gno.land/p/aib/encoding/proto/v0\"\n\t\"gno.land/p/aib/ibc/lightclient/v0\"\n\t\"gno.land/p/aib/ibc/types/v0\"\n\t\"gno.land/p/aib/merkle/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// MsgHeader defines the Tendermint client consensus Header.\n// It encapsulates all the information necessary to update from a trusted\n// Tendermint ConsensusState. The inclusion of TrustedHeight and\n// TrustedValidators allows this update to process correctly, so long as the\n// ConsensusState for the TrustedHeight exists, this removes race conditions\n// among relayers The SignedHeader and ValidatorSet are the new untrusted update\n// fields for the client. The TrustedHeight is the height of a stored\n// ConsensusState on the client that will be used to verify the new untrusted\n// header. The Trusted ConsensusState must be within the unbonding period of\n// current time in order to correctly verify, and the TrustedValidators must\n// hash to TrustedConsensusState.NextValidatorsHash since that is the last\n// trusted validator set at the TrustedHeight.\ntype MsgHeader struct {\n\tHeader            *Header\n\tCommit            *Commit\n\tValidatorSet      *ValidatorSet\n\tTrustedHeight     types.Height\n\tTrustedValidators *ValidatorSet\n}\n\n// Implements lightclient.ClientMessage\nfunc (*MsgHeader) ClientType() string {\n\treturn lightclient.Tendermint\n}\n\n// Implements lightclient.ClientMessage\n// NOTE: TrustedHeight and TrustedValidators may be empty when creating client\n// with MsgCreateClient\nfunc (h *MsgHeader) ValidateBasic() error {\n\tif h.Header == nil {\n\t\treturn errors.New(\"missing header\")\n\t}\n\tif h.Commit == nil {\n\t\treturn errors.New(\"missing commit\")\n\t}\n\tif err := h.Header.ValidateBasic(); err != nil {\n\t\treturn ufmt.Errorf(\"invalid header: %v\", err)\n\t}\n\tif err := h.Commit.ValidateBasic(); err != nil {\n\t\treturn ufmt.Errorf(\"invalid commit: %v\", err)\n\t}\n\t// Make sure the header is consistent with the commit.\n\tif h.Header.Height != h.Commit.Height {\n\t\treturn ufmt.Errorf(\"header and commit height mismatch: %d vs %d\", h.Header.Height, h.Commit.Height)\n\t}\n\tif hhash, chash := h.Header.Hash(), h.Commit.BlockID.Hash; !bytes.Equal(hhash, chash) {\n\t\tcs, hs := hex.EncodeToString(chash), hex.EncodeToString(hhash)\n\t\treturn ufmt.Errorf(\"expected BlockID#Hash and Header#Hash to be the same, got %q != %q\", cs, hs)\n\t}\n\n\t// TrustedHeight is less than Header for updates and misbehaviour\n\tif h.TrustedHeight.GTE(h.GetHeight()) {\n\t\treturn ufmt.Errorf(\"trustedHeight %s must be less than header height %s\",\n\t\t\th.TrustedHeight, h.GetHeight())\n\t}\n\n\tif h.ValidatorSet == nil {\n\t\treturn ufmt.Errorf(\"validator set is nil\")\n\t}\n\tif h.TrustedValidators == nil {\n\t\treturn ufmt.Errorf(\"trusted validator set is nil\")\n\t}\n\tif !bytes.Equal(h.Header.ValidatorsHash, h.ValidatorSet.Hash()) {\n\t\treturn ufmt.Errorf(\"validator set does not match hash\")\n\t}\n\tif len(h.ValidatorSet.Validators) != len(h.Commit.Signatures) {\n\t\treturn ufmt.Errorf(\"Invalid commit -- wrong set size: %v vs %v\",\n\t\t\tlen(h.ValidatorSet.Validators), len(h.Commit.Signatures))\n\t}\n\treturn nil\n}\n\n// ConsensusState returns the updated consensus state associated with the\n// header\nfunc (h MsgHeader) ConsensusState() ConsensusState {\n\treturn ConsensusState{\n\t\tTimestamp:          h.Header.Time,\n\t\tRoot:               NewMerkleRoot(h.Header.AppHash),\n\t\tNextValidatorsHash: h.Header.NextValidatorsHash,\n\t}\n}\n\n// GetHeight returns the current height. It returns 0 if the tendermint\n// header is nil.\n// NOTE: the header.Header is checked to be non nil in ValidateBasic.\nfunc (h MsgHeader) GetHeight() types.Height {\n\trevision := types.ParseChainID(h.Header.ChainID)\n\treturn types.NewHeight(revision, h.Header.Height)\n}\n\n// Header defines the structure of a block header.\ntype Header struct {\n\t// basic block info\n\tVersion Consensus\n\tChainID string\n\tHeight  uint64\n\tTime    time.Time\n\n\t// prev block info\n\tLastBlockID BlockID\n\n\t// hashes of block data\n\t// commit info from validators from the last block\n\tLastCommitHash []byte\n\t// transactions\n\tDataHash []byte\n\n\t// hashes from the app output form the preb block\n\t// validators for the current block\n\tValidatorsHash []byte\n\t// validators for the next block\n\tNextValidatorsHash []byte\n\t// consensus params for the current block\n\tConsensusHash []byte\n\t// state afterx txs from the previous block\n\tAppHash []byte\n\t// root hash of all results from the txs from the previous block\n\tLastResultsHash []byte\n\n\t// consensus info\n\t// evidence included in the block\n\tEvidenceHash []byte\n\t//original proposer of the block\n\tProposerAddress []byte\n}\n\nconst (\n\t// BlockProtocol versions all block data structures and processing.\n\t// This includes validity of blocks and state updates.\n\tBlockProtocol uint64 = 11\n\tAddressSize          = 20\n\t// MaxSignatureSize is a maximum allowed signature size for the Proposal\n\t// and Vote.\n\t// XXX: secp256k1 does not have Size nor MaxSize defined.\n\tMaxSignatureSize = 64\n)\n\n// ValidateBasic performs stateless validation on a Header returning an error if any\n// validation fails.\n//\n// NOTE: Timestamp validation is subtle and handled elsewhere.\nfunc (h Header) ValidateBasic() error {\n\tif h.Version.Block != BlockProtocol {\n\t\treturn ufmt.Errorf(\"block protocol is incorrect: got: %d, want: %d \", h.Version.Block, BlockProtocol)\n\t}\n\tif len(h.ChainID) \u003e MaxChainIDLen {\n\t\treturn ufmt.Errorf(\"chainID is too long; got: %d, max: %d\", len(h.ChainID), MaxChainIDLen)\n\t}\n\n\tif h.Height == 0 {\n\t\treturn errors.New(\"zero Height\")\n\t}\n\n\tif err := h.LastBlockID.ValidateBasic(); err != nil {\n\t\treturn ufmt.Errorf(\"wrong LastBlockID: %v\", err)\n\t}\n\n\tif err := validateHash(h.LastCommitHash); err != nil {\n\t\treturn ufmt.Errorf(\"wrong LastCommitHash: %v\", err)\n\t}\n\n\tif err := validateHash(h.DataHash); err != nil {\n\t\treturn ufmt.Errorf(\"wrong DataHash: %v\", err)\n\t}\n\n\tif err := validateHash(h.EvidenceHash); err != nil {\n\t\treturn ufmt.Errorf(\"wrong EvidenceHash: %v\", err)\n\t}\n\n\tif len(h.ProposerAddress) != AddressSize {\n\t\treturn ufmt.Errorf(\n\t\t\t\"invalid ProposerAddress length; got: %d, expected: %d\",\n\t\t\tlen(h.ProposerAddress), AddressSize,\n\t\t)\n\t}\n\n\t// Basic validation of hashes related to application data.\n\t// Will validate fully against state in state#ValidateBlock.\n\tif err := validateHash(h.ValidatorsHash); err != nil {\n\t\treturn ufmt.Errorf(\"wrong ValidatorsHash: %v\", err)\n\t}\n\tif err := validateHash(h.NextValidatorsHash); err != nil {\n\t\treturn ufmt.Errorf(\"wrong NextValidatorsHash: %v\", err)\n\t}\n\tif err := validateHash(h.ConsensusHash); err != nil {\n\t\treturn ufmt.Errorf(\"wrong ConsensusHash: %v\", err)\n\t}\n\t// NOTE: AppHash is arbitrary length\n\tif err := validateHash(h.LastResultsHash); err != nil {\n\t\treturn ufmt.Errorf(\"wrong LastResultsHash: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (h *Header) Hash() []byte {\n\tif h == nil || len(h.ValidatorsHash) == 0 {\n\t\treturn nil\n\t}\n\tbz := merkle.HashFromByteSlices([][]byte{\n\t\th.Version.ProtoMarshal(),\n\t\tproto.AppendLengthDelimited(nil, 1, []byte(h.ChainID)),\n\t\tproto.AppendVarint(nil, 1, h.Height),\n\t\tproto.TimeMarshal(h.Time),\n\t\th.LastBlockID.ProtoMarshal(),\n\t\tproto.AppendLengthDelimited(nil, 1, h.LastCommitHash),\n\t\tproto.AppendLengthDelimited(nil, 1, h.DataHash),\n\t\tproto.AppendLengthDelimited(nil, 1, h.ValidatorsHash),\n\t\tproto.AppendLengthDelimited(nil, 1, h.NextValidatorsHash),\n\t\tproto.AppendLengthDelimited(nil, 1, h.ConsensusHash),\n\t\tproto.AppendLengthDelimited(nil, 1, h.AppHash),\n\t\tproto.AppendLengthDelimited(nil, 1, h.LastResultsHash),\n\t\tproto.AppendLengthDelimited(nil, 1, h.EvidenceHash),\n\t\tproto.AppendLengthDelimited(nil, 1, h.ProposerAddress),\n\t})\n\t// For debugging purposes:\n\t// hex := hex.EncodeToString\n\t// println(\"VERSION\", hex(h.Version.ProtoMarshal()))\n\t// println(\"CHAIN\", hex(proto.AppendLengthDelimited(nil, 1, []byte(h.ChainID))))\n\t// println(\"HEIGHT\", hex(proto.AppendVarint(nil, 1, h.Height)))\n\t// println(\"TIME\", hex(proto.TimeMarshal(h.Time)))\n\t// println(\"LAST\", hex(h.LastBlockID.ProtoMarshal()))\n\t// println(\"COMMITHASH\", hex(proto.AppendLengthDelimited(nil, 1, h.LastCommitHash)))\n\t// println(\"DATAHASH\", hex(proto.AppendLengthDelimited(nil, 1, h.DataHash)))\n\t// println(\"VALHASH\", hex(proto.AppendLengthDelimited(nil, 1, h.ValidatorsHash)))\n\t// println(\"NEXTVALHASH\", hex(proto.AppendLengthDelimited(nil, 1, h.NextValidatorsHash)))\n\t// println(\"CONSHASH\", hex(proto.AppendLengthDelimited(nil, 1, h.ConsensusHash)))\n\t// println(\"APPHASH\", hex(proto.AppendLengthDelimited(nil, 1, h.AppHash)))\n\t// println(\"RES\", hex(proto.AppendLengthDelimited(nil, 1, h.LastResultsHash)))\n\t// println(\"EV\", hex(proto.AppendLengthDelimited(nil, 1, h.EvidenceHash)))\n\t// println(\"PROP\", hex(proto.AppendLengthDelimited(nil, 1, h.ProposerAddress)))\n\t// println(\"MERKLE\", hex(bz))\n\treturn bz\n}\n\nfunc (h Header) Expired(trustingPeriod time.Duration) error {\n\tvar (\n\t\texpirationTime = h.Time.Add(trustingPeriod)\n\t\tnow            = time.Now()\n\t)\n\tif !expirationTime.After(now) {\n\t\treturn ufmt.Errorf(\"header %d has expired at %v (now: %v)\", h.Height, expirationTime, now)\n\t}\n\treturn nil\n}\n\n// Commit contains the evidence that a block was committed by a set of validators.\ntype Commit struct {\n\tHeight     uint64\n\tRound      int32\n\tBlockID    BlockID\n\tSignatures []CommitSig\n}\n\n// BytesToSign returns the bytes-to-sign for a validator's vote.\n// These bytes are produced from a protobuf marshaled type [CanonicalVote][1].\n// Since proto is not available in Gno, the function tries to generate these\n// bytes manually, using the same logic as protobuf.\n//\n// [1]: https://buf.build/tendermint/tendermint/docs/main:tendermint.types#tendermint.types.CanonicalVote\nfunc (c *Commit) BytesToSign(chainID string, valIdx int) []byte {\n\tvar buf []byte\n\n\t// Field 1: type (SignedMsgType - varint)\n\tbuf = proto.AppendVarint(buf, 1, 2) // type precommit\n\n\t// Field 2: height (sfixed64 - fixed64)\n\tbuf = proto.AppendFixed64(buf, 2, uint64(c.Height))\n\n\t// Field 3: round (sfixed64 - fixed64)\n\tbuf = proto.AppendFixed64(buf, 3, uint64(c.Round))\n\n\t// Field 4: block_id (CanonicalBlockID - length-delimited)\n\tif c.Signatures[valIdx].BlockIDFlag == BlockIDFlagCommit {\n\t\tbuf = proto.AppendLengthDelimited(buf, 4, c.BlockID.ProtoMarshal())\n\t}\n\n\t// Field 5: timestamp (google.protobuf.Timestamp - length-delimited)\n\tbuf = proto.AppendTime(buf, 5, c.Signatures[valIdx].Timestamp)\n\n\t// Field 6: chain_id (string - length-delimited)\n\tbuf = proto.AppendLengthDelimited(buf, 6, []byte(chainID))\n\n\t// Prefix with total length\n\tlength := len(buf)\n\tbuf = append(binary.AppendUvarint(nil, uint64(length)), buf...)\n\n\treturn buf\n}\n\n// ValidateBasic performs basic validation that doesn't involve state data.\n// Does not actually check the cryptographic signatures.\nfunc (c *Commit) ValidateBasic() error {\n\tif c.Round \u003c 0 {\n\t\treturn errors.New(\"negative Round\")\n\t}\n\n\tif c.Height \u003e= 1 {\n\t\tif c.BlockID.IsZero() {\n\t\t\treturn errors.New(\"commit cannot be for nil block\")\n\t\t}\n\n\t\tif len(c.Signatures) == 0 {\n\t\t\treturn errors.New(\"no signatures in commit\")\n\t\t}\n\t\tfor i, commitSig := range c.Signatures {\n\t\t\tif err := commitSig.ValidateBasic(); err != nil {\n\t\t\t\treturn ufmt.Errorf(\"wrong CommitSig #%d: %v\", i, err)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// CommitSig is a part of the Vote included in a Commit.\ntype CommitSig struct {\n\tBlockIDFlag      BlockIDFlag\n\tValidatorAddress []byte\n\tTimestamp        time.Time\n\tSignature        []byte\n}\n\n// ValidateBasic performs basic validation.\nfunc (cs CommitSig) ValidateBasic() error {\n\tswitch cs.BlockIDFlag {\n\tcase BlockIDFlagAbsent:\n\tcase BlockIDFlagCommit:\n\tcase BlockIDFlagNil:\n\tdefault:\n\t\treturn ufmt.Errorf(\"unknown BlockIDFlag: %v\", cs.BlockIDFlag)\n\t}\n\n\tswitch cs.BlockIDFlag {\n\tcase BlockIDFlagAbsent:\n\t\tif len(cs.ValidatorAddress) != 0 {\n\t\t\treturn errors.New(\"validator address is present\")\n\t\t}\n\t\tif !cs.Timestamp.IsZero() {\n\t\t\treturn errors.New(\"time is present\")\n\t\t}\n\t\tif len(cs.Signature) != 0 {\n\t\t\treturn errors.New(\"signature is present\")\n\t\t}\n\tdefault:\n\t\tif len(cs.ValidatorAddress) != AddressSize {\n\t\t\treturn ufmt.Errorf(\"expected ValidatorAddress size to be %d bytes, got %d bytes\",\n\t\t\t\tAddressSize,\n\t\t\t\tlen(cs.ValidatorAddress),\n\t\t\t)\n\t\t}\n\t\t// NOTE: Timestamp validation is subtle and handled elsewhere.\n\t\tif len(cs.Signature) == 0 {\n\t\t\treturn errors.New(\"signature is missing\")\n\t\t}\n\t\tif len(cs.Signature) \u003e MaxSignatureSize {\n\t\t\treturn ufmt.Errorf(\"signature is too big (max: %d)\", MaxSignatureSize)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// BlockIdFlag indicates which BlockID the signature is for\ntype BlockIDFlag byte\n\nconst (\n\tBlockIDFlagUnknown BlockIDFlag = 0\n\tBlockIDFlagAbsent  BlockIDFlag = 1\n\tBlockIDFlagCommit  BlockIDFlag = 2\n\tBlockIDFlagNil     BlockIDFlag = 3\n)\n\n// BlockID\ntype BlockID struct {\n\tHash          []byte\n\tPartSetHeader PartSetHeader\n}\n\n// Equals returns true if the BlockID matches the given BlockID\nfunc (blockID BlockID) Equals(other BlockID) bool {\n\treturn bytes.Equal(blockID.Hash, other.Hash) \u0026\u0026\n\t\tblockID.PartSetHeader.Equals(other.PartSetHeader)\n}\n\n// IsZero returns true if this is the BlockID of a nil block.\nfunc (blockID BlockID) IsZero() bool {\n\treturn len(blockID.Hash) == 0 \u0026\u0026\n\t\tblockID.PartSetHeader.IsZero()\n}\n\n// ValidateBasic performs basic validation.\nfunc (blockID BlockID) ValidateBasic() error {\n\t// Hash can be empty in case of POLBlockID in Proposal.\n\tif err := validateHash(blockID.Hash); err != nil {\n\t\treturn ufmt.Errorf(\"wrong blockID Hash: %v\", err)\n\t}\n\tif err := blockID.PartSetHeader.ValidateBasic(); err != nil {\n\t\treturn ufmt.Errorf(\"wrong PartSetHeader: %v\", err)\n\t}\n\treturn nil\n}\n\nfunc (b BlockID) ProtoMarshal() (bz []byte) {\n\tif len(b.Hash) \u003e 0 {\n\t\tbz = proto.AppendLengthDelimited(bz, 1, b.Hash)\n\t}\n\tbz = proto.AppendLengthDelimited(bz, 2, b.PartSetHeader.ProtoMarshal())\n\treturn\n}\n\n// PartsetHeader\ntype PartSetHeader struct {\n\tTotal uint32\n\tHash  []byte\n}\n\nfunc (psh PartSetHeader) IsZero() bool {\n\treturn psh.Total == 0 \u0026\u0026 len(psh.Hash) == 0\n}\n\nfunc (psh PartSetHeader) Equals(other PartSetHeader) bool {\n\treturn psh.Total == other.Total \u0026\u0026 bytes.Equal(psh.Hash, other.Hash)\n}\n\n// ValidateBasic performs basic validation.\nfunc (psh PartSetHeader) ValidateBasic() error {\n\t// Hash can be empty in case of POLBlockID.PartSetHeader in Proposal.\n\tif err := validateHash(psh.Hash); err != nil {\n\t\treturn ufmt.Errorf(\"wrong parSetHeader Hash: %v\", err)\n\t}\n\treturn nil\n}\n\nfunc (psh PartSetHeader) ProtoMarshal() (bz []byte) {\n\tif psh.Total \u003e 0 {\n\t\tbz = proto.AppendVarint(bz, 1, uint64(psh.Total))\n\t}\n\tif len(psh.Hash) \u003e 0 {\n\t\tbz = proto.AppendLengthDelimited(bz, 2, psh.Hash)\n\t}\n\treturn\n}\n\n// Consensus captures the consensus rules for processing a block in the blockchain,\n// including all blockchain data structures and the rules of the application's\n// state transition machine.\ntype Consensus struct {\n\tBlock uint64\n\tApp   uint64\n}\n\nfunc (c Consensus) ProtoMarshal() (bz []byte) {\n\tif c.Block != 0 {\n\t\tbz = proto.AppendVarint(bz, 1, c.Block)\n\t}\n\tif c.App != 0 {\n\t\tbz = proto.AppendVarint(bz, 2, c.App)\n\t}\n\treturn\n}\n"},{"name":"misbehaviour.gno","body":"package tendermint\n\nimport (\n\t\"gno.land/p/aib/ibc/lightclient/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Misbehaviour is a wrapper over two conflicting Headers\n// that implements Misbehaviour interface expected by ICS-02\ntype Misbehaviour struct {\n\tHeader1 *MsgHeader\n\tHeader2 *MsgHeader\n}\n\n// Implements lightclient.ClientMessage\nfunc (Misbehaviour) ClientType() string {\n\treturn lightclient.Tendermint\n}\n\n// Implements lightclient.ClientMessage\nfunc (m Misbehaviour) ValidateBasic() error {\n\tif m.Header1 == nil {\n\t\treturn ufmt.Errorf(\"misbehaviour Header1 cannot be nil\")\n\t}\n\tif m.Header2 == nil {\n\t\treturn ufmt.Errorf(\"misbehaviour Header2 cannot be nil\")\n\t}\n\tif m.Header1.TrustedHeight.RevisionHeight == 0 {\n\t\treturn ufmt.Errorf(\"misbehaviour Header1 cannot have zero revision height\")\n\t}\n\tif m.Header2.TrustedHeight.RevisionHeight == 0 {\n\t\treturn ufmt.Errorf(\"misbehaviour Header2 cannot have zero revision height\")\n\t}\n\tif m.Header1.TrustedValidators == nil {\n\t\treturn ufmt.Errorf(\"trusted validator set in Header1 cannot be empty\")\n\t}\n\tif m.Header2.TrustedValidators == nil {\n\t\treturn ufmt.Errorf(\"trusted validator set in Header2 cannot be empty\")\n\t}\n\tif m.Header1.Header.ChainID != m.Header2.Header.ChainID {\n\t\treturn ufmt.Errorf(\"headers must have identical chainIDs\")\n\t}\n\n\t// ValidateBasic on both validators\n\tif err := m.Header1.ValidateBasic(); err != nil {\n\t\treturn ufmt.Errorf(\"header 1 failed validation: %v\", err)\n\t}\n\tif err := m.Header2.ValidateBasic(); err != nil {\n\t\treturn ufmt.Errorf(\"header 2 failed validation: %v\", err)\n\t}\n\t// Ensure that Height1 is greater than or equal to Height2\n\tif m.Header1.GetHeight().LT(m.Header2.GetHeight()) {\n\t\treturn ufmt.Errorf(\"Header1 height is less than Header2 height (%s \u003c %s)\", m.Header1.GetHeight(), m.Header2.GetHeight())\n\t}\n\n\tif err := validateCommit(m.Header1); err != nil {\n\t\treturn ufmt.Errorf(\"invalid header1 commit: %v\", err)\n\t}\n\tif err := validateCommit(m.Header2); err != nil {\n\t\treturn ufmt.Errorf(\"invalid header2 commit: %v\", err)\n\t}\n\treturn nil\n}\n\n// validateCommit checks if the given commit is a valid commit from the\n// passed-in MsgHeader.\nfunc validateCommit(msg *MsgHeader) error {\n\tvotingPowerNeeded, err := votingPowerThreshold(msg.ValidatorSet, 2, 3)\n\tif err != nil {\n\t\treturn ufmt.Errorf(\"validator set: %v\", err)\n\t}\n\tif err := verifyCommit(msg.Header.ChainID, msg.ValidatorSet, msg.Commit, votingPowerNeeded, true); err != nil {\n\t\treturn ufmt.Errorf(\"validator set did not commit to header: %v\", err)\n\t}\n\treturn nil\n}\n"},{"name":"state.gno","body":"package tendermint\n\nimport (\n\t\"bytes\"\n\t\"crypto/sha256\"\n\t\"errors\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/aib/encoding/proto/v0\"\n\t\"gno.land/p/aib/ibc/lightclient/v0\"\n\t\"gno.land/p/aib/ibc/types/v0\"\n\t\"gno.land/p/aib/ics23/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// ClientState from Tendermint tracks the current validator set, latest height,\n// and a possible frozen height.\ntype ClientState struct {\n\tChainID    string\n\tTrustLevel Fraction\n\t// duration of the period since the LatestTimestamp during which the\n\t// submitted headers are valid for upgrade\n\tTrustingPeriod time.Duration\n\t// duration of the staking unbonding period\n\tUnbondingPeriod time.Duration\n\t// defines how much new (untrusted) header's Time can drift into the future.\n\tMaxClockDrift time.Duration\n\t// Block height when the client was frozen due to a misbehaviour\n\tFrozenHeight types.Height\n\t// Latest height the client was updated to\n\tLatestHeight types.Height\n\t// Proof specifications used in verifying counterparty state\n\tProofSpecs []*ics23.ProofSpec\n\n\t// Path at which next upgraded client will be committed.\n\t// Each element corresponds to the key for a single CommitmentProof in the\n\t// chained proof. NOTE: ClientState must stored under\n\t// `{upgradePath}/{upgradeHeight}/clientState` ConsensusState must be stored\n\t// under `{upgradepath}/{upgradeHeight}/consensusState` For SDK chains using\n\t// the default upgrade module, upgrade_path should be []string{\"upgrade\",\n\t// \"upgradedIBCState\"}`\n\tUpgradePath []string\n}\n\nfunc NewClientState(chainID string, trustLevel Fraction,\n\ttrustingPeriod, ubdPeriod, maxClockDrift time.Duration,\n\tlatestHeight types.Height, specs []*ics23.ProofSpec, upgradePath []string) *ClientState {\n\treturn \u0026ClientState{\n\t\tChainID:         chainID,\n\t\tTrustLevel:      trustLevel,\n\t\tTrustingPeriod:  trustingPeriod,\n\t\tUnbondingPeriod: ubdPeriod,\n\t\tMaxClockDrift:   maxClockDrift,\n\t\tLatestHeight:    latestHeight,\n\t\tFrozenHeight:    types.ZeroHeight(),\n\t\tProofSpecs:      specs,\n\t\tUpgradePath:     upgradePath,\n\t}\n}\n\ntype Fraction struct {\n\tNumerator   uint64\n\tDenominator uint64\n}\n\nfunc NewFraction(numerator, denominator uint64) Fraction {\n\treturn Fraction{\n\t\tNumerator:   numerator,\n\t\tDenominator: denominator,\n\t}\n}\n\n// DefaultTrustLevel is the tendermint light client default trust level\nvar DefaultTrustLevel = NewFraction(1, 3)\n\n// ConsensusState defines the consensus state from Tendermint.\ntype ConsensusState struct {\n\t// timestamp that corresponds to the block height in which the ConsensusState\n\t// was stored.\n\tTimestamp time.Time\n\t// commitment root (i.e app hash)\n\tRoot               MerkleRoot\n\tNextValidatorsHash []byte\n\n\tprocessedTime   time.Time\n\tprocessedHeight types.Height\n}\n\nfunc (cs ConsensusState) Equal(other ConsensusState) bool {\n\treturn cs.Timestamp.Equal(other.Timestamp) \u0026\u0026\n\t\tbytes.Equal(cs.Root.Hash, other.Root.Hash) \u0026\u0026\n\t\tbytes.Equal(cs.NextValidatorsHash, other.NextValidatorsHash)\n}\n\n// isSentinel reports whether cs is the placeholder VerifyUpgradeAndUpdateState\n// stores at the upgraded height. Its root is SentinelRoot, which no real\n// AppHash can equal, so it stands for \"no header seen at this height yet\":\n// the first genuine header at that height replaces it.\nfunc (cs ConsensusState) isSentinel() bool {\n\treturn bytes.Equal(cs.Root.Hash, []byte(SentinelRoot))\n}\n\ntype MerkleRoot struct {\n\tHash []byte\n}\n\n// NewMerkleRoot is a root over a copy of hash: a later write to hash does not\n// move it.\nfunc NewMerkleRoot(hash []byte) MerkleRoot {\n\treturn MerkleRoot{Hash: cloneBytes(hash)}\n}\n\nfunc cloneBytes(b []byte) []byte {\n\tif b == nil {\n\t\treturn nil\n\t}\n\treturn append([]byte{}, b...)\n}\n\n// cloneProofSpecs deep-copies proof specifications.\nfunc cloneProofSpecs(specs []*ics23.ProofSpec) []*ics23.ProofSpec {\n\tif specs == nil {\n\t\treturn nil\n\t}\n\tout := make([]*ics23.ProofSpec, len(specs))\n\tfor i, s := range specs {\n\t\tout[i] = s.Clone()\n\t}\n\treturn out\n}\n\nfunc cloneStrings(s []string) []string {\n\tif s == nil {\n\t\treturn nil\n\t}\n\treturn append([]string{}, s...)\n}\n\n// clone is a deep copy of cs: what a client keeps of a client state shares\n// no slice or pointer with what it was given.\nfunc (cs ClientState) clone() *ClientState {\n\tout := cs\n\tout.ProofSpecs = cloneProofSpecs(cs.ProofSpecs)\n\tout.UpgradePath = cloneStrings(cs.UpgradePath)\n\treturn \u0026out\n}\n\n// Empty returns true if the root is empty\nfunc (mr MerkleRoot) Empty() bool {\n\treturn len(mr.Hash) == 0\n}\n\nconst (\n\t// MaxChainIDLen is a maximum length of the chain ID.\n\tMaxChainIDLen = 50\n\t// SentinelRoot is used as a stand-in root value for the consensus state set at the upgrade height\n\tSentinelRoot = \"sentinel_root\"\n\t// ClientStateTypeURL is the proto Any type URL counterparties use when\n\t// storing the upgraded ClientState via cdc.MarshalInterface.\n\tClientStateTypeURL = \"/ibc.lightclients.tendermint.v1.ClientState\"\n\t// ConsensusStateTypeURL is the proto Any type URL counterparties use\n\t// when storing the upgraded ConsensusState via cdc.MarshalInterface.\n\tConsensusStateTypeURL = \"/ibc.lightclients.tendermint.v1.ConsensusState\"\n)\n\n// Implements lightclient.ClientState\nfunc (ClientState) ClientType() string {\n\treturn lightclient.Tendermint\n}\n\n// Implements lightclient.ClientState\nfunc (cs ClientState) ValidateBasic() error {\n\tif strings.TrimSpace(cs.ChainID) == \"\" {\n\t\treturn ufmt.Errorf(\"chainID cannot be empty string\")\n\t}\n\n\tif len(cs.ChainID) \u003e MaxChainIDLen {\n\t\treturn ufmt.Errorf(\"chainID is too long; got: %d, max: %d\", len(cs.ChainID), MaxChainIDLen)\n\t}\n\n\tif err := ValidateTrustLevel(cs.TrustLevel); err != nil {\n\t\treturn err\n\t}\n\tif cs.TrustingPeriod \u003c= 0 {\n\t\treturn errors.New(\"trusting period must be greater than zero\")\n\t}\n\tif cs.UnbondingPeriod \u003c= 0 {\n\t\treturn errors.New(\"unbonding period must be greater than zero\")\n\t}\n\tif cs.MaxClockDrift \u003c= 0 {\n\t\treturn errors.New(\"max clock drift must be greater than zero\")\n\t}\n\n\t// the latest height revision number must match the chain id revision number\n\tif cs.LatestHeight.RevisionNumber != types.ParseChainID(cs.ChainID) {\n\t\treturn ufmt.Errorf(\"latest height revision number must match chain id revision number (%d != %d)\", cs.LatestHeight.RevisionNumber, types.ParseChainID(cs.ChainID))\n\t}\n\tif cs.LatestHeight.RevisionHeight == 0 {\n\t\treturn ufmt.Errorf(\"tendermint client's latest height revision height cannot be zero\")\n\t}\n\tif cs.TrustingPeriod \u003e= cs.UnbondingPeriod {\n\t\treturn ufmt.Errorf(\"trusting period (%s) should be \u003c unbonding period (%s)\", cs.TrustingPeriod, cs.UnbondingPeriod)\n\t}\n\n\tif cs.ProofSpecs == nil {\n\t\treturn ufmt.Errorf(\"proof specs cannot be nil for tm client\")\n\t}\n\tfor i, spec := range cs.ProofSpecs {\n\t\tif spec == nil {\n\t\t\treturn ufmt.Errorf(\"proof spec cannot be nil at index: %d\", i)\n\t\t}\n\t}\n\t// UpgradePath may be empty, but if it isn't, each key must be non-empty\n\tfor i, k := range cs.UpgradePath {\n\t\tif strings.TrimSpace(k) == \"\" {\n\t\t\treturn ufmt.Errorf(\"key in upgrade path at index %d cannot be empty\", i)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// ValidateTrustLevel checks that trustLevel is within the allowed range [1/3,\n// 1]. If not, it returns an error. 1/3 is the minimum amount of trust needed\n// which does not break the security model.\nfunc ValidateTrustLevel(lvl Fraction) error {\n\tif lvl.Numerator*3 \u003c lvl.Denominator || // \u003c 1/3\n\t\tlvl.Numerator \u003e lvl.Denominator || // \u003e 1\n\t\tlvl.Denominator == 0 {\n\t\treturn ufmt.Errorf(\"trustLevel must be within [1/3, 1], given %v\", lvl)\n\t}\n\treturn nil\n}\n\n// Implements lightclient.ConsensusState\nfunc (ConsensusState) ClientType() string {\n\treturn lightclient.Tendermint\n}\n\n// Implements lightclient.ConsensusState\nfunc (cs ConsensusState) ValidateBasic() error {\n\tif cs.Root.Empty() {\n\t\treturn errors.New(\"root cannot be empty\")\n\t}\n\tif err := validateHash(cs.NextValidatorsHash); err != nil {\n\t\treturn ufmt.Errorf(\"next validators hash is invalid: %v\", err)\n\t}\n\tif cs.Timestamp.Unix() \u003c= 0 {\n\t\treturn errors.New(\"timestamp must be a positive Unix time\")\n\t}\n\treturn nil\n}\n\n// validateHash returns an error if the hash is not empty, but its\n// size != tmhash.Size.\nfunc validateHash(h []byte) error {\n\tif len(h) \u003e 0 \u0026\u0026 len(h) != sha256.Size {\n\t\treturn ufmt.Errorf(\"expected size to be %d bytes, got %d bytes\",\n\t\t\tsha256.Size,\n\t\t\tlen(h),\n\t\t)\n\t}\n\treturn nil\n}\n\nfunc (cs ClientState) ProtoMarshal() (bz []byte) {\n\tif cs.ChainID != \"\" {\n\t\tbz = proto.AppendLengthDelimited(bz, 1, []byte(cs.ChainID))\n\t}\n\t// Fields 2..7 are marked `(gogoproto.nullable) = false` in ibc-go's\n\t// .proto, so they're emitted even when zero (as length-0 messages).\n\tbz = proto.AppendAlwaysLengthDelimited(bz, 2, cs.TrustLevel.ProtoMarshal())\n\tbz = proto.AppendAlwaysLengthDelimited(bz, 3, proto.DurationMarshal(cs.TrustingPeriod))\n\tbz = proto.AppendAlwaysLengthDelimited(bz, 4, proto.DurationMarshal(cs.UnbondingPeriod))\n\tbz = proto.AppendAlwaysLengthDelimited(bz, 5, proto.DurationMarshal(cs.MaxClockDrift))\n\tbz = proto.AppendAlwaysLengthDelimited(bz, 6, cs.FrozenHeight.ProtoMarshal())\n\tbz = proto.AppendAlwaysLengthDelimited(bz, 7, cs.LatestHeight.ProtoMarshal())\n\tfor _, spec := range cs.ProofSpecs {\n\t\tbz = proto.AppendLengthDelimited(bz, 8, spec.ProtoMarshal())\n\t}\n\tfor _, path := range cs.UpgradePath {\n\t\tbz = proto.AppendLengthDelimited(bz, 9, []byte(path))\n\t}\n\treturn\n}\n\n// ZeroCustomFields returns a copy of the ClientState with all\n// client-customizable fields zeroed. Only the chain-specified fields\n// (ChainID, UnbondingPeriod, LatestHeight, ProofSpecs, UpgradePath) are\n// preserved. The counterparty chain commits to this zeroed form when\n// scheduling an upgrade, so verification on the upgrade proof must be\n// performed against the same shape.\nfunc (cs ClientState) ZeroCustomFields() ClientState {\n\treturn ClientState{\n\t\tChainID:         cs.ChainID,\n\t\tUnbondingPeriod: cs.UnbondingPeriod,\n\t\tLatestHeight:    cs.LatestHeight,\n\t\tProofSpecs:      cs.ProofSpecs,\n\t\tUpgradePath:     cs.UpgradePath,\n\t}\n}\n\nfunc (f Fraction) ProtoMarshal() (bz []byte) {\n\tbz = proto.AppendVarint(bz, 1, f.Numerator)\n\tbz = proto.AppendVarint(bz, 2, f.Denominator)\n\treturn\n}\n\nfunc (cs ConsensusState) ProtoMarshal() (bz []byte) {\n\t// Timestamp and Root are `(gogoproto.nullable) = false` in ibc-go;\n\t// always-emit semantics. NextValidatorsHash is plain bytes — proto3\n\t// default applies (omitted when empty).\n\tbz = proto.AppendAlwaysLengthDelimited(bz, 1, proto.TimeMarshal(cs.Timestamp))\n\tbz = proto.AppendAlwaysLengthDelimited(bz, 2, cs.Root.ProtoMarshal())\n\tbz = proto.AppendLengthDelimited(bz, 3, cs.NextValidatorsHash)\n\treturn\n}\n\nfunc (mr MerkleRoot) ProtoMarshal() (bz []byte) {\n\tbz = proto.AppendLengthDelimited(bz, 1, mr.Hash)\n\treturn\n}\n"},{"name":"tendermint.gno","body":"package tendermint\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"time\"\n\n\t\"gno.land/p/aib/encoding/proto/v0\"\n\t\"gno.land/p/aib/ibc/lightclient/v0\"\n\t\"gno.land/p/aib/ibc/types/v0\"\n\t\"gno.land/p/aib/ics23/v0\"\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\ntype TMLightClient struct {\n\tClientState            *ClientState\n\tConsensusStateByHeight *bptree.BPTree // height:*ConsensusState\n}\n\nfunc NewTMLightClient() *TMLightClient {\n\treturn \u0026TMLightClient{\n\t\tConsensusStateByHeight: bptree.NewBPTree32(),\n\t}\n}\n\n// NewLightClient is NewTMLightClient typed as the generic light-client\n// interface, so it can be handed as a constructor value where a\n// func() lightclient.Interface is expected (the IBC core store allocates\n// verifiers through such a constructor).\nfunc NewLightClient() lightclient.Interface {\n\treturn NewTMLightClient()\n}\n\nvar _ lightclient.Interface = (*TMLightClient)(nil)\n\n// Implements lightclient.Interface. It runs once, on a client not yet\n// initialized: a stored client's trusted state is never replaced through it.\nfunc (tm *TMLightClient) Initialize(clientState lightclient.ClientState, consensusState lightclient.ConsensusState) error {\n\tif tm.ClientState != nil {\n\t\treturn errors.New(\"tendermint client initialized already\")\n\t}\n\tcs := clientState.(ClientState)\n\t// Deep copies throughout: the caller keeps its own slices and may\n\t// write to them after the call; the trusted state must not follow.\n\ttm.ClientState = cs.clone()\n\tin := consensusState.(ConsensusState)\n\t// Construct a fresh ConsensusState in our own (caller's) realm context\n\t// rather than mutating the inbound value: the inbound value carries the\n\t// constructor realm's PkgID stamp, and borrow rule #2 makes writes to\n\t// foreign-stamped fields panic with \"readonly tainted object\".\n\tconsState := ConsensusState{\n\t\tTimestamp:          in.Timestamp,\n\t\tRoot:               NewMerkleRoot(in.Root.Hash),\n\t\tNextValidatorsHash: cloneBytes(in.NextValidatorsHash),\n\t\tprocessedTime:      time.Now(),\n\t\tprocessedHeight:    types.GetSelfHeight(),\n\t}\n\ttm.SetConsensusState(cs.LatestHeight, \u0026consState)\n\treturn nil\n}\n\n// Implements lightclient.Interface\nfunc (tm *TMLightClient) VerifyClientMessage(clientMsg lightclient.ClientMessage) error {\n\tswitch msg := clientMsg.(type) {\n\tcase *MsgHeader:\n\t\treturn tm.verifyHeader(msg)\n\tcase *Misbehaviour:\n\t\treturn tm.verifyMisbehavior(msg)\n\tdefault:\n\t\treturn errors.New(\"unknown client message type\")\n\t}\n}\n\n// Implements lightclient.Interface\nfunc (tm *TMLightClient) CheckForMisbehaviour(clientMsg lightclient.ClientMessage) bool {\n\tswitch msg := clientMsg.(type) {\n\tcase *MsgHeader:\n\t\tconsState := msg.ConsensusState()\n\t\t// Check if the Client store already has a consensus state for the header's\n\t\t// height.\n\t\t// If the consensus state exists, and it matches the header then we return\n\t\t// early since header has already been submitted in a previous\n\t\t// UpdateClient.\n\t\t// The placeholder stored at the upgraded height by\n\t\t// VerifyUpgradeAndUpdateState is not a header, so it is treated as\n\t\t// absent: the genuine header at that height must replace it, not be\n\t\t// read as conflicting with it (which would freeze the client).\n\t\tif existingConsState, found := tm.GetConsensusState(msg.GetHeight()); found \u0026\u0026 !existingConsState.isSentinel() {\n\t\t\t// This header has already been submitted and the necessary state is\n\t\t\t// already stored in client store, thus we can return early without\n\t\t\t// further validation.\n\t\t\tif existingConsState.Equal(consState) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\t// A consensus state already exists for this height, but it does not\n\t\t\t// match the provided header. The assumption is that Header has already\n\t\t\t// been validated. Thus we can return true as misbehaviour is present\n\t\t\treturn true\n\t\t}\n\n\t\t// Check that consensus state timestamps are monotonic\n\t\tprevCons, prevOk := tm.GetPreviousConsensusState(msg.GetHeight())\n\t\tnextCons, nextOk := tm.GetNextConsensusState(msg.GetHeight())\n\t\t// if previous consensus state exists, check consensus state time is\n\t\t// greater than previous consensus state time if previous consensus state\n\t\t// is not before current consensus state return true\n\t\tif prevOk \u0026\u0026 !prevCons.Timestamp.Before(consState.Timestamp) {\n\t\t\treturn true\n\t\t}\n\t\t// if next consensus state exists, check consensus state time is less than\n\t\t// next consensus state time if next consensus state is not after current\n\t\t// consensus state return true\n\t\tif nextOk \u0026\u0026 !nextCons.Timestamp.After(consState.Timestamp) {\n\t\t\treturn true\n\t\t}\n\n\tcase *Misbehaviour:\n\t\t// if heights are equal check that this is valid misbehaviour of a fork\n\t\t// otherwise if heights are unequal check that this is valid misbehavior of\n\t\t// BFT time violation.\n\t\tif msg.Header1.GetHeight().EQ(msg.Header2.GetHeight()) {\n\t\t\t// Ensure that Commit Hashes are different\n\t\t\tif !bytes.Equal(\n\t\t\t\tmsg.Header1.Commit.BlockID.Hash,\n\t\t\t\tmsg.Header2.Commit.BlockID.Hash,\n\t\t\t) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t} else if !msg.Header1.Header.Time.After(msg.Header2.Header.Time) {\n\t\t\t// Header1 is at greater height than Header2 (ensured by\n\t\t\t// Misbehaviour.ValidateBasic()), therefore Header1 time must be less\n\t\t\t// than or equal to Header2 time in order to be valid misbehaviour\n\t\t\t// (violation of monotonic time).\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// FrozenHeight is same for all misbehaviour\nvar FrozenHeight = types.NewHeight(0, 1)\n\n// Implements lightclient.Interface\nfunc (tm *TMLightClient) UpdateStateOnMisbehaviour(clientMsg lightclient.ClientMessage) {\n\t// Whole-slot replace instead of field-write: tm.ClientState was\n\t// constructed in the caller's realm (the relayer's MsgCreateClient\n\t// payload) so its fields are foreign-stamped and v2's borrow rule\n\t// rejects direct writes. tm.ClientState (the slot) lives on tm,\n\t// which is core-owned, so reassigning the pointer is fine.\n\ttm.ClientState = cloneClientStateWithFrozenHeight(tm.ClientState, FrozenHeight)\n}\n\n// Implements lightclient.Interface\nfunc (tm *TMLightClient) UpdateState(clientMsg lightclient.ClientMessage) []types.Height {\n\tmsg, ok := clientMsg.(*MsgHeader)\n\tif !ok {\n\t\t// clientMsg is an invalid Misbehaviour, no update necessary\n\t\treturn []types.Height{}\n\t}\n\n\t// check for duplicate update; the placeholder stored at the upgraded\n\t// height is overwritten by the first genuine header at that height (see\n\t// CheckForMisbehaviour).\n\tmsgHeight := msg.GetHeight()\n\tif existing, found := tm.GetConsensusState(msgHeight); found \u0026\u0026 !existing.isSentinel() {\n\t\t// perform no-op\n\t\treturn []types.Height{msgHeight}\n\t}\n\t// Amortized pruning, as ibc-go does: each update removes at most the\n\t// oldest consensus state if it expired, so the set converges to roughly\n\t// trustingPeriod / updateInterval entries and the storage deposit of the\n\t// removed bytes is refunded to the updater.\n\ttm.pruneOldestConsensusState(time.Now())\n\t// Update latestHeight if required. Whole-slot replace; see\n\t// UpdateStateOnMisbehaviour for the v2 borrow-rule rationale.\n\tif msgHeight.GT(tm.ClientState.LatestHeight) {\n\t\ttm.ClientState = cloneClientStateWithLatestHeight(tm.ClientState, msgHeight)\n\t}\n\t// Build and store new consensus state from clientMsg\n\tconsState := \u0026ConsensusState{\n\t\tTimestamp:          msg.Header.Time,\n\t\tRoot:               NewMerkleRoot(msg.Header.AppHash), // a copy\n\t\tNextValidatorsHash: cloneBytes(msg.Header.NextValidatorsHash),\n\t\tprocessedTime:      time.Now(),\n\t\tprocessedHeight:    types.GetSelfHeight(),\n\t}\n\ttm.SetConsensusState(msgHeight, consState)\n\treturn []types.Height{msgHeight}\n}\n\n// Implements lightclient.Interface\nfunc (tm *TMLightClient) VerifyMembership(height types.Height,\n\tproofs []ics23.CommitmentProof, path types.MerklePath,\n\tvalue []byte) error {\n\tif err := ics23.CheckProofs(proofs); err != nil {\n\t\treturn err\n\t}\n\tif tm.ClientState.LatestHeight.LT(height) {\n\t\treturn ufmt.Errorf(\n\t\t\t\"client state height \u003c proof height (%s \u003c %s), please ensure the client has been updated\", tm.ClientState.LatestHeight, height,\n\t\t)\n\t}\n\tif len(value) == 0 {\n\t\treturn ufmt.Errorf(\"empty value in membership proof\")\n\t}\n\tif len(tm.ClientState.ProofSpecs) != len(proofs) {\n\t\treturn ufmt.Errorf(\n\t\t\t\"length of specs: %d not equal to length of proof: %d\",\n\t\t\tlen(tm.ClientState.ProofSpecs), len(proofs),\n\t\t)\n\t}\n\tif len(path.KeyPath) != len(proofs) {\n\t\treturn ufmt.Errorf(\n\t\t\t\"path length %d not same as proof %d\", len(path.KeyPath), len(proofs),\n\t\t)\n\t}\n\tconsState, found := tm.GetConsensusState(height)\n\tif !found {\n\t\treturn ufmt.Errorf(\"please ensure the proof was constructed against a height that exists on the client\")\n\t}\n\treturn tm.verifyChainedMembershipProof(consState.Root.Hash, proofs, path, value, 0)\n}\n\n// Implements lightclient.Interface\nfunc (tm *TMLightClient) VerifyNonMembership(height types.Height,\n\tproofs []ics23.CommitmentProof, path types.MerklePath) error {\n\tif err := ics23.CheckProofs(proofs); err != nil {\n\t\treturn err\n\t}\n\tif tm.ClientState.LatestHeight.LT(height) {\n\t\treturn ufmt.Errorf(\n\t\t\t\"client state height \u003c proof height (%d \u003c %d), please ensure the client has been updated\", tm.ClientState.LatestHeight, height,\n\t\t)\n\t}\n\tif len(tm.ClientState.ProofSpecs) != len(proofs) {\n\t\treturn ufmt.Errorf(\n\t\t\t\"length of specs: %d not equal to length of proof: %d\",\n\t\t\tlen(tm.ClientState.ProofSpecs), len(proofs),\n\t\t)\n\t}\n\tif len(path.KeyPath) != len(proofs) {\n\t\treturn ufmt.Errorf(\n\t\t\t\"path length %d not same as proof %d\", len(path.KeyPath), len(proofs),\n\t\t)\n\t}\n\n\tconsState, found := tm.GetConsensusState(height)\n\tif !found {\n\t\treturn ufmt.Errorf(\"please ensure the proof was constructed against a height that exists on the client\")\n\t}\n\t// VerifyNonMembership will verify the absence of key in lowest subtree, and\n\t// then chain inclusion proofs of all subroots up to final root.\n\tnonexist := proofs[0].GetNonexist()\n\tif nonexist == nil {\n\t\treturn ufmt.Errorf(\"commitment proof must be non-existence proof for verifying non-membership\")\n\t}\n\tsubroot, err := nonexist.Calculate()\n\tif err != nil {\n\t\treturn ufmt.Errorf(\"could not calculate root for proof index 0, merkle tree is likely empty. %v\", err)\n\t}\n\n\tkey := path.KeyPath[len(path.KeyPath)-1]\n\tif err := nonexist.Verify(tm.ClientState.ProofSpecs[0], subroot, key); err != nil {\n\t\treturn ufmt.Errorf(\"failed to verify non-membership proof with key %s: %v\", string(key), err)\n\t}\n\n\t// Verify chained membership proof starting from index 1 with value = subroot\n\treturn tm.verifyChainedMembershipProof(consState.Root.Hash, proofs, path, subroot, 1)\n}\n\n// Implements lightclient.Interface\nfunc (tm *TMLightClient) Status() lightclient.Status {\n\tif !tm.ClientState.FrozenHeight.IsZero() {\n\t\treturn lightclient.Frozen\n\t}\n\t// get latest consensus state to check for expiry\n\tlastConsState, found := tm.GetConsensusState(tm.LatestHeight())\n\tif !found {\n\t\t// if the client state does not have an associated consensus state for its\n\t\t// latest height then it must be expired\n\t\treturn lightclient.Expired\n\t}\n\tif tm.IsExpired(lastConsState.Timestamp, time.Now()) {\n\t\treturn lightclient.Expired\n\t}\n\treturn lightclient.Active\n}\n\n// Implements lightclient.Interface\nfunc (tm *TMLightClient) LatestHeight() types.Height {\n\treturn tm.ClientState.LatestHeight\n}\n\n// Implements lightclient.Interface\nfunc (tm *TMLightClient) TimestampAtHeight(height types.Height) (uint64, error) {\n\tcs, found := tm.GetConsensusState(height)\n\tif !found {\n\t\treturn 0, ufmt.Errorf(\"no consensus state found for height %s\", height.String())\n\t}\n\treturn uint64(cs.Timestamp.Unix()), nil\n}\n\n// Implements lightclient.Interface.\n//\n// RecoverClient copies the substitute's consensus state at its latest height\n// into the subject (tm), updates the subject's ChainID, LatestHeight and\n// TrustingPeriod to match the substitute, and un-freezes the subject by\n// resetting FrozenHeight. The substitute and subject client states must match\n// in TrustLevel, UnbondingPeriod, MaxClockDrift, ProofSpecs and UpgradePath.\nfunc (tm *TMLightClient) RecoverClient(substitute lightclient.Interface) error {\n\tif err := tm.CheckRecovery(substitute); err != nil {\n\t\treturn err\n\t}\n\tsub := substitute.(*TMLightClient)\n\tsubLatest := sub.ClientState.LatestHeight\n\tsubConsState, _ := sub.GetConsensusState(subLatest)\n\n\t// Whole-slot replace, single allocation. See UpdateStateOnMisbehaviour\n\t// for the v2 borrow-rule rationale on why per-field writes don't work.\n\ttm.ClientState = \u0026ClientState{\n\t\tChainID:         sub.ClientState.ChainID,\n\t\tTrustLevel:      tm.ClientState.TrustLevel,\n\t\tTrustingPeriod:  sub.ClientState.TrustingPeriod,\n\t\tUnbondingPeriod: tm.ClientState.UnbondingPeriod,\n\t\tMaxClockDrift:   tm.ClientState.MaxClockDrift,\n\t\tFrozenHeight:    types.ZeroHeight(),\n\t\tLatestHeight:    subLatest,\n\t\tProofSpecs:      tm.ClientState.ProofSpecs,\n\t\tUpgradePath:     tm.ClientState.UpgradePath,\n\t}\n\t// A copy: the substitute keeps its own.\n\ttm.SetConsensusState(subLatest, \u0026ConsensusState{\n\t\tTimestamp:          subConsState.Timestamp,\n\t\tRoot:               NewMerkleRoot(subConsState.Root.Hash),\n\t\tNextValidatorsHash: cloneBytes(subConsState.NextValidatorsHash),\n\t\tprocessedTime:      subConsState.processedTime,\n\t\tprocessedHeight:    subConsState.processedHeight,\n\t})\n\treturn nil\n}\n\n// CheckRecovery is why RecoverClient would refuse substitute, or nil; it\n// changes nothing, so a governed recovery is refused deterministically\n// before it runs.\nfunc (tm *TMLightClient) CheckRecovery(substitute lightclient.Interface) error {\n\tif substitute == nil {\n\t\treturn errors.New(\"substitute must not be nil\")\n\t}\n\tsub, ok := substitute.(*TMLightClient)\n\tif !ok {\n\t\treturn ufmt.Errorf(\"substitute client type mismatch: expected *TMLightClient, got %T\", substitute)\n\t}\n\tif field := mismatchingClientStateField(tm.ClientState, sub.ClientState); field != \"\" {\n\t\treturn ufmt.Errorf(\"subject and substitute client states differ on %s\", field)\n\t}\n\tsubLatest := sub.ClientState.LatestHeight\n\tif _, found := sub.GetConsensusState(subLatest); !found {\n\t\treturn ufmt.Errorf(\"substitute consensus state not found at height %s\", subLatest)\n\t}\n\treturn nil\n}\n\n// cloneClientStateWithFrozenHeight returns a fresh *ClientState in the\n// caller's realm with FrozenHeight replaced. Used to whole-slot replace\n// tm.ClientState when a per-field write would violate borrow rules.\nfunc cloneClientStateWithFrozenHeight(cs *ClientState, h types.Height) *ClientState {\n\treturn \u0026ClientState{\n\t\tChainID:         cs.ChainID,\n\t\tTrustLevel:      cs.TrustLevel,\n\t\tTrustingPeriod:  cs.TrustingPeriod,\n\t\tUnbondingPeriod: cs.UnbondingPeriod,\n\t\tMaxClockDrift:   cs.MaxClockDrift,\n\t\tFrozenHeight:    h,\n\t\tLatestHeight:    cs.LatestHeight,\n\t\tProofSpecs:      cs.ProofSpecs,\n\t\tUpgradePath:     cs.UpgradePath,\n\t}\n}\n\n// cloneClientStateWithLatestHeight is the LatestHeight counterpart of\n// cloneClientStateWithFrozenHeight.\nfunc cloneClientStateWithLatestHeight(cs *ClientState, h types.Height) *ClientState {\n\treturn \u0026ClientState{\n\t\tChainID:         cs.ChainID,\n\t\tTrustLevel:      cs.TrustLevel,\n\t\tTrustingPeriod:  cs.TrustingPeriod,\n\t\tUnbondingPeriod: cs.UnbondingPeriod,\n\t\tMaxClockDrift:   cs.MaxClockDrift,\n\t\tFrozenHeight:    cs.FrozenHeight,\n\t\tLatestHeight:    h,\n\t\tProofSpecs:      cs.ProofSpecs,\n\t\tUpgradePath:     cs.UpgradePath,\n\t}\n}\n\n// mismatchingClientStateField returns the name of the first client-state field\n// that differs between subject and substitute, or an empty string if all\n// parameters that must not change during a client recovery match. ChainID,\n// LatestHeight, FrozenHeight and TrustingPeriod are allowed to differ — the\n// substitute's TrustingPeriod is adopted by the subject.\nfunc mismatchingClientStateField(subject, substitute *ClientState) string {\n\tif subject.TrustLevel != substitute.TrustLevel {\n\t\treturn \"TrustLevel\"\n\t}\n\tif subject.UnbondingPeriod != substitute.UnbondingPeriod {\n\t\treturn \"UnbondingPeriod\"\n\t}\n\tif subject.MaxClockDrift != substitute.MaxClockDrift {\n\t\treturn \"MaxClockDrift\"\n\t}\n\tif len(subject.ProofSpecs) != len(substitute.ProofSpecs) {\n\t\treturn \"ProofSpecs\"\n\t}\n\tfor i := range subject.ProofSpecs {\n\t\tif !subject.ProofSpecs[i].Equal(substitute.ProofSpecs[i]) {\n\t\t\treturn \"ProofSpecs\"\n\t\t}\n\t}\n\tif len(subject.UpgradePath) != len(substitute.UpgradePath) {\n\t\treturn \"UpgradePath\"\n\t}\n\tfor i := range subject.UpgradePath {\n\t\tif subject.UpgradePath[i] != substitute.UpgradePath[i] {\n\t\t\treturn \"UpgradePath\"\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// Implements lightclient.Interface\nfunc (tm *TMLightClient) VerifyUpgradeAndUpdateState(newClient, newConsState,\n\tupgradeClientProof, upgradeConsensusStateProof any) error {\n\tupgradedClientState, ok := newClient.(ClientState)\n\tif !ok {\n\t\treturn ufmt.Errorf(\"upgraded client state must be tendermint.ClientState, got %T\", newClient)\n\t}\n\tif err := upgradedClientState.ValidateBasic(); err != nil {\n\t\treturn ufmt.Errorf(\"invalid upgraded client state: %v\", err)\n\t}\n\n\tupgradedConsensusState, ok := newConsState.(ConsensusState)\n\tif !ok {\n\t\treturn ufmt.Errorf(\"upgraded consensus state must be tendermint.ConsensusState, got %T\", newConsState)\n\t}\n\tif err := upgradedConsensusState.ValidateBasic(); err != nil {\n\t\treturn ufmt.Errorf(\"invalid upgraded consensus state: %v\", err)\n\t}\n\n\tif len(tm.ClientState.UpgradePath) == 0 {\n\t\treturn errors.New(\"cannot upgrade client: no upgrade path set\")\n\t}\n\tif !upgradedClientState.LatestHeight.GT(tm.ClientState.LatestHeight) {\n\t\treturn ufmt.Errorf(\n\t\t\t\"upgraded client latest height (%s) must be greater than current latest height (%s)\",\n\t\t\tupgradedClientState.LatestHeight, tm.ClientState.LatestHeight,\n\t\t)\n\t}\n\n\tlastHeight := tm.ClientState.LatestHeight\n\n\tclientProofs, ok := upgradeClientProof.([]ics23.CommitmentProof)\n\tif !ok {\n\t\treturn ufmt.Errorf(\"upgradeClientProof must be []ics23.CommitmentProof, got %T\", upgradeClientProof)\n\t}\n\tif len(clientProofs) == 0 {\n\t\treturn errors.New(\"upgradeClientProof cannot be empty\")\n\t}\n\n\tconsensusProofs, ok := upgradeConsensusStateProof.([]ics23.CommitmentProof)\n\tif !ok {\n\t\treturn ufmt.Errorf(\"upgradeConsensusStateProof must be []ics23.CommitmentProof, got %T\", upgradeConsensusStateProof)\n\t}\n\tif len(consensusProofs) == 0 {\n\t\treturn errors.New(\"upgradeConsensusStateProof cannot be empty\")\n\t}\n\n\tpath := tm.ClientState.UpgradePath\n\tclientPath := buildUpgradeMerklePath(path, \"upgradedClient\", lastHeight)\n\t// Counterparties commit the upgraded client/consensus states wrapped in\n\t// google.protobuf.Any (cdc.MarshalInterface) and with the client's\n\t// customizable fields zeroed, so we verify against the same shape.\n\tclientValue := proto.MarshalAny(\n\t\tClientStateTypeURL,\n\t\tupgradedClientState.ZeroCustomFields().ProtoMarshal(),\n\t)\n\tif err := tm.VerifyMembership(lastHeight, clientProofs, clientPath, clientValue); err != nil {\n\t\treturn ufmt.Errorf(\"failed to verify upgrade client proof: %v\", err)\n\t}\n\n\t// \"upgradedConsState\" matches the SDK upgrade module's KeyUpgradedConsState\n\t// (note: no \"ensus\" — must match what the counterparty stored).\n\tconsensusPath := buildUpgradeMerklePath(path, \"upgradedConsState\", lastHeight)\n\tconsensusValue := proto.MarshalAny(\n\t\tConsensusStateTypeURL,\n\t\tupgradedConsensusState.ProtoMarshal(),\n\t)\n\tif err := tm.VerifyMembership(lastHeight, consensusProofs, consensusPath, consensusValue); err != nil {\n\t\treturn ufmt.Errorf(\"failed to verify upgrade consensus state proof: %v\", err)\n\t}\n\n\t// Construct the new client by combining chain-specified fields from the\n\t// upgraded client (ChainID, UnbondingPeriod, LatestHeight, ProofSpecs,\n\t// UpgradePath) with the customizable fields preserved from the current\n\t// client (TrustLevel, TrustingPeriod, MaxClockDrift). FrozenHeight is\n\t// reset on a successful upgrade. If the unbonding period shrinks, the\n\t// trusting period is scaled proportionally to maintain the security\n\t// ratio.\n\ttrustingPeriod := tm.ClientState.TrustingPeriod\n\tif upgradedClientState.UnbondingPeriod \u003c tm.ClientState.UnbondingPeriod {\n\t\ttrustingPeriod = calculateNewTrustingPeriod(\n\t\t\ttrustingPeriod, tm.ClientState.UnbondingPeriod, upgradedClientState.UnbondingPeriod,\n\t\t)\n\t}\n\tnewClientState := \u0026ClientState{\n\t\tChainID:         upgradedClientState.ChainID,\n\t\tTrustLevel:      tm.ClientState.TrustLevel,\n\t\tTrustingPeriod:  trustingPeriod,\n\t\tUnbondingPeriod: upgradedClientState.UnbondingPeriod,\n\t\tMaxClockDrift:   tm.ClientState.MaxClockDrift,\n\t\tFrozenHeight:    types.ZeroHeight(),\n\t\tLatestHeight:    upgradedClientState.LatestHeight,\n\t\tProofSpecs:      cloneProofSpecs(upgradedClientState.ProofSpecs),\n\t\tUpgradePath:     cloneStrings(upgradedClientState.UpgradePath),\n\t}\n\tif err := newClientState.ValidateBasic(); err != nil {\n\t\treturn ufmt.Errorf(\"upgraded client state failed basic validation: %v\", err)\n\t}\n\n\ttm.ClientState = newClientState\n\t// The upgraded consensus state is committed before the new chain\n\t// produces any blocks, so its Root is a placeholder. Construct a fresh\n\t// copy in our realm context with the sentinel Root so this consensus\n\t// state cannot be used to verify packet proofs — real Roots arrive\n\t// later via UpdateClient. Constructing a new value avoids the borrow\n\t// rule #2 readonly write that hits an inbound foreign-stamped value.\n\tnewConsensusState := ConsensusState{\n\t\tTimestamp:          upgradedConsensusState.Timestamp,\n\t\tRoot:               NewMerkleRoot([]byte(SentinelRoot)),\n\t\tNextValidatorsHash: cloneBytes(upgradedConsensusState.NextValidatorsHash),\n\t\tprocessedTime:      time.Now(),\n\t\tprocessedHeight:    types.GetSelfHeight(),\n\t}\n\ttm.SetConsensusState(newClientState.LatestHeight, \u0026newConsensusState)\n\n\treturn nil\n}\n\n// calculateNewTrustingPeriod scales the trusting period proportionally to a\n// shorter unbonding period: newTrusting = trusting * newUnbonding /\n// currentUnbonding (with truncation). The math is performed in seconds to\n// stay within int64 range for realistic chain values; sub-second precision\n// is irrelevant for IBC trusting periods, which are measured in days.\nfunc calculateNewTrustingPeriod(trustingPeriod, currentUnbondingPeriod, newUnbondingPeriod time.Duration) time.Duration {\n\tcurrentSec := int64(currentUnbondingPeriod / time.Second)\n\tif currentSec == 0 {\n\t\treturn 0\n\t}\n\ttrustingSec := int64(trustingPeriod / time.Second)\n\tnewUnbondingSec := int64(newUnbondingPeriod / time.Second)\n\treturn time.Duration(trustingSec*newUnbondingSec/currentSec) * time.Second\n}\n\n// buildUpgradeMerklePath constructs the merkle path the counterparty chain\n// stores the upgraded client/consensus state at. Each prefix element of\n// UpgradePath is kept as a separate KeyPath segment; the final element is\n// suffixed with the upgrade height and the leaf key (e.g. \"upgradedClient\"),\n// matching ibc-go's constructUpgrade*MerklePath. Caller must ensure\n// len(path) \u003e 0.\nfunc buildUpgradeMerklePath(path []string, lastKey string, height types.Height) types.MerklePath {\n\tkeyPath := make([][]byte, 0, len(path))\n\tfor _, k := range path[:len(path)-1] {\n\t\tkeyPath = append(keyPath, []byte(k))\n\t}\n\tappendedKey := ufmt.Sprintf(\"%s/%d/%s\", path[len(path)-1], height.RevisionHeight, lastKey)\n\tkeyPath = append(keyPath, []byte(appendedKey))\n\treturn types.MerklePath{KeyPath: keyPath}\n}\n\nfunc (tm *TMLightClient) SetConsensusState(height types.Height, consState *ConsensusState) {\n\ttm.ConsensusStateByHeight.Set(height.StringNatSort(), consState)\n}\n\nfunc (tm *TMLightClient) HasConsensusState(height types.Height) bool {\n\treturn tm.ConsensusStateByHeight.Has(height.StringNatSort())\n}\n\n// GetConsensusState returns the consensus state mapped at height, if any.\nfunc (tm *TMLightClient) GetConsensusState(height types.Height) (*ConsensusState, bool) {\n\tcs := tm.ConsensusStateByHeight.Get(height.StringNatSort())\n\tif cs == nil {\n\t\treturn nil, false\n\t}\n\treturn cs.(*ConsensusState), true\n}\n\n// GetNextConsensusState returns the lowest consensus state that is larger than\n// the given height.\nfunc (tm *TMLightClient) GetNextConsensusState(height types.Height) (*ConsensusState, bool) {\n\tvar cs *ConsensusState\n\ttm.ConsensusStateByHeight.Iterate(height.StringNatSort(), \"\", func(k string, v any) bool {\n\t\tif k == height.StringNatSort() {\n\t\t\treturn false // ignore passed height\n\t\t}\n\t\tcs = v.(*ConsensusState)\n\t\treturn true\n\t})\n\treturn cs, cs != nil\n}\n\n// GetPreviousConsensusState returns the highest consensus state that is lower\n// than the given height.\nfunc (tm *TMLightClient) GetPreviousConsensusState(height types.Height) (*ConsensusState, bool) {\n\tvar cs *ConsensusState\n\ttm.ConsensusStateByHeight.ReverseIterate(\"\", height.StringNatSort(), func(k string, v any) bool {\n\t\tif k == height.StringNatSort() {\n\t\t\treturn false // ignore passed height\n\t\t}\n\t\tcs = v.(*ConsensusState)\n\t\treturn true\n\t})\n\treturn cs, cs != nil\n}\n\n// pruneOldestConsensusState removes the lowest-height consensus state if it\n// has expired at now, and reports whether it did. Only the oldest entry is\n// examined, so the cost per call is constant.\nfunc (tm *TMLightClient) pruneOldestConsensusState(now time.Time) bool {\n\tif tm.ConsensusStateByHeight.Size() == 0 {\n\t\treturn false\n\t}\n\tkey, v := tm.ConsensusStateByHeight.GetByIndex(0)\n\tif !tm.IsExpired(v.(*ConsensusState).Timestamp, now) {\n\t\treturn false\n\t}\n\ttm.ConsensusStateByHeight.Remove(key)\n\treturn true\n}\n\n// PruneExpiredConsensusStates removes every consensus state that has expired\n// at now and returns how many were removed. An expired consensus state can no\n// longer verify headers or packet proofs, so removing it loses nothing.\n// UpdateState already prunes one entry per update; this bulk variant catches\n// up on clients that accumulated history before that, e.g. from an\n// implementation's OnInstall migration. Timestamps increase with height\n// (CheckForMisbehaviour freezes the client otherwise), so stopping at the\n// first unexpired entry finds them all.\nfunc (tm *TMLightClient) PruneExpiredConsensusStates(now time.Time) int {\n\tn := 0\n\tfor tm.pruneOldestConsensusState(now) {\n\t\tn++\n\t}\n\treturn n\n}\n\n// IsExpired returns whether or not the client has passed the trusting period\n// since the last update (in which case no headers are considered valid).\nfunc (tm *TMLightClient) IsExpired(latestTimestamp, now time.Time) bool {\n\texpirationTime := latestTimestamp.Add(tm.ClientState.TrustingPeriod)\n\treturn !expirationTime.After(now)\n}\n"},{"name":"validator.gno","body":"package tendermint\n\nimport (\n\t\"bytes\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"math\"\n\n\t\"gno.land/p/aib/encoding/proto/v0\"\n\t\"gno.land/p/aib/merkle/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// MaxTotalVotingPower is the largest total voting power a validator set may\n// have, as in cometbft. It leaves headroom for the threshold multiplications.\nconst MaxTotalVotingPower = int64(math.MaxInt64) / 8\n\n// ValidatorSet has no total voting power field: a relayer-supplied total\n// isn't covered by Hash(), so it is always recomputed from Validators (see\n// computeTotalVotingPower), as cometbft does in ValidatorSetFromProto.\ntype ValidatorSet struct {\n\tValidators []*Validator\n\tProposer   *Validator\n}\n\nfunc NewValset(vals ...*Validator) *ValidatorSet {\n\treturn \u0026ValidatorSet{\n\t\tValidators: vals,\n\t\tProposer:   vals[0],\n\t}\n}\n\nfunc (vals ValidatorSet) Hash() []byte {\n\tbzs := make([][]byte, len(vals.Validators))\n\tfor i, val := range vals.Validators {\n\t\tbzs[i] = val.Bytes()\n\t}\n\treturn merkle.HashFromByteSlices(bzs)\n}\n\n// computeTotalVotingPower returns the sum of the validators' voting powers.\n// It fails if a validator has a negative voting power or if the total exceeds\n// MaxTotalVotingPower.\nfunc (vals ValidatorSet) computeTotalVotingPower() (int64, error) {\n\tvar total int64\n\tfor i, val := range vals.Validators {\n\t\tif val == nil {\n\t\t\treturn 0, ufmt.Errorf(\"validator #%d is nil\", i)\n\t\t}\n\t\tif val.VotingPower \u003c 0 {\n\t\t\treturn 0, ufmt.Errorf(\"validator #%d has negative voting power %d\", i, val.VotingPower)\n\t\t}\n\t\tif val.VotingPower \u003e MaxTotalVotingPower-total {\n\t\t\treturn 0, ufmt.Errorf(\"total voting power exceeds maximum %d\", MaxTotalVotingPower)\n\t\t}\n\t\ttotal += val.VotingPower\n\t}\n\treturn total, nil\n}\n\n// votingPowerThreshold returns total*numerator/denominator, the voting power a\n// commit by vals must exceed, where total is recomputed from the validators.\nfunc votingPowerThreshold(vals *ValidatorSet, numerator, denominator uint64) (int64, error) {\n\ttotal, err := vals.computeTotalVotingPower()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tif denominator == 0 || denominator \u003e math.MaxInt64 {\n\t\treturn 0, ufmt.Errorf(\"invalid voting power threshold denominator %d\", denominator)\n\t}\n\tif total == 0 {\n\t\treturn 0, nil\n\t}\n\tif numerator \u003e uint64(math.MaxInt64/total) {\n\t\treturn 0, errors.New(\"voting power threshold overflows\")\n\t}\n\treturn total * int64(numerator) / int64(denominator), nil\n}\n\n// GetByAddress returns an index of the validator with address and validator\n// itself (copy) if found. Otherwise, -1 and nil are returned.\nfunc (vals *ValidatorSet) GetByAddress(address []byte) (index int32, val *Validator) {\n\tfor idx, val := range vals.Validators {\n\t\tif bytes.Equal(val.Address, address) {\n\t\t\treturn int32(idx), val\n\t\t}\n\t}\n\treturn -1, nil\n}\n\ntype Validator struct {\n\tAddress          []byte\n\tPubKey           []byte\n\tVotingPower      int64\n\tProposerPriority int64\n}\n\nfunc NewValidator(addr, pubkey string, vp int64) *Validator {\n\taddrBz, err := base64.StdEncoding.DecodeString(addr)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tpubkeyBz, err := base64.StdEncoding.DecodeString(pubkey)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn \u0026Validator{\n\t\tAddress:     addrBz,\n\t\tPubKey:      pubkeyBz,\n\t\tVotingPower: vp,\n\t}\n}\n\n// Bytes computes the unique encoding of a validator with a given voting power.\n// These are the bytes that gets hashed in consensus. It excludes address as\n// its redundant with the pubkey.This also excludes ProposerPriority which\n// changes every round.\n// Proto ref: https://buf.build/tendermint/tendermint/docs/main:tendermint.types#tendermint.types.SimpleValidator\nfunc (v Validator) Bytes() (bz []byte) {\n\tpkBz := proto.AppendLengthDelimited(nil, 1, v.PubKey)\n\tbz = proto.AppendLengthDelimited(bz, 1, pkBz)\n\tbz = proto.AppendVarint(bz, 2, uint64(v.VotingPower))\n\treturn\n}\n"},{"name":"verify.gno","body":"package tendermint\n\nimport (\n\t\"bytes\"\n\t\"crypto/ed25519\"\n\t\"encoding/hex\"\n\t\"time\"\n\n\t\"gno.land/p/aib/ibc/types/v0\"\n\t\"gno.land/p/aib/ics23/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nfunc (tm *TMLightClient) verifyHeader(msg *MsgHeader) error {\n\t// Retrieve trusted consensus states for trusted height\n\tconsState, found := tm.GetConsensusState(msg.TrustedHeight)\n\tif !found {\n\t\treturn ufmt.Errorf(\"could not get trusted consensus state for Header at TrustedHeight: %s\", msg.TrustedHeight)\n\t}\n\n\t// UpdateClient only accepts updates with a header at the same revision\n\t// as the trusted consensus state\n\tif msg.GetHeight().RevisionNumber != msg.TrustedHeight.RevisionNumber {\n\t\treturn ufmt.Errorf(\n\t\t\t\"header height revision %d does not match trusted header revision %d\",\n\t\t\tmsg.GetHeight().RevisionNumber, msg.TrustedHeight.RevisionNumber,\n\t\t)\n\t}\n\tif err := checkTrustedHeader(msg, consState); err != nil {\n\t\treturn err\n\t}\n\n\tvar (\n\t\tnow             = time.Now()\n\t\tuntrustedHeader = msg.Header\n\t\tuntrustedValset = msg.ValidatorSet\n\t\tchainID         = tm.ClientState.ChainID\n\t\ttrustedHeader   = \u0026Header{\n\t\t\tChainID:            chainID,\n\t\t\tHeight:             msg.TrustedHeight.RevisionHeight,\n\t\t\tTime:               consState.Timestamp,\n\t\t\tNextValidatorsHash: consState.NextValidatorsHash,\n\t\t}\n\t\ttrustedValset = msg.TrustedValidators\n\t)\n\n\t// UpdateClient only accepts updates with a header at the same revision\n\t// as the trusted consensus state\n\tif untrustedHeader.ChainID != trustedHeader.ChainID {\n\t\treturn ufmt.Errorf(\"header belongs to another chain %q, not %q\",\n\t\t\tuntrustedHeader.ChainID, trustedHeader.ChainID)\n\t}\n\n\tif !untrustedHeader.Time.After(trustedHeader.Time) {\n\t\treturn ufmt.Errorf(\n\t\t\t\"expected new header time %v to be after old header time %v\",\n\t\t\tuntrustedHeader.Time, trustedHeader.Time,\n\t\t)\n\t}\n\n\tif !untrustedHeader.Time.Before(now.Add(tm.ClientState.MaxClockDrift)) {\n\t\treturn ufmt.Errorf(\n\t\t\t\"new header has a time from the future %v (now: %v; max clock drift: %v)\",\n\t\t\tuntrustedHeader.Time, now, tm.ClientState.MaxClockDrift,\n\t\t)\n\t}\n\n\tif err := trustedHeader.Expired(tm.ClientState.TrustingPeriod); err != nil {\n\t\treturn err\n\t}\n\n\tif untrustedHeader.Height == trustedHeader.Height+1 {\n\t\t// Adjacent heights, check the validator hashes are the same\n\t\tif !bytes.Equal(untrustedHeader.ValidatorsHash, trustedHeader.NextValidatorsHash) {\n\t\t\th1, h2 := hex.EncodeToString(trustedHeader.NextValidatorsHash), hex.EncodeToString(untrustedHeader.ValidatorsHash)\n\t\t\treturn ufmt.Errorf(\n\t\t\t\t\"expected old header next validators (%s) to match those from new header (%s)\",\n\t\t\t\th1, h2,\n\t\t\t)\n\t\t}\n\t} else {\n\t\t// Heights aren't adjacent, this requires to check if at least trustlevel\n\t\t// ([1/3, 1]) of trusted validators have signed correctly.\n\t\ttl := tm.ClientState.TrustLevel\n\t\tvotingPowerNeeded, err := votingPowerThreshold(trustedValset, tl.Numerator, tl.Denominator)\n\t\tif err != nil {\n\t\t\treturn ufmt.Errorf(\"trusted validators: %v\", err)\n\t\t}\n\t\tif err := verifyCommit(chainID, trustedValset, msg.Commit, votingPowerNeeded, false); err != nil {\n\t\t\treturn ufmt.Errorf(\"check trusted validators VP\u003e%d: %v\", votingPowerNeeded, err)\n\t\t}\n\t}\n\t// Ensure that +2/3 of untrustedValset signed correctly.\n\t//\n\t// NOTE: this should always be the last check because untrustedValset can be\n\t// intentionally made very large to DOS the light client.\n\tvotingPowerNeeded, err := votingPowerThreshold(untrustedValset, 2, 3)\n\tif err != nil {\n\t\treturn ufmt.Errorf(\"untrusted validators: %v\", err)\n\t}\n\terr = verifyCommit(chainID, untrustedValset, msg.Commit, votingPowerNeeded, true)\n\tif err != nil {\n\t\treturn ufmt.Errorf(\"check untrusted validators VP\u003e%d: %v\", votingPowerNeeded, err)\n\t}\n\treturn nil\n}\n\n// verifyMisbehaviour determines whether or not two conflicting headers at the\n// same height would have convinced the light client.\nfunc (tm TMLightClient) verifyMisbehavior(misbehaviour *Misbehaviour) error {\n\t// Regardless of the type of misbehaviour, ensure that both headers are valid\n\t// and would have been accepted by light-client\n\n\t// Retrieve trusted consensus states for each Header in misbehaviour\n\ttmConsensusState1, found := tm.GetConsensusState(misbehaviour.Header1.TrustedHeight)\n\tif !found {\n\t\treturn ufmt.Errorf(\n\t\t\t\"could not get trusted consensus state from clientStore for Header1 at TrustedHeight: %s\",\n\t\t\tmisbehaviour.Header1.TrustedHeight,\n\t\t)\n\t}\n\n\ttmConsensusState2, found := tm.GetConsensusState(misbehaviour.Header2.TrustedHeight)\n\tif !found {\n\t\treturn ufmt.Errorf(\n\t\t\t\"could not get trusted consensus state from clientStore for Header2 at TrustedHeight: %s\",\n\t\t\tmisbehaviour.Header2.TrustedHeight,\n\t\t)\n\t}\n\n\t// Check the validity of the two conflicting headers against their respective\n\t// trusted consensus states.\n\tif err := tm.checkMisbehaviourHeader(tmConsensusState1, misbehaviour.Header1); err != nil {\n\t\treturn ufmt.Errorf(\"verifying Header1 in Misbehaviour failed: %v\", err)\n\t}\n\tif err := tm.checkMisbehaviourHeader(tmConsensusState2, misbehaviour.Header2); err != nil {\n\t\treturn ufmt.Errorf(\"verifying Header2 in Misbehaviour failed: %v\", err)\n\t}\n\n\treturn nil\n}\n\n// checkTrustedHeader checks that consensus state matches trusted fields of Header\nfunc checkTrustedHeader(msg *MsgHeader, consensusState *ConsensusState) error {\n\ttvalHash := msg.TrustedValidators.Hash()\n\tif !bytes.Equal(consensusState.NextValidatorsHash, tvalHash) {\n\t\th1, h2 := hex.EncodeToString(consensusState.NextValidatorsHash), hex.EncodeToString(tvalHash)\n\t\treturn ufmt.Errorf(\n\t\t\t\"trusted validators do not hash to latest trusted validators. Expected: %q, got: %q\",\n\t\t\th1, h2,\n\t\t)\n\t}\n\treturn nil\n}\n\n// verifyCommit verifies the signatures included in a commit. Returns an error\n// if votingPowerNeeded is not reached with the verified signatures.\n// CONTRACT: both commit and validator set should have passed validate basic.\nfunc verifyCommit(\n\tchainID string,\n\tvals *ValidatorSet,\n\tcommit *Commit,\n\tvotingPowerNeeded int64,\n\tlookUpByIndex bool,\n) error {\n\tvar (\n\t\tval                *Validator\n\t\tvalIdx             int32\n\t\tseenVals           = make(map[int32]int, len(commit.Signatures))\n\t\ttalliedVotingPower int64\n\t)\n\tfor idx, commitSig := range commit.Signatures {\n\t\tif commitSig.BlockIDFlag != BlockIDFlagCommit {\n\t\t\tcontinue\n\t\t}\n\n\t\t// If the vals and commit have a 1-to-1 correspondence we can retrieve\n\t\t// them by index else we need to retrieve them by address\n\t\tif lookUpByIndex {\n\t\t\tval = vals.Validators[idx]\n\t\t} else {\n\t\t\tvalIdx, val = vals.GetByAddress(commitSig.ValidatorAddress)\n\n\t\t\t// if the signature doesn't belong to anyone in the validator set\n\t\t\t// then we just skip over it\n\t\t\tif val == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// because we are getting validators by address we need to make sure\n\t\t\t// that the same validator doesn't commit twice\n\t\t\tif firstIndex, ok := seenVals[valIdx]; ok {\n\t\t\t\tsecondIndex := idx\n\t\t\t\taddr := hex.EncodeToString(val.Address)\n\t\t\t\treturn ufmt.Errorf(\"double vote from %s (%d and %d)\", addr, firstIndex, secondIndex)\n\t\t\t}\n\t\t\tseenVals[valIdx] = idx\n\t\t}\n\n\t\tif len(val.PubKey) == 0 {\n\t\t\taddr := hex.EncodeToString(val.Address)\n\t\t\treturn ufmt.Errorf(\"validator %s has a nil PubKey at index %d\", addr, idx)\n\t\t}\n\n\t\tvoteSignBytes := commit.BytesToSign(chainID, idx)\n\n\t\t// CONTRACT: all validators use ed25519 keys\n\t\tif !ed25519.Verify(val.PubKey, voteSignBytes, commitSig.Signature) {\n\t\t\treturn ufmt.Errorf(\"verify signature fail (#%d)\", idx)\n\t\t}\n\n\t\ttalliedVotingPower += val.VotingPower\n\n\t\t// check if we have enough signatures and can thus exit early\n\t\tif talliedVotingPower \u003e votingPowerNeeded {\n\t\t\treturn nil\n\t\t}\n\t}\n\treturn ufmt.Errorf(\n\t\t\"not enough voting power, got %v, needed \u003e%v\",\n\t\ttalliedVotingPower, votingPowerNeeded,\n\t)\n}\n\n// verifyChainedMembershipProof takes a list of proofs and specs and verifies\n// each proof sequentially ensuring that the value is committed to by first\n// proof and each subsequent subroot is committed to by the next subroot and\n// checking that the final calculated root is equal to the given roothash.\n// The proofs and specs are passed in from lowest subtree to the highest\n// subtree, but the keys are passed in from highest subtree to lowest.\n// The index specifies what index to start chaining the membership proofs, this\n// is useful since the lowest proof may not be a membership proof, thus we will\n// want to start the membership proof chaining from index 1 with value being\n// the lowest subroot\nfunc (tm TMLightClient) verifyChainedMembershipProof(root []byte,\n\tproofs []ics23.CommitmentProof, keys types.MerklePath, value []byte,\n\tindex int) error {\n\tvar (\n\t\tspecs   = tm.ClientState.ProofSpecs\n\t\tsubroot []byte\n\t\terr     error\n\t)\n\t// Initialize subroot to value since the proofs list may be empty.\n\t// This may happen if this call is verifying intermediate proofs after the\n\t// lowest proof has been executed.\n\t// In this case, there may be no intermediate proofs to verify and we just\n\t// check that lowest proof root equals final root\n\tsubroot = value\n\tfor i := index; i \u003c len(proofs); i++ {\n\t\t// verify membership of the proof at this index with appropriate key and\n\t\t// value\n\t\texist := proofs[i].GetExist()\n\t\tif exist == nil {\n\t\t\treturn ufmt.Errorf(\"commitment proof must be existence proof for verifying membership\")\n\t\t}\n\t\tsubroot, err = exist.Calculate()\n\t\tif err != nil {\n\t\t\treturn ufmt.Errorf(\"could not calculate proof root at index %d, merkle tree may be empty. %v\", i, err)\n\t\t}\n\n\t\t// Since keys are passed in from highest to lowest, we must grab their\n\t\t// indices in reverse order from the proofs and specs which are lowest to\n\t\t// highest\n\t\tkey := keys.KeyPath[len(keys.KeyPath)-1-i]\n\n\t\tif err := exist.Verify(specs[i], subroot, key, value); err != nil {\n\t\t\treturn ufmt.Errorf(\"failed to verify membership proof at index %d: %v\", i, err)\n\t\t}\n\t\t// Set value to subroot so that we verify next proof in chain commits to\n\t\t// this subroot\n\t\tvalue = subroot\n\t}\n\n\t// Check that chained proof root equals passed-in root\n\tif !bytes.Equal(root, subroot) {\n\t\th1, h2 := hex.EncodeToString(root), hex.EncodeToString(subroot)\n\t\treturn ufmt.Errorf(\"proof did not commit to expected root: %s, got: %s. Please ensure proof was submitted with correct proofHeight and to the correct chain.\", h1, h2)\n\t}\n\n\treturn nil\n}\n\n// checkMisbehaviourHeader checks that a MsgHeader in Misbehaviour is valid\n// misbehaviour given a trusted ConsensusState.\nfunc (tm TMLightClient) checkMisbehaviourHeader(consState *ConsensusState, msg *MsgHeader) error {\n\t// check the trusted fields for the header against ConsensusState\n\tif err := checkTrustedHeader(msg, consState); err != nil {\n\t\treturn err\n\t}\n\n\t// assert that the age of the trusted consensus state is not older than the\n\t// trusting period\n\tconsensusAge := time.Now().Sub(consState.Timestamp)\n\tif consensusAge \u003e= tm.ClientState.TrustingPeriod {\n\t\treturn ufmt.Errorf(\n\t\t\t\"current timestamp minus the latest consensus state timestamp is greater than or equal to the trusting period (%s \u003e= %s)\",\n\t\t\tconsensusAge, tm.ClientState.TrustingPeriod,\n\t\t)\n\t}\n\n\tchainID := tm.ClientState.ChainID\n\t// If chainID is in revision format, then set revision number of chainID with\n\t// the revision number of the misbehaviour header.\n\t// NOTE: misbehaviour verification is not supported for chains which upgrade\n\t// to a new chainID without strictly following the chainID revision format.\n\tif types.IsRevisionFormat(chainID) {\n\t\tchainID, _ = types.SetRevisionNumber(chainID, msg.GetHeight().RevisionNumber)\n\t}\n\n\t// - ValidatorSet must have TrustLevel similarity with trusted\n\t// FromValidatorSet\n\t// - ValidatorSets on both headers are valid given the last trusted\n\t// ValidatorSet\n\ttl := tm.ClientState.TrustLevel\n\tvotingPowerNeeded, err := votingPowerThreshold(msg.TrustedValidators, tl.Numerator, tl.Denominator)\n\tif err != nil {\n\t\treturn ufmt.Errorf(\"trusted validators: %v\", err)\n\t}\n\tif err := verifyCommit(chainID, msg.TrustedValidators, msg.Commit, votingPowerNeeded, false); err != nil {\n\t\treturn ufmt.Errorf(\"validator set in header has too much change from trusted validator set: %v\", err)\n\t}\n\treturn nil\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"UnWYl965X2aOOew6Mo9VcG1VAG1L9/cyYm88h6fKQOwlxDyZM0DzvdQC7Lwo1yF86sMWd6A9uRVcIork8hKU8g=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"jsonpage","path":"gno.land/p/aib/jsonpage/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/aib/jsonpage/v0\"\ngno = \"0.9\"\n"},{"name":"jsonpage.gno","body":"package jsonpage\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/mux/v0\"\n\t\"gno.land/p/onbloc/json/v0\"\n)\n\ntype JSONRenderer interface {\n\tRenderJSON() *json.Node\n}\n\nconst (\n\tmaxLimit     = 100\n\tdefaultLimit = 20\n)\n\n// Render returns a paginated JSON output.\n// CONTRACT: provide non-nil renderer func or values from the bptree.BPTree must\n// implement JSONRenderer.\nfunc Render(t bptree.ITree, r *mux.Request, renderer func(string, any) *json.Node) *json.Node {\n\tvar (\n\t\tpage  = 1\n\t\tlimit = defaultLimit\n\t)\n\tp, err := strconv.Atoi(r.Query.Get(\"page\"))\n\tif err == nil \u0026\u0026 p \u003e 0 {\n\t\tpage = p\n\t}\n\tl, err := strconv.Atoi(r.Query.Get(\"limit\"))\n\tif err == nil \u0026\u0026 l \u003e 0 {\n\t\tlimit = l\n\t}\n\treturn render(t, page, limit, renderer, false)\n}\n\nfunc render(t bptree.ITree, page, limit int, renderer func(string, any) *json.Node, reverse bool) *json.Node {\n\ttotal := t.Size()\n\tif limit == 0 || limit \u003e total {\n\t\tlimit = total\n\t}\n\tif limit \u003e maxLimit {\n\t\tlimit = maxLimit\n\t}\n\tvar (\n\t\toffset  = (page - 1) * limit\n\t\titems   []*json.Node\n\t\titerate = t.IterateByOffset\n\t)\n\tif reverse {\n\t\titerate = t.ReverseIterateByOffset\n\t}\n\titerate(offset, limit, func(k string, v any) bool {\n\t\tif renderer != nil {\n\t\t\titems = append(items, renderer(k, v))\n\t\t} else {\n\t\t\titems = append(items, v.(JSONRenderer).RenderJSON())\n\t\t}\n\t\treturn false\n\t})\n\ttotalPages := 1\n\tif limit \u003e 0 {\n\t\ttotalPages = (total + limit - 1) / limit\n\t}\n\treturn json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\"items\": json.ArrayNode(\"\", items),\n\t\t\"page\":  json.NumberNode(\"\", float64(page)),\n\t\t\"total\": json.NumberNode(\"\", float64(totalPages)),\n\t})\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"1eLTUXsWisZrFG/1JziCpIPYOdrwnsYzp7nzvAyXZVkWsGExpsEIKnTB3yDf81q8HzyrLjh6arDpNGCa0PupPw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"core","path":"gno.land/r/aib/ibc/core","files":[{"name":"README.md","body":"# r/aib/ibc\n\nBecause most of the functions in this realm take complex args, it is required\nto call them using `MsgRun` (`maketx run` with the CLI) instead of the more\ncommonly used `MsgCall`.\n\nHere is an exemple of the command:\n\n```\n$ gnokey maketx run -gas-fee 1000000ugnot -gas-wanted 90000000 \\\n    -broadcast -chainid \"dev\" -remote \"tcp://127.0.0.1:26657\" \\\n    ADDRESS run.gno\n```\n\n`run.gno` content depends on the called function, see the following sections\nfor examples.\n\n## RegisterApp\n\nApplications register themselves with `core.RegisterApp(cross(cur), portID, app)`\nfrom their own realm (a user cannot call it), each port at most once, and a port\nis never freed.\n\n- A registration from this project's namespace, `gno.land/r/aib/...`, is active\n  immediately: only the owner of the `aib` namespace can deploy there.\n- A registration from any other realm is recorded as **pending** and routes\n  nothing until an ibcgov decision activates it with\n  `ApproveApp(portID, pkgPath)`, or the authority drops it with\n  `RejectApp(portID, pkgPath)` (GovDAO constructor\n  `NewRejectAppProposalRequest`, see the Authority section). Pending\n  registrations claim no port, so they can\n  neither block a trusted app nor be approved once the port is taken.\n  `IsAppPending(portID, pkgPath)` and `/r/aib/ibc/core:apps/pending` report them;\n  the listing shows only the first 100 pending registrations, with no\n  pagination, and the full count apart, so a registration missing from it\n  is not shown absent: ask `IsAppPending(portID, pkgPath)` for a particular\n  one.\n\nEmitted events: `app_registered`, `app_registration_pending`, `app_approved`,\n`app_rejected`, with `port` and `pkg_path` attributes.\n\n## CreateClient\n\nSee [`zz_create_client_example_filetest.gno`](./filetests/zz_create_client_example_filetest.gno)\n\nEmitted event:\n```json\n{\n  \"type\": \"create_client\",\n  \"attrs\": [\n    {\n      \"key\": \"client_id\",\n      \"value\": \"07-tendermint-1\"\n    },\n    {\n      \"key\": \"client_type\",\n      \"value\": \"07-tendermint\"\n    },\n    {\n      \"key\": \"consensus_heights\",\n      \"value\": \"2/2\"\n    }\n  ],\n  \"pkg_path\": \"gno.land/r/aib/ibc/core\"\n}\n```\n\n## RegisterCounterparty\n\n`RegisterCounterparty(clientID, merklePrefix, counterpartyClientID)` must be\nsigned by the transaction's origin signer that created the client (its\ncreator), for any client type: the public relaying of Ethereum clients does\nnot extend to it. It is write-once: once a counterparty is registered, a\nsecond registration is refused. An Ethereum client's counterparty is the\nrouter's client id with eureka's single empty merkle prefix.\n\nSee [`zz_register_counterparty_example_filetest.gno`](./filetests/zz_register_counterparty_example_filetest.gno)\n\n## UpdateClient\n\nSee [`zz_update_client_example_filetest.gno`](./filetests/zz_update_client_example_filetest.gno)\n\nEmitted event:\n```json\n{\n  \"type\": \"update_client\",\n  \"attrs\": [\n    {\n      \"key\": \"client_id\",\n      \"value\": \"07-tendermint-1\"\n    },\n    {\n      \"key\": \"client_type\",\n      \"value\": \"07-tendermint\"\n    },\n    {\n      \"key\": \"consensus_heights\",\n      \"value\": \"2/5\"\n    }\n  ],\n  \"pkg_path\": \"gno.land/r/aib/ibc/core\"\n}\n```\n\n## Packet limits\n\nThe core checks these on every client's packets, the same on send and on\nreceive (`types.Packet.ValidateBasic`; on an Ethereum client's packets\n`types.Packet.ValidateBasicRouter`, which differs only in the client ids):\n\n| Field | Limit |\n|---|---|\n| payloads | exactly one (see SendPacket) |\n| payload data | at most `types.MaximumPayloadsSize`, 262,144 bytes (256 KiB), counted as the sum of `len(Payload.Value)`: the application's encoded packet data only. The ports, version, encoding, client ids and timeout are not counted, nor is the message carrying the packet (its proofs, or the `ethrelay` wire argument) |\n| payload value | not empty |\n| source and destination port | 2 to 128 characters of `a-z A-Z 0-9 . _ + - # [ ] \u003c \u003e` (no `/`) |\n| payload version and encoding | not blank; anything else is the application's to check (transfer: `ics20-1`, and `application/x-protobuf` or `application/x-solidity-abi`) |\n| source and destination client id | 4 to 64 characters of the same alphabet. On an Ethereum client's packets either may instead be an ICS26 router client id: `client-N` (N a decimal uint64 of at most 20 digits, no leading zero) or a custom id of 4 to 128 characters of that alphabet, not starting with `client-` or `channel-`. `SendPacket` also checks its source client against the first rule before it builds the packet |\n| sequence | not 0 |\n| timeout timestamp | not 0 (Unix seconds). On send: after the current block time, at most 24 hours after it (`maxTimeoutDelta`), and after the sending client's latest consensus timestamp. On receive: refused when the current block time is at or after the packet's timeout timestamp |\n\nA packet over a limit is never committed or received. On send, `SendPacket`\npanics and the whole transaction aborts: nothing is escrowed or burned and no\nsequence is used. On receive, `RecvPacket` refuses the message before it\nverifies a proof, writes a receipt or calls the application, on every route,\nEthereum's included. This is a core refusal, not an application error\nacknowledgement: no receipt and no acknowledgement are written, so the\npacket stays pending at its source until its timeout, when the source\nrefunds it (a timeout proves the receipt absent).\n\n## SendPacket\n\nSee [`zz_send_packet_example_filetest.gno`](./filetests/zz_send_packet_example_filetest.gno)\n\nA packet must carry **exactly one** payload; `SendPacket` and `RecvPacket` both\nreject any other count. The restriction matches ibc-go and keeps `RecvPacket`\natomic: its callback loop stops at the first failing payload without reverting\nthe ones that already succeeded, so several payloads could commit a partial\nstate change while the counterparty refunds the whole packet.\n\nEmitted event:\n```json\n[\n  {\n    \"type\": \"send_packet\",\n    \"attrs\": [\n      {\n        \"key\": \"packet_source_client\",\n        \"value\": \"07-tendermint-1\"\n      },\n      {\n        \"key\": \"packet_dest_client\",\n        \"value\": \"counter-party-id\"\n      },\n      {\n        \"key\": \"packet_sequence\",\n        \"value\": \"1\"\n      },\n      {\n        \"key\": \"packet_timeout_timestamp\",\n        \"value\": \"1234571490\"\n      },\n      {\n        \"key\": \"encoded_packet_hex\",\n        \"value\": \"0801120f30372d74656e6465726d696e742d311a10636f756e7465722d70617274792d696420e2a1d8cc042a3f0a12676e6f2e6c616e645f725f69626361707031120f64657374696e6174696f6e506f72741a02763122106170706c69636174696f6e2f6a736f6e2a027b7d\"\n      }\n    ],\n    \"pkg_path\": \"gno.land/r/aib/ibc/core\"\n  },\n]\n```\n\n## WriteAcknowledgement\n\nFor the standard synchronous flow, `RecvPacket` writes the application\nacknowledgement itself. An application may instead return\n`PacketStatus_Async` from `OnRecvPacket`, in which case the ack is deferred\nuntil the application calls `WriteAcknowledgement` later (typically from a\nsubsequent `OnAcknowledgementPacket` or `OnTimeoutPacket` callback while\nforwarding the parent packet). Only the realm whose `OnRecvPacket` returned\nasync is authorized to write the ack.\n\nSee [`z10a_async_ack_filetest.gno`](./filetests/z10a_async_ack_filetest.gno) for the\nfull A → B → C forward-and-ack call graph.\n\nEmitted event:\n```json\n{\n  \"type\": \"write_acknowledgement\",\n  \"attrs\": [\n    {\n      \"key\": \"packet_source_client\",\n      \"value\": \"07-tendermint-42\"\n    },\n    {\n      \"key\": \"packet_dest_client\",\n      \"value\": \"07-tendermint-1\"\n    },\n    {\n      \"key\": \"packet_sequence\",\n      \"value\": \"1\"\n    },\n    {\n      \"key\": \"packet_timeout_timestamp\",\n      \"value\": \"1234571490\"\n    },\n    {\n      \"key\": \"encoded_packet_hex\",\n      \"value\": \"0801121030372d74656e6465726d696e742d34321a0f30372d74656e6465726d696e742d3120e2a1d8cc042a280a056170704944120561707049441a02763122106170706c69636174696f6e2f6a736f6e2a027b7d\"\n    },\n    {\n      \"key\": \"encoded_acknowledgement_hex\",\n      \"value\": \"0a0101\"\n    }\n  ],\n  \"pkg_path\": \"gno.land/r/aib/ibc/core\"\n}\n```\n\n## Acknowledgement\n\nSee [`zz_acknowledgement_example_filetest.gno`](./filetests/zz_acknowledgement_example_filetest.gno)\n\nEmitted event:\n```json\n[\n  {\n    \"type\": \"acknowledge_packet\",\n    \"attrs\": [\n      {\n        \"key\": \"packet_source_client\",\n        \"value\": \"07-tendermint-1\"\n      },\n      {\n        \"key\": \"packet_dest_client\",\n        \"value\": \"07-tendermint-42\"\n      },\n      {\n        \"key\": \"packet_sequence\",\n        \"value\": \"1\"\n      },\n      {\n        \"key\": \"packet_timeout_timestamp\",\n        \"value\": \"1234571490\"\n      },\n      {\n        \"key\": \"encoded_packet_hex\",\n        \"value\": \"0801120f30372d74656e6465726d696e742d311a1030372d74656e6465726d696e742d343220e2a1d8cc042a300a03617070120f64657374696e6174696f6e506f72741a02763122106170706c69636174696f6e2f6a736f6e2a027b7d\"\n      }\n    ],\n    \"pkg_path\": \"gno.land/r/aib/ibc/core\"\n  }\n]\n```\n\n## RecoverClient\n\nSee [`zz_recover_client_example_filetest.gno`](./filetests/zz_recover_client_example_filetest.gno)\n\nSee [`recover-client.md`](./recover-client.md) for the end-to-end flow and\nwhich parameters a Tendermint recovery can adjust.\n\nRecovery is a governed decision (7 days). The subject must be Frozen or\nExpired, the substitute Active, the two of the same client type, and neither\nretired. For an Ethereum client (`ethereum.CheckRecovery`) the substitute is\nread through its `Ethereum()` state, so it may be the v0 verifier or a\nverifier version that wraps it (as after a verifier replacement), and so may\nthe subject; a verifier type without that state needs recovery support of\nits own. The substitute must track the same chain and router: the same chain id, genesis validators root,\ngenesis time and slot, fork schedule, sync committee size, seconds per slot,\nslots per epoch, epochs per sync committee period, router (IBC contract)\naddress and commitment slot; its latest slot must be strictly ahead of the\nsubject's, with a consensus state there. `ethereum.RecoverClient` then copies\nthe substitute's latest consensus state into the subject, adopts its latest\nslot and execution block number and its minimum sync committee\nparticipation, unfreezes the subject, and trusts nothing older: consensus\nstates below the copied one stop verifying proofs. The client's trust\ngeneration grows with the recovery.\n\nEmitted event:\n```json\n{\n  \"type\": \"recover_client\",\n  \"attrs\": [\n    {\n      \"key\": \"subject_client_id\",\n      \"value\": \"07-tendermint-1\"\n    },\n    {\n      \"key\": \"substitute_client_id\",\n      \"value\": \"07-tendermint-2\"\n    },\n    {\n      \"key\": \"client_type\",\n      \"value\": \"07-tendermint\"\n    }\n  ],\n  \"pkg_path\": \"gno.land/r/aib/ibc/core\"\n}\n```\n\n## UpgradeClient\n\nSee [`zz_upgrade_client_example_filetest.gno`](./filetests/zz_upgrade_client_example_filetest.gno)\n\nSee [`upgrade-client.md`](./upgrade-client.md) for the lifecycle, the\nproof shape, and the field mapping between the current client and the\nupgraded client.\n\nEmitted event:\n```json\n{\n  \"type\": \"upgrade_client\",\n  \"attrs\": [\n    {\n      \"key\": \"client_id\",\n      \"value\": \"07-tendermint-1\"\n    },\n    {\n      \"key\": \"client_type\",\n      \"value\": \"07-tendermint\"\n    },\n    {\n      \"key\": \"consensus_height\",\n      \"value\": \"1/100\"\n    }\n  ],\n  \"pkg_path\": \"gno.land/r/aib/ibc/core\"\n}\n```\n\n## Pause / Unpause\n\n`core.Pause` suspends the packet path: `SendPacket`, `RecvPacket`,\n`WriteAcknowledgement`, `Acknowledgement` and `Timeout` panic with `core is\npaused` until `core.Unpause`. Client operations (`CreateClient`,\n`RegisterCounterparty`, `UpdateClient`, `UpgradeClient`, `RecoverClient`) keep\nworking, so relayers can keep clients alive during a pause. `Pause` is gated\nby the authority (GovDAO or ibcgov, see below); `Unpause` is an ibcgov\ndecision. A pause stops the core's packet path, the transfer app's\ncallbacks included, so refunds that would arrive through acknowledgements\nand timeouts wait: it is reserved for a core bug. It does not stop the\ntransfer app's local payouts of what is already booked: `Claim` keeps paying\nclaims and `ClaimRefunds` keeps paying queued refunds on an Ethereum route\nduring a pause. To stop those, use the route's flags (`payouts` stops claim\npayouts and sweep payments, `refunds` holds refund payouts); they are the\nincident tool for the Ethereum route (ADR pr6375 §7.3).\n\n`core.Paused()` reports the state, which is also shown on `/r/aib/ibc/core:admin`.\nIn-flight packets are not lost: commitments, receipts and acknowledgements stay\nin place and resume after `Unpause`; packets that time out meanwhile are\nrefunded by the usual timeout path once resumed.\n\nEmitted events: `pause` and `unpause`, with no attributes.\n\n## PauseApp / UnpauseApp\n\n`core.PauseApp(portID)` suspends one application instead of the whole packet\npath: `SendPacket` refuses payloads for that port (the sender's transaction\nfails, nothing moves), and `RecvPacket` answers incoming packets for it with the\nerror acknowledgement without invoking the app, so the counterparty refunds its\nsenders. `Acknowledgement` and `Timeout` callbacks keep running, so refunds the\napp owes are never held back. `core.UnpauseApp(portID)` resumes it. `PauseApp`\nis gated by the authority (GovDAO constructor `NewPauseAppProposalRequest`);\n`UnpauseApp` is an ibcgov decision. `IsAppPaused`\nand the `paused` field of `/r/aib/ibc/core:apps` report the state. Applications\ndo not carry a pause switch of their own.\n\nEmitted events: `app_paused` and `app_unpaused`, with a `port` attribute.\n\n## Authority\n\nAdministration is split by direction (ADR pr6375 R14):\n\n- **Tightening operations** (`Pause`, `PauseApp`, `RejectApp`) act at once and\n  are gated by an authority (`p/aib/authority`). Its initial members are\n  the GovDAO proxy `gno.land/r/gov/dao` and the governance realm\n  `gno.land/r/aib/ibc/ibcgov` (`IBCGovPath`). There is no deployer member.\n  ibcgov can remove GovDAO at once (`NewCoreRemoveMemberRequest`); only\n  ibcgov itself is irremovable, so GovDAO's tightening powers last only\n  while it remains a member.\n- **Governed operations** (`Unpause`, `UnpauseApp`, `ApproveApp`,\n  `AddRelayer`, `RemoveRelayer`, `RecoverClient`, `UpdateImpl`,\n  `MigrateClient`, `RetireClient`, `RetireClientPort`, `AddAuthorityMember`,\n  `RemoveAuthorityMember`) are decisions of `gno.land/r/aib/ibc/ibcgov` alone,\n  under its decision ledger, the governor's one-way yield to GovDAO and the\n  guardians' holds. The loosening ones wait 7 days: `ApproveApp`,\n  `AddRelayer`, `RecoverClient`, `UpdateImpl`, `MigrateClient`,\n  `AddAuthorityMember`, and `RemoveRelayer` of the last listed relayer (an\n  empty whitelist lets anyone relay). The others apply at once: `Unpause` and\n  `UnpauseApp` (spent under a guardian hold), `RemoveAuthorityMember`, and\n  `RemoveRelayer` while other relayers stay listed (the whitelist is read\n  when the decision is recorded). `RetireClient` and `RetireClientPort` are\n  not decisions of their own: a route replacement retires the route's old\n  client, and ibcgov's acceptance of IBCAdmin's epoch notice finishes the\n  retirement (see below). A call from anyone\n  else, GovDAO's proxy included, panics with\n  `only gno.land/r/aib/ibc/ibcgov decides this`.\n\nThe principal is the caller of the realm function, `cur.Previous()`, not the\ntransaction signer.\n\n- `IsAuthorityMember(addr)`: reports membership; `/r/aib/ibc/core:admin` lists\n  the members.\n\nGovDAO acts on the tightening operations through proposals\n(`NewPauseProposalRequest`, `NewPauseAppProposalRequest`,\n`NewRejectAppProposalRequest`) returning a `dao.ProposalRequest` whose callback\nruns the operation with the DAO proxy as caller. A GovDAO member submits it\nwith `MsgRun`:\n\n```gno\npid := dao.MustCreateProposal(cross(cur), core.NewPauseProposalRequest(cross(cur)))\n```\n\nthen members vote (`MustVoteOnProposalSimple`) and anyone executes\n(`ExecuteProposal`), both plain `MsgCall`s on `gno.land/r/gov/dao`.\n\nEmitted events: `authority_member_added` and `authority_member_removed`, with an\n`address` attribute; `client_retired`, with a `client_id` attribute (and a\n`port` attribute when the client is retired for one port only).\n\n## Relaying for Ethereum clients\n\n`UpdateClient`, `RecvPacket`, `Acknowledgement` and `Timeout` apply the relayer\nwhitelist only to non-Ethereum clients. For a client of type `ethereum`, every\none of them is permissionless (ADR pr6375 R17): all of them are proof-checked,\nand a refund needs both a fresh client update and the acknowledgement or\ntimeout, so neither may wait on a listed relayer. `CreateClient` and\n`UpgradeClient` keep the whitelist.\n\n## Receipts\n\nA received packet's receipt is `types.CommitPacketReceipt(packet)`, a sha256\ncommitment to the sequence, the source client and the packet's IBC v2\ncommitment, stored at the ICS24 receipt path (ADR pr6375 R19). It says which\npacket occupies a sequence, so a reconciliation can recompute it from the\nauthenticated packet. Ordinary timeout processing proves only a receipt's\nabsence; Ethereum's reconciliation also proves its membership and compares\nits value (`GnoICS20Evidence`). The same\npacket relayed again is a no-op once the checks that come before the\nreceipt's pass (the destination client known, not retired and Active, the\ncounterparty registered and matching the source, the packet not timed out):\nan identical retry at or after its timeout is refused like any late packet.\nA different packet at a received sequence is refused.\n\n## Client migration and retirement\n\n`MigrateClient(clientID, migration, params)` migrates a live client while\nkeeping its id, consensus states and packet state, through an implementation\nthat implements `core.ClientMigrator` (ADR pr6375 R16). `impl/v1` has two\nmigrations.\n\n`ethereum-verifier` (params: the target verifier version; ibcgov\n`NewReplaceVerifierRequest(clientID, toVersion)`) replaces the client's\nverifier by another verifier version. The conversion is code of a core\nimplementation: a later implementation compiles it in and declares it as\nplain values (`implv1.NewWithConversions(store, version, from, to,\nconvert)`, each `convert` a function of the new verifier's /p/ package\nreceiving a read-only view of the live verifier, `ethereum.Retired`, so it\ncannot write to it), and\nis installed by the governed, manifest-checked implementation upgrade;\n`impl/v1` itself declares none. No other code converts a verifier. The\nactive implementation's preflight reads its declared pairs and the client's\nstate (an Ethereum client, Active or Expired, never Frozen, its\n`VerifierVersion` the declared source, and a verifier exposing its ethereum/v0 state through\n`Ethereum() *ethereum.EthLightClient`, which the read-only view is built\nfrom: a verifier keeping its state otherwise needs migration support of its\nown, and is refused, the decision spent); the conversion and every check on its result (it names the target\nversion, keeps the live verifier's status, Active or Expired, its latest\nheight and, when it exposes ethereum/v0 state, the slot it trusts consensus\nstates from) run inside one recovered\nboundary in the core's realm: a panic or a mismatch refuses the migration\nwith nothing changed, and the decision is spent; otherwise the verifier is\ninstalled through `Client.SetLightClient`. The new verifier keeps the\nclient's history without copying it (`ethereum.NewFrom`): it holds a\nread-only view of the retired verifier and serves every consensus state at\nor below the retired verifier's latest slot from it, storing its own apart,\nso every pre-replacement trusted height stays provable; nothing writes to\nthe retired verifier again. The client keeps its id and packet state; its\ntrust generation advances, and ibcgov empties its route's payout meters. An\nExpired client is converted too and stays Expired, so a client whose updates\nstopped long enough to expire is repaired by the conversion, then an\nordinary recovery (`RecoverClient`, with a substitute ahead of it); a Frozen\nclient is recovered first (transfer filetest `z9zu`).\n\n`ethereum-fork-schedule` (params `ethereum.EncodeForkParameters(...)`)\nchanges the fork schedule of the client's verifier in place, through the\n`CheckForkParameters` and `MigrateForkParameters` methods the verifier\nimplements: ethereum/v0's verifier, and a later version embedding it. A\nverifier without them is refused when the decision is checked, and the\ndecision is spent. The new schedule is checked\n(`ethereum.CheckForkParameters`, through `CheckMigrateClient`) when the\ndecision executes, before anything changes: the client must be Active, the schedule must differ from the current\none, the genesis fork version and slot never change, every fork already\nactive at the current epoch (the later of the latest trusted slot's and the\nblock time's) keeps its version and epoch, every changed fork activates after\nthe current epoch, and the fork epochs stay in order. A fork whose containers\nchange needs a new verifier version, installed by `ethereum-verifier`.\n\n`RetireClient(clientID)` marks the client retired in the proxy, for a route\nreplacement (ADR pr6375 §7.12, R18; the parent ADR freezes the old client,\nthe retirement marker stands in for that). It refuses every later packet\nmessage, update, registration and recovery naming the client, with one\nexception: the app that registered a packet's port may still write a pending\nasynchronous acknowledgement for a packet the client received before its\nretirement (`WriteAcknowledgement`), so the packet reaches a terminal state.\nThe verifier itself is untouched: its status stays what it was (the `z16b`\nfiletest reports `retired: true status: Active`), and `IsClientRetired`, not\nthe status, tells a retired client apart. Both are governance decisions; the\nretirement happens inside a route replacement.\n\n`RetireClientPort(clientID, port)` retires a client for one port only: every\npacket message naming the client with a payload on that port (its source\nport when sent from here, its destination port when received) is refused,\nwhile its other ports, its updates and its counterparty stay\n(`IsClientRetiredFor(clientID, port)`). A route replacement retires the old\nclient for the `transfer` port only while ibcgov's governance epoch runs\nover it, or while it is the client of a transition or tombstone on\nibcgov's expected graph at any distance, so governance packets in flight\ncan still be re-sent over it and a later notice can still arrive; and, on\nthe governed Ethereum route once ibcgov's channel is bound, also when the graph\ndoes not hold it and no epoch switch has passed it, since a catch-up may\nneed it later. ibcgov retires a client for governance only when an\nauthenticated epoch switch leaves it as the old epoch's client (the path's\nintermediate clients are kept, since whoever applies a notice names the\npath); a switch's pruning and a tombstone purge never\nretire anything: the clients they disconnect are kept off the graph, gov\nport open and inert, and `PruneGovRetirements` retires nothing a switch\ndid not pass (ADR 0004, Governance channel; transfer filetests `z9zv`,\n`z9zza`, `z9zzq`, `z9zzu`).\n\n`ClientRouteIdentity(clientID)` is the history a client's verifier\nauthenticates, and the router it reads: for an Ethereum client its chain\nid, genesis validators root, ICS26 router and commitments slot, all fixed at\ncreation (`IdentifiedVerifier`; `\"\"` for a client whose verifier names\nnone). A route records it at its enrolment, and a replacement client must\nname the same one.\n\nThe read accessors `ClientType`, `CounterpartyClientID`, `IsClientRetired`,\n`IsClientRetiredFor`, `ClientRouteIdentity`,\n`PacketCommitment`, `PacketReceipt`, `PacketAcknowledgement` and\n`HasPendingAsyncAck` return copies, for realms (the transfer app, ibcgov) that\nmust check a packet's state.\n\n## Upgrades\n\nThis realm is a permanent, thin proxy (see\n[ADR 0001](../../../../../docs/adrs/0001-proxy-realm-for-core-upgrades.md)): it\nowns the state, the chain params and the events, which must never move, and\nforwards every entry point to an implementation realm that holds the client and\npacket lifecycle logic. The first one is `gno.land/r/aib/ibc/core/impl/v0`.\n\nAn implementation registers itself from its `init` with `RegisterImpl`; the\nfirst registration activates at once (bootstrap), every later one is only a\ncandidate until an `UpdateImpl` decision of `gno.land/r/aib/ibc/ibcgov`\n(7 days) activates it. The\nnew implementation is built against the same store and its `OnInstall` hook\nruns before the switch, so clients, commitments, receipts and acknowledgements\nsurvive and a failing migration leaves the current implementation active.\nRollback is `UpdateImpl` with the previous path. `ImplPath()` and\n`ImplVersion()` tell which one is active; the home and `admin` pages show it.\nNothing outside the chain changes during an upgrade: this path, the entry\npoints, the events and the render routes are the proxy's, so the relayer, the\ncounterparty and the users keep using them. The steps are in\n[`docs/upgrade-runbook.md`](../../../../../docs/upgrade-runbook.md).\n\n`Render` is forwarded to the implementation as well, JSON routes included, so\nthe routes can follow the verifier they read across upgrades. The routes the\nrelayer consumes (`clients`, `clients/{id}`, its consensus states, packet\ncommitments and receipts) are a compatibility contract pinned by the `z0c`\nfiletest: a change there means a relayer release. `clients/{id}/consensus_states`\nlists, paginated, an Ethereum client's consensus states as a Tendermint\nclient's (`z16h`): those its verifier stores itself, a state below a\nrecovery's cut-off listed as untrusted; after a verifier replacement, the\nretired verifier's states are served one height at a time\n(`clients/{id}/consensus_states/{revision}/{height}`).\n\nEmitted event:\n\n```json\n{\n  \"type\": \"impl_updated\",\n  \"attrs\": [\n    {\"key\": \"old_path\", \"value\": \"gno.land/r/aib/ibc/core/impl/v0\"},\n    {\"key\": \"new_path\", \"value\": \"gno.land/r/aib/ibc/core/impl/v1\"},\n    {\"key\": \"version\", \"value\": \"v1\"}\n  ],\n  \"pkg_path\": \"gno.land/r/aib/ibc/core\"\n}\n```\n"},{"name":"admin.gno","body":"package core\n\nimport (\n\t\"chain\"\n\t\"chain/runtime/unsafe\"\n\n\t\"gno.land/p/aib/authority/v0\"\n\t\"gno.land/p/aib/ibc/lightclient/ethereum/v0\"\n\t\"gno.land/p/aib/ibc/types/v0\"\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nconst (\n\tEventTypePause                  = \"pause\"\n\tEventTypeUnpause                = \"unpause\"\n\tEventTypeAuthorityMemberAdded   = \"authority_member_added\"\n\tEventTypeAuthorityMemberRemoved = \"authority_member_removed\"\n\tEventTypeClientRetired          = \"client_retired\"\n)\n\n// IBCGovPath is the governance realm of the IBC realms (ADR pr6375 R11, R14):\n// it holds the decision ledger, the governor and its one-way yield to GovDAO,\n// and the 7-day delays. Every power that loosens something here (an\n// implementation upgrade, the relayer list, unpausing, approving an app,\n// recovering, migrating or retiring a client, the authority's membership) is\n// gated on a call from it, so no decision skips its delay.\nconst IBCGovPath = \"gno.land/r/aib/ibc/ibcgov\"\n\nvar (\n\t// auth gates the immediate, tightening operations: Pause, PauseApp and\n\t// RejectApp. Its members are fixed at init to the GovDAO proxy, which acts\n\t// through the proposal constructors in proposals.gno, and the ibcgov\n\t// realm; there is no deployer member (ADR pr6375 R14). The principal is\n\t// cur.Previous().Address(): a GovDAO callback declared in this realm, or a\n\t// crossing call from ibcgov. Membership changes are governance decisions.\n\tauth     *authority.Authority\n\trelayers = bptree.NewBPTree32()\n\tpaused   bool\n)\n\nfunc init(cur realm) {\n\tauth = authority.NewWithMembers(\n\t\tchain.PackageAddress(authority.GovDAOPath),\n\t\tchain.PackageAddress(IBCGovPath),\n\t)\n}\n\n// The tightening functions below are thin crossing wrappers over non-crossing\n// (_ int, rlm realm, ...) helpers. The helpers are what the GovDAO proposal\n// callbacks (proposals.gno) invoke: a callback runs with this realm's live cur\n// whose Previous() is the GovDAO proxy, whereas a crossing call from inside\n// the callback would present this realm itself.\n\n// assertGovernance panics unless the caller of the crossing function whose\n// cur it is passed (a non-crossing call) is the ibcgov realm.\nfunc assertGovernance(cur realm, op string) {\n\tif p := cur.Previous().PkgPath(); p != IBCGovPath {\n\t\tcaller := p\n\t\tif caller == \"\" {\n\t\t\tcaller = string(cur.Previous().Address())\n\t\t}\n\t\tpanic(ufmt.Sprintf(\"%s: only %s decides this, not %s\", op, IBCGovPath, caller))\n\t}\n}\n\n// AddAuthorityMember adds a member to the authority. Governance only.\nfunc AddAuthorityMember(cur realm, addr address) {\n\tassertGovernance(cur, \"add_authority_member\")\n\tauth.AddMember(0, cur, addr)\n\tchain.Emit(EventTypeAuthorityMemberAdded, \"address\", string(addr))\n}\n\n// RemoveAuthorityMember removes a member; the last one cannot be removed.\n// Governance only.\nfunc RemoveAuthorityMember(cur realm, addr address) {\n\tassertGovernance(cur, \"remove_authority_member\")\n\tif addr == chain.PackageAddress(IBCGovPath) {\n\t\t// ibcgov changes membership as a member: removing it would freeze\n\t\t// the membership and its tightening powers for good.\n\t\tpanic(\"remove_authority_member: the governance realm stays a member\")\n\t}\n\tauth.RemoveMember(0, cur, addr)\n\tchain.Emit(EventTypeAuthorityMemberRemoved, \"address\", string(addr))\n}\n\n// AuthorityMembers returns the members of the authority.\nfunc AuthorityMembers() []address {\n\treturn auth.Members()\n}\n\n// IsAuthorityMember reports whether addr may run the tightening operations.\nfunc IsAuthorityMember(addr address) bool {\n\treturn auth.IsMember(addr)\n}\n\n// AddRelayer adds an address to the authorized relayer whitelist. Governance\n// only.\nfunc AddRelayer(cur realm, addr address) {\n\tassertGovernance(cur, \"add_relayer\")\n\trelayers.Set(string(addr), true)\n}\n\n// RemoveRelayer removes an address from the authorized relayer whitelist.\n// Governance only: removing the last one opens relaying to anyone.\nfunc RemoveRelayer(cur realm, addr address) {\n\tassertGovernance(cur, \"remove_relayer\")\n\trelayers.Remove(string(addr))\n}\n\n// Relayers returns the relayer whitelist, empty when any relayer is allowed.\nfunc Relayers() []address {\n\tout := make([]address, 0, relayers.Size())\n\trelayers.Iterate(\"\", \"\", func(k string, _ any) bool {\n\t\tout = append(out, address(k))\n\t\treturn false\n\t})\n\treturn out\n}\n\n// Pause suspends the packet path: SendPacket, RecvPacket, WriteAcknowledgement,\n// Acknowledgement and Timeout panic until Unpause. Client operations\n// (CreateClient, RegisterCounterparty, UpdateClient, UpgradeClient,\n// RecoverClient) keep working, so clients do not expire during a pause. It\n// blocks refunds too, so it is reserved for a core bug (ADR pr6375 §7.3).\n// Authority members only; see proposals.gno for the GovDAO path.\nfunc Pause(cur realm) {\n\tpause(0, cur)\n}\n\nfunc pause(_ int, rlm realm) {\n\tauth.Assert(0, rlm, \"pause\")\n\tpaused = true\n\tchain.Emit(EventTypePause)\n}\n\n// Unpause resumes the packet path. Governance only.\nfunc Unpause(cur realm) {\n\tassertGovernance(cur, \"unpause\")\n\tpaused = false\n\tchain.Emit(EventTypeUnpause)\n}\n\n// Paused reports whether the packet path is suspended.\nfunc Paused() bool {\n\treturn paused\n}\n\n// ensureNotPaused panics if the packet path is suspended.\nfunc ensureNotPaused() {\n\tif paused {\n\t\tpanic(\"core is paused\")\n\t}\n}\n\n// RetireClient retires a client for good: every packet message, client update\n// and counterparty registration naming it is refused from now on, whatever\n// its verifier's status. A route replacement (ADR pr6375 §7.12, R18) retires\n// the old client of a route that a new client replaces, after which the old\n// route's packets are settled by reconciliation. Governance only.\nfunc RetireClient(cur realm, clientID string) {\n\tassertGovernance(cur, \"retire_client\")\n\tif store.getClient(clientID) == nil {\n\t\tpanic(ufmt.Sprintf(\"client %s not found\", clientID))\n\t}\n\tif store.isRetired(clientID) {\n\t\tpanic(ufmt.Sprintf(\"client %s is retired already\", clientID))\n\t}\n\tstore.retired.Set(clientID, true)\n\tchain.Emit(EventTypeClientRetired, \"client_id\", clientID)\n}\n\n// IsClientRetired reports whether RetireClient retired clientID.\nfunc IsClientRetired(clientID string) bool {\n\treturn store.isRetired(clientID)\n}\n\n// RetireClientPort retires a client for one port only: every packet message\n// naming the client with a payload on port is refused from now on, while\n// its other ports, client updates and counterparty stay. A route\n// replacement retires the old client for the transfer port while the\n// governance channel's epoch still runs over it (ADR pr6375 §7.12, the\n// two-phase epoch switch); RetireClient retires it for good once IBCAdmin's\n// epoch notice moves the channel. Governance only.\nfunc RetireClientPort(cur realm, clientID, port string) {\n\tassertGovernance(cur, \"retire_client_port\")\n\tif store.getClient(clientID) == nil {\n\t\tpanic(ufmt.Sprintf(\"client %s not found\", clientID))\n\t}\n\tif store.isRetiredFor(clientID, port) {\n\t\tpanic(ufmt.Sprintf(\"client %s is retired for port %s already\", clientID, port))\n\t}\n\tstore.retired.Set(clientID+\"|\"+port, true)\n\tchain.Emit(EventTypeClientRetired, \"client_id\", clientID, \"port\", port)\n}\n\n// IsClientRetiredAny reports whether clientID is retired, or retired for any\n// port (RetireClientPort): such a client takes no new role.\nfunc IsClientRetiredAny(clientID string) bool {\n\treturn store.isRetiredAny(clientID)\n}\n\n// IsClientRetiredFor reports whether clientID is retired, or retired for\n// port (RetireClientPort).\nfunc IsClientRetiredFor(clientID, port string) bool {\n\treturn store.isRetiredFor(clientID, port)\n}\n\n// ensureNotRetired panics if clientID was retired.\nfunc ensureNotRetired(clientID string) {\n\tif store.isRetired(clientID) {\n\t\tpanic(ufmt.Sprintf(\"client %s is retired\", clientID))\n\t}\n}\n\n// ensureNotRetiredFor panics if clientID was retired, or retired for the\n// local port of a payload (its source port when sent from here, its\n// destination port when received).\nfunc ensureNotRetiredFor(clientID string, payloads []types.Payload, received bool) {\n\tensureNotRetired(clientID)\n\tfor _, p := range payloads {\n\t\tport := p.SourcePort\n\t\tif received {\n\t\t\tport = p.DestinationPort\n\t\t}\n\t\tif store.isRetiredFor(clientID, port) {\n\t\t\tpanic(ufmt.Sprintf(\"client %s is retired for port %s\", clientID, port))\n\t\t}\n\t}\n}\n\n// ensureAuthorizedRelayer panics if the EOA that signed the tx is not in the\n// relayer whitelist. If the whitelist is empty any EOA is allowed. Returns the\n// relayer's EOA address.\n//\n// Unlike the authority above, this gate is bound to the transaction signer\n// (unsafe.OriginCaller): relaying is a high-frequency, per-key operation that\n// the relayer may legitimately route through its own MsgRun scripts, and a\n// relayer key never acts through governance. Trade-off: the signer can relay\n// through any intermediate realm, the same tx.origin risk class as v1's\n// runtime.OriginCaller.\nfunc ensureAuthorizedRelayer() address {\n\tcaller := unsafe.OriginCaller()\n\tif relayers.Size() == 0 {\n\t\treturn caller\n\t}\n\tif !relayers.Has(string(caller)) {\n\t\tpanic(ufmt.Sprintf(\"unauthorized relayer %s\", caller))\n\t}\n\treturn caller\n}\n\n// ensureRelayerFor is ensureAuthorizedRelayer for a message whose proof is\n// checked against clientID. Relaying for an Ethereum client is permissionless\n// (ADR pr6375 R17): its updates, receives, acknowledgements and timeouts are\n// all proof-checked, and a refund must never wait on a listed relayer, so the\n// whitelist does not apply. Any other client keeps the whitelist.\nfunc ensureRelayerFor(clientID string) address {\n\tif c := store.getClient(clientID); c != nil \u0026\u0026 c.typ == ethereum.ClientType {\n\t\treturn unsafe.OriginCaller()\n\t}\n\treturn ensureAuthorizedRelayer()\n}\n"},{"name":"app.gno","body":"package core\n\nimport (\n\t\"chain\"\n\t\"strings\"\n\n\t\"gno.land/p/aib/ibc/app/v0\"\n\t\"gno.land/p/aib/ibc/host/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// trustedAppPrefix is this project's realm namespace. Only the owner of the\n// `aib` namespace can deploy under it (r/sys/names), so a registration coming\n// from there activates at once; anything else waits for the authority.\nconst trustedAppPrefix = \"gno.land/r/aib/\"\n\nconst (\n\tEventTypeAppRegistered = \"app_registered\"\n\tEventTypeAppPending    = \"app_registration_pending\"\n\tEventTypeAppApproved   = \"app_approved\"\n\tEventTypeAppRejected   = \"app_rejected\"\n\tEventTypeAppPaused     = \"app_paused\"\n\tEventTypeAppUnpaused   = \"app_unpaused\"\n)\n\n// RegisterApp registers an IBCApp for portID. The app is identified by the\n// calling realm, hence it must be a realm. A registration from this project's\n// namespace (trustedAppPrefix) is active immediately. Any other realm's\n// registration is only recorded as pending, routes nothing, and is activated\n// by a governance decision with ApproveApp (ADR pr6375 R14: approving an app\n// loosens, so it waits for the decision's delay in ibcgov). Pending\n// registrations claim no\n// port: several candidates may wait for the same one, and none can block a\n// trusted app. A registration for a port that is already active is refused.\nfunc RegisterApp(cur realm, portID string, a app.IBCApp) {\n\tcaller := cur.Previous()\n\tif caller.IsUser() {\n\t\tpanic(\"RegisterApp can only be called from an other realm\")\n\t}\n\tif err := host.PortIdentifierValidator(portID); err != nil {\n\t\tpanic(ufmt.Errorf(\"invalid portID: %v\", err))\n\t}\n\tif _, ok := store.routes[portID]; ok {\n\t\tpanic(ufmt.Errorf(\"portID %s has already been registered\", portID))\n\t}\n\tentry := App{\n\t\tIBCApp:  a,\n\t\tPkgPath: caller.PkgPath(),\n\t\tAddress: caller.Address(),\n\t}\n\tif strings.HasPrefix(entry.PkgPath, trustedAppPrefix) {\n\t\tstore.routes[portID] = entry\n\t\tchain.Emit(EventTypeAppRegistered, \"port\", portID, \"pkg_path\", entry.PkgPath)\n\t\treturn\n\t}\n\tstore.pendingApps.Set(pendingKey(portID, entry.PkgPath), entry)\n\tchain.Emit(EventTypeAppPending, \"port\", portID, \"pkg_path\", entry.PkgPath)\n}\n\n// ApproveApp activates the pending registration of pkgPath for portID.\n// Governance only.\nfunc ApproveApp(cur realm, portID, pkgPath string) {\n\tassertGovernance(cur, \"approve_app\")\n\tkey := pendingKey(portID, pkgPath)\n\tv := store.pendingApps.Get(key)\n\tif v == nil {\n\t\tpanic(ufmt.Sprintf(\"no pending registration of %s for port %s\", pkgPath, portID))\n\t}\n\tif _, ok := store.routes[portID]; ok {\n\t\tpanic(ufmt.Errorf(\"portID %s has already been registered\", portID))\n\t}\n\tstore.routes[portID] = v.(App)\n\tstore.pendingApps.Remove(key)\n\tchain.Emit(EventTypeAppApproved, \"port\", portID, \"pkg_path\", pkgPath)\n}\n\n// RejectApp drops the pending registration of pkgPath for portID. Authority\n// members only (GovDAO through proposals.gno, or ibcgov). The storage deposit the entry held is refunded to the sender\n// of this transaction, as for any deletion.\nfunc RejectApp(cur realm, portID, pkgPath string) {\n\trejectApp(0, cur, portID, pkgPath)\n}\n\nfunc rejectApp(_ int, rlm realm, portID, pkgPath string) {\n\tauth.Assert(0, rlm, \"reject_app\")\n\tkey := pendingKey(portID, pkgPath)\n\tif !store.pendingApps.Has(key) {\n\t\tpanic(ufmt.Sprintf(\"no pending registration of %s for port %s\", pkgPath, portID))\n\t}\n\tstore.pendingApps.Remove(key)\n\tchain.Emit(EventTypeAppRejected, \"port\", portID, \"pkg_path\", pkgPath)\n}\n\n// AppPkgPath returns the realm registered on portID, or \"\" if the port is\n// free.\nfunc AppPkgPath(portID string) string {\n\ta, ok := store.routes[portID]\n\tif !ok {\n\t\treturn \"\"\n\t}\n\treturn a.PkgPath\n}\n\n// IsAppPending reports whether pkgPath has a pending registration for portID.\nfunc IsAppPending(portID, pkgPath string) bool {\n\treturn store.pendingApps.Has(pendingKey(portID, pkgPath))\n}\n\n// pendingKey joins a port and a package path. ':' is not a valid port\n// identifier character (host.IsValidID), so the key is unambiguous.\nfunc pendingKey(portID, pkgPath string) string {\n\treturn portID + \":\" + pkgPath\n}\n\n// PauseApp suspends the application registered on portID: SendPacket refuses\n// payloads for that port, so a user's send fails and nothing moves, and\n// RecvPacket answers incoming packets for it with the error acknowledgement\n// without invoking the app, so the counterparty refunds its senders. The\n// Acknowledgement and Timeout callbacks keep running, so refunds already owed\n// by the app are never held back. Authority members only; see proposals.gno for\n// the GovDAO path, and Pause for suspending the whole packet path instead.\nfunc PauseApp(cur realm, portID string) {\n\tpauseApp(0, cur, portID)\n}\n\nfunc pauseApp(_ int, rlm realm, portID string) {\n\tauth.Assert(0, rlm, \"pause_app\")\n\tsetAppPaused(portID, true)\n\tchain.Emit(EventTypeAppPaused, \"port\", portID)\n}\n\n// UnpauseApp resumes the application registered on portID. Governance only.\nfunc UnpauseApp(cur realm, portID string) {\n\tassertGovernance(cur, \"unpause_app\")\n\tsetAppPaused(portID, false)\n\tchain.Emit(EventTypeAppUnpaused, \"port\", portID)\n}\n\n// IsAppPaused reports whether the application on portID is suspended. False\n// for an unregistered port.\nfunc IsAppPaused(portID string) bool {\n\ta, ok := store.routes[portID]\n\treturn ok \u0026\u0026 a.Paused\n}\n\nfunc setAppPaused(portID string, paused bool) {\n\ta, ok := store.routes[portID]\n\tif !ok {\n\t\tpanic(ufmt.Sprintf(\"no registered app for port %s\", portID))\n\t}\n\ta.Paused = paused\n\tstore.routes[portID] = a\n}\n"},{"name":"client.gno","body":"package core\n\nimport (\n\t\"gno.land/p/aib/ibc/lightclient/v0\"\n\t\"gno.land/p/aib/ibc/types/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// The client entry points below are thin dispatchers to the active\n// implementation (see upgrade.gno and core.gno).\n\n// CreateClient generates a new client identifier and initializes the light\n// client with the given states. Relayers only; the relayer becomes the\n// client's creator.\nfunc CreateClient(cur realm, clientState lightclient.ClientState, consensusState lightclient.ConsensusState) string {\n\trelayer := ensureAuthorizedRelayer()\n\treturn mustImpl().CreateClient(0, cur, relayer, clientState, consensusState)\n}\n\n// ClientIDs returns the list of known client identifiers, sorted lexically by\n// id (the underlying b+tree order). Intended for read-only callers (UIs,\n// other realms building txlinks).\nfunc ClientIDs() []string {\n\treturn store.ClientIDs()\n}\n\n// ClientLatestHeight returns the latest (highest) consensus height the given\n// client has been updated to, formatted as \"\u003crevision\u003e/\u003cheight\u003e\". This is the\n// client's latest trusted height. Returns \"\" if the client is unknown.\n// Intended for read-only callers (UIs) that want to surface how up-to-date a\n// client is.\nfunc ClientLatestHeight(clientID string) string {\n\tc := store.getClient(clientID)\n\tif c == nil {\n\t\treturn \"\"\n\t}\n\treturn c.lightClient.LatestHeight().String()\n}\n\n// ClientStatus returns the status of the given client (Active, Expired,\n// Frozen, ...). Returns \"\" if the client is unknown. Intended for read-only\n// callers (UIs) that want to surface whether a client is usable.\nfunc ClientStatus(clientID string) string {\n\tc := store.getClient(clientID)\n\tif c == nil {\n\t\treturn \"\"\n\t}\n\treturn c.lightClient.Status()\n}\n\n// RegisterCounterparty will register the IBC v2 counterparty info for the\n// given clientID. It must be called by the same relayer that called\n// CreateClient.\nfunc RegisterCounterparty(cur realm, clientID string, counterpartyMerklePrefix [][]byte, counterpartyClientID string) {\n\tensureNotRetired(clientID)\n\tmustImpl().RegisterCounterparty(0, cur, clientID, counterpartyMerklePrefix, counterpartyClientID)\n}\n\n// UpdateClient will update the given IBC v2 light client with a new header.\n// Can also be used to submit a misbehavior (clientMessage can be a header or\n// a misbehavior). Relayers only, except for an Ethereum client, whose updates\n// anyone may submit (see ensureRelayerFor): a refund needs a fresh client.\nfunc UpdateClient(cur realm, clientID string, clientMessage lightclient.ClientMessage) {\n\tensureNotRetired(clientID)\n\tensureRelayerFor(clientID)\n\tc := store.getClient(clientID)\n\tfrozen := c != nil \u0026\u0026 c.lightClient.Status() == lightclient.Frozen\n\tmustImpl().UpdateClient(0, cur, clientID, clientMessage)\n\t// A misbehaviour that froze the client ends its trust generation.\n\tif c != nil \u0026\u0026 !frozen \u0026\u0026 c.lightClient.Status() == lightclient.Frozen {\n\t\tc.trustGen++\n\t}\n}\n\n// UpgradeClient upgrades the client to a new client and consensus state,\n// verified by proofs that the counterparty chain committed to those states\n// at its UpgradePath under the current client's latest consensus root.\n// Relayers only.\nfunc UpgradeClient(cur realm, clientID string, clientState, consensusState, proofUpgradeClient, proofUpgradeConsensusState any) {\n\tensureNotRetired(clientID)\n\tensureAuthorizedRelayer()\n\tmustImpl().UpgradeClient(0, cur, clientID, clientState, consensusState, proofUpgradeClient, proofUpgradeConsensusState)\n\tnewTrustGeneration(clientID)\n}\n\n// RecoverClient recovers a frozen or expired subject client using a healthy\n// substitute client that tracks the same counterparty chain. Governance only\n// (ADR pr6375 R14): a recovery trusts the substitute, so it waits for its\n// decision's delay in ibcgov.\nfunc RecoverClient(cur realm, subjectClientID, substituteClientID string) {\n\tassertGovernance(cur, \"recover_client\")\n\tensureNotRetired(subjectClientID)\n\tensureNotRetired(substituteClientID)\n\tmustImpl().RecoverClient(0, cur, subjectClientID, substituteClientID)\n\tnewTrustGeneration(subjectClientID)\n}\n\n// newTrustGeneration starts a new trust generation of clientID's verifier,\n// after a call that changed whom it trusts. The core does it itself rather\n// than leave it to the implementation, which an upgrade replaces.\nfunc newTrustGeneration(clientID string) {\n\tif c := store.getClient(clientID); c != nil {\n\t\tc.trustGen++\n\t}\n}\n\n// ClientTrustGeneration is clientID's trust generation (0 if unknown): it\n// grows on each recovery, freeze, upgrade, migration and substitution of the\n// verifier, so an outcome proven through the client is settled on only while\n// the generation it was proven under is current (ADR pr6375 §5.4).\nfunc ClientTrustGeneration(clientID string) uint64 {\n\tc := store.getClient(clientID)\n\tif c == nil {\n\t\treturn 0\n\t}\n\treturn c.trustGen\n}\n\n// recoveryChecker is implemented by a light client that tells, without a\n// change, why it would refuse a recovery from substitute.\ntype recoveryChecker interface {\n\tCheckRecovery(substitute lightclient.Interface) error\n}\n\n// CheckRecoverClient is why RecoverClient(subjectClientID, substituteClientID)\n// would fail now, or \"\": every check of the core and of the subject's light\n// client, with no change. A governed recovery that would fail is refused (and\n// its decision spent) instead of aborting and staying pending.\nfunc CheckRecoverClient(subjectClientID, substituteClientID string) string {\n\tsubject, substitute := store.getClient(subjectClientID), store.getClient(substituteClientID)\n\tswitch {\n\tcase subjectClientID == substituteClientID:\n\t\treturn \"the subject and the substitute are one client\"\n\tcase subject == nil || substitute == nil:\n\t\treturn \"unknown client\"\n\tcase store.isRetired(subjectClientID) || store.isRetired(substituteClientID):\n\t\treturn \"a retired client is never recovered or used to recover\"\n\tcase subject.Type() != substitute.Type():\n\t\treturn \"the subject and the substitute are of different types\"\n\t}\n\tif status := subject.LightClient().Status(); status != lightclient.Frozen \u0026\u0026 status != lightclient.Expired {\n\t\treturn \"client \" + subjectClientID + \" is \" + string(status) + \", not frozen or expired\"\n\t}\n\tif status := substitute.LightClient().Status(); status != lightclient.Active {\n\t\treturn \"substitute \" + substituteClientID + \" is \" + string(status) + \", not Active\"\n\t}\n\tif rc, ok := subject.LightClient().(recoveryChecker); ok {\n\t\tif err := rc.CheckRecovery(substitute.LightClient()); err != nil {\n\t\t\treturn err.Error()\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// ClientMigrationChecker is implemented by a ClientMigrator that tells,\n// without a change, why a migration would fail.\ntype ClientMigrationChecker interface {\n\tCheckMigrateClient(clientID, migration string, params []byte) error\n}\n\n// CheckMigrateClient is why MigrateClient(clientID, migration, params) would\n// fail now, or \"\", with no change: a governed migration that would fail is\n// refused (and its decision spent) instead of aborting and staying pending.\nfunc CheckMigrateClient(clientID, migration string, params []byte) string {\n\tif store.getClient(clientID) == nil {\n\t\treturn \"unknown client \" + clientID\n\t}\n\tif store.isRetired(clientID) {\n\t\treturn \"client \" + clientID + \" is retired\"\n\t}\n\tm, ok := mustImpl().(ClientMigrationChecker)\n\tif !ok {\n\t\treturn \"the active implementation \" + implVersion + \" cannot migrate clients\"\n\t}\n\tif err := m.CheckMigrateClient(clientID, migration, params); err != nil {\n\t\treturn err.Error()\n\t}\n\treturn \"\"\n}\n\n// ClientMigrator is implemented by an implementation that can replace the\n// verifier of a live client while keeping its id, its consensus states and\n// its packet state (ADR pr6375 R16): for an Ethereum client, a new fork\n// schedule before a fork, or a conversion to a new light-client package\n// version whose containers changed. migration names the conversion and\n// params carries its arguments in the wire format it documents.\n//\n// MigrateClient returns why it refused, with nothing changed, or nil once\n// the migration is installed: every failure it can tell before installing\n// (a converted verifier failing its checks included) is a refusal, so the\n// governed decision is spent rather than left pending.\ntype ClientMigrator interface {\n\tMigrateClient(_ int, rlm realm, clientID, migration string, params []byte) error\n}\n\n// MigrateClient migrates the verifier of clientID (see ClientMigrator) and\n// returns \"\" once it is installed, or why it was refused, nothing changed.\n// Governance only. Panics if the active implementation migrates nothing.\nfunc MigrateClient(cur realm, clientID, migration string, params []byte) string {\n\tassertGovernance(cur, \"migrate_client\")\n\tensureNotRetired(clientID)\n\tm, ok := mustImpl().(ClientMigrator)\n\tif !ok {\n\t\tpanic(\"core: the active implementation \" + implVersion + \" cannot migrate clients\")\n\t}\n\tif err := m.MigrateClient(0, cur, clientID, migration, params); err != nil {\n\t\treturn err.Error()\n\t}\n\tnewTrustGeneration(clientID)\n\treturn \"\"\n}\n\n// ClientType returns the type of clientID (\"07-tendermint\", \"ethereum\", ...),\n// or \"\" if the client is unknown.\nfunc ClientType(clientID string) string {\n\tc := store.getClient(clientID)\n\tif c == nil {\n\t\treturn \"\"\n\t}\n\treturn c.typ\n}\n\n// CounterpartyClientID returns the counterparty client id registered for\n// clientID, or \"\" if none (or the client is unknown).\nfunc CounterpartyClientID(clientID string) string {\n\tc := store.getClient(clientID)\n\tif c == nil {\n\t\treturn \"\"\n\t}\n\treturn c.counterpartyClientID\n}\n\n// PacketCommitment returns a copy of the commitment stored for a packet sent\n// on clientID, or nil.\nfunc PacketCommitment(clientID string, sequence uint64) []byte {\n\tc := store.getClient(clientID)\n\tif c == nil {\n\t\treturn nil\n\t}\n\treturn cloneBytes(c.PacketCommitment(sequence))\n}\n\n// PacketReceipt returns a copy of the receipt stored for a packet received on\n// clientID (types.CommitPacketReceipt of the packet), or nil.\nfunc PacketReceipt(clientID string, sequence uint64) []byte {\n\tc := store.getClient(clientID)\n\tif c == nil {\n\t\treturn nil\n\t}\n\treturn cloneBytes(c.PacketReceipt(sequence))\n}\n\n// PacketAcknowledgement returns a copy of the acknowledgement commitment\n// written for a packet received on clientID, or nil (none yet, or an\n// asynchronous acknowledgement still unwritten).\nfunc PacketAcknowledgement(clientID string, sequence uint64) []byte {\n\tc := store.getClient(clientID)\n\tif c == nil {\n\t\treturn nil\n\t}\n\treturn cloneBytes(c.PacketAcknowledgement(sequence))\n}\n\n// ClientLatestTimestamp is the counterparty time (unix seconds) of\n// clientID's latest consensus state, and whether it is known: a packet sent\n// over the client must time out after it.\nfunc ClientLatestTimestamp(clientID string) (uint64, bool) {\n\tc := store.getClient(clientID)\n\tif c == nil {\n\t\treturn 0, false\n\t}\n\tlc := c.LightClient()\n\tt, err := lc.TimestampAtHeight(lc.LatestHeight())\n\tif err != nil {\n\t\treturn 0, false\n\t}\n\treturn t, true\n}\n\n// ClientTimestampAt is the counterparty time (unix seconds) of clientID's\n// consensus state at height.\nfunc ClientTimestampAt(clientID string, height types.Height) (uint64, error) {\n\tc := store.getClient(clientID)\n\tif c == nil {\n\t\treturn 0, ufmt.Errorf(\"client %s not found\", clientID)\n\t}\n\treturn c.LightClient().TimestampAtHeight(height)\n}\n\n// valueProver is implemented by a light client that reads a counterparty\n// commitment's value from a proof (the Ethereum client).\ntype valueProver interface {\n\tProvenValue(height types.Height, proof any, path types.MerklePath) ([]byte, error)\n}\n\n// CounterpartyValue is the value proof establishes for key in clientID's\n// counterparty store at height (zero bytes when none is stored), the key\n// prefixed with the counterparty's merkle prefix as registered: what a\n// reconciliation reads of the destination's receipt and acknowledgement\n// (ADR pr6375 §7.12). It changes nothing.\nfunc CounterpartyValue(clientID string, height types.Height, proof any, key []byte) ([]byte, error) {\n\tc := store.getClient(clientID)\n\tif c == nil {\n\t\treturn nil, ufmt.Errorf(\"client %s not found\", clientID)\n\t}\n\t// Only a healthy verifier speaks for the counterparty: not frozen,\n\t// expired or retired (ADR pr6375 §5.4 \"Proven outcomes\").\n\tif store.isRetired(clientID) {\n\t\treturn nil, ufmt.Errorf(\"client %s is retired\", clientID)\n\t}\n\tif status := c.LightClient().Status(); status != lightclient.Active {\n\t\treturn nil, ufmt.Errorf(\"client %s is %s, not Active\", clientID, status)\n\t}\n\tvp, ok := c.LightClient().(valueProver)\n\tif !ok {\n\t\treturn nil, ufmt.Errorf(\"client %s cannot read a counterparty value\", clientID)\n\t}\n\treturn vp.ProvenValue(height, proof, types.BuildMerklePath(c.CounterpartyMerklePrefix(), key))\n}\n\n// HasPendingAsyncAck reports whether a packet received on clientID waits for\n// its asynchronous acknowledgement.\nfunc HasPendingAsyncAck(clientID string, sequence uint64) bool {\n\tc := store.getClient(clientID)\n\treturn c != nil \u0026\u0026 c.hasPendingAsyncAck(sequence)\n}\n\n// PendingAsyncAck returns a copy of the packet received on clientID that\n// waits for its asynchronous acknowledgement, the block height at which it\n// was received, and whether one waits.\nfunc PendingAsyncAck(clientID string, sequence uint64) (packet types.Packet, height int64, ok bool) {\n\tc := store.getClient(clientID)\n\tif c == nil {\n\t\treturn types.Packet{}, 0, false\n\t}\n\tp, _, found := c.PendingAsyncAck(sequence)\n\tif !found {\n\t\treturn types.Packet{}, 0, false\n\t}\n\tout := p\n\tout.Payloads = make([]types.Payload, len(p.Payloads))\n\tfor i, pl := range p.Payloads {\n\t\tpl.Value = cloneBytes(pl.Value)\n\t\tout.Payloads[i] = pl\n\t}\n\treturn out, c.pendingAsyncAckHeight(sequence), true\n}\n\nfunc cloneBytes(b []byte) []byte {\n\tif b == nil {\n\t\treturn nil\n\t}\n\treturn append([]byte{}, b...)\n}\n"},{"name":"core.gno","body":"package core\n\nimport (\n\t\"gno.land/p/aib/ibc/types/v0\"\n)\n\n// The packet entry points below are thin dispatchers to the active\n// implementation (see upgrade.gno): the gates that must survive any\n// implementation run here, then the call is forwarded non-crossing with this\n// realm's own cur, so the current realm stays this proxy.\n\n// SendPacket validates msg, stores the packet commitment, emits send_packet\n// and invokes each payload app's OnSendPacket. Returns the packet sequence.\nfunc SendPacket(cur realm, msg types.MsgSendPacket) (sequence uint64) {\n\tensureNotPaused()\n\tensureNotRetiredFor(msg.SourceClient, msg.Payloads, false)\n\treturn mustImpl().SendPacket(0, cur, msg)\n}\n\n// RecvPacket verifies the packet commitment proof against the destination\n// client, writes the receipt, invokes the app's OnRecvPacket and writes the\n// acknowledgement, unless the app answers asynchronously (see\n// WriteAcknowledgement). Relayers only, except for an Ethereum client, whose\n// relaying is permissionless (see ensureRelayerFor).\nfunc RecvPacket(cur realm, msg types.MsgRecvPacket) types.ResponseResultType {\n\tensureNotPaused()\n\tensureNotRetiredFor(msg.Packet.DestinationClient, msg.Packet.Payloads, true)\n\tensureRelayerFor(msg.Packet.DestinationClient)\n\treturn mustImpl().RecvPacket(0, cur, msg)\n}\n\n// WriteAcknowledgement writes the acknowledgement of a packet whose\n// OnRecvPacket returned an async status. Only the realm that registered the\n// app for that packet may call it. A retired client still takes it for a\n// packet it received before its retirement: the app finalizes the receipt it\n// already holds, so the packet reaches a terminal state before the\n// reconciliation (ADR pr6375 §7.12); nothing new is received there.\nfunc WriteAcknowledgement(cur realm, clientID string, sequence uint64, ack types.Acknowledgement) {\n\tensureNotPaused()\n\tif !HasPendingAsyncAck(clientID, sequence) {\n\t\tensureNotRetired(clientID)\n\t}\n\tmustImpl().WriteAcknowledgement(0, cur, clientID, sequence, ack)\n}\n\n// Acknowledgement verifies the acknowledgement proof against the source\n// client, deletes the packet commitment and invokes the app's\n// OnAcknowledgementPacket. Relayers only, except for an Ethereum client (see\n// ensureRelayerFor): a refund never waits on a listed relayer.\nfunc Acknowledgement(cur realm, msg types.MsgAcknowledgement) types.ResponseResultType {\n\tensureNotPaused()\n\tensureNotRetiredFor(msg.Packet.SourceClient, msg.Packet.Payloads, false)\n\tensureRelayerFor(msg.Packet.SourceClient)\n\treturn mustImpl().Acknowledgement(0, cur, msg)\n}\n\n// Timeout verifies the receipt absence proof against the source client,\n// deletes the packet commitment and invokes the app's OnTimeoutPacket.\n// Relayers only, except for an Ethereum client (see ensureRelayerFor).\nfunc Timeout(cur realm, msg types.MsgTimeout) types.ResponseResultType {\n\tensureNotPaused()\n\tensureNotRetiredFor(msg.Packet.SourceClient, msg.Packet.Payloads, false)\n\tensureRelayerFor(msg.Packet.SourceClient)\n\treturn mustImpl().Timeout(0, cur, msg)\n}\n"},{"name":"emit.gno","body":"package core\n\nimport (\n\t\"chain\"\n\t\"encoding/hex\"\n\n\t\"gno.land/p/aib/ibc/types/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// The Emit* functions below are the single source of truth for the IBC event\n// schema. chain.Emit stamps the event with the package whose code contains the\n// call, so the implementation emits through them and every event carries\n// pkg_path=corePkgPath whichever implementation is active. They are exported\n// because the implementation lives in another package, hence the\n// assertProxyRealm guard: a foreign realm cannot forge IBC events under this\n// realm's pkg_path.\n\nfunc EmitCreateClient(_ int, rlm realm, clientID, clientType, consensusHeights string) {\n\tassertProxyRealm(0, rlm)\n\tchain.Emit(types.EventTypeCreateClient,\n\t\ttypes.AttributeKeyClientID, clientID,\n\t\ttypes.AttributeKeyClientType, clientType,\n\t\ttypes.AttributeKeyConsensusHeights, consensusHeights,\n\t)\n}\n\nfunc EmitUpdateClient(_ int, rlm realm, clientID, clientType, consensusHeights string) {\n\tassertProxyRealm(0, rlm)\n\tchain.Emit(types.EventTypeUpdateClient,\n\t\ttypes.AttributeKeyClientID, clientID,\n\t\ttypes.AttributeKeyClientType, clientType,\n\t\ttypes.AttributeKeyConsensusHeights, consensusHeights,\n\t)\n}\n\nfunc EmitSubmitMisbehaviour(_ int, rlm realm, clientID, clientType string) {\n\tassertProxyRealm(0, rlm)\n\tchain.Emit(types.EventTypeSubmitMisbehaviour,\n\t\ttypes.AttributeKeyClientID, clientID,\n\t\ttypes.AttributeKeyClientType, clientType,\n\t)\n}\n\nfunc EmitUpgradeClient(_ int, rlm realm, clientID, clientType, consensusHeight string) {\n\tassertProxyRealm(0, rlm)\n\tchain.Emit(types.EventTypeUpgradeClient,\n\t\ttypes.AttributeKeyClientID, clientID,\n\t\ttypes.AttributeKeyClientType, clientType,\n\t\ttypes.AttributeKeyConsensusHeight, consensusHeight,\n\t)\n}\n\nfunc EmitRecoverClient(_ int, rlm realm, subjectClientID, substituteClientID, clientType string) {\n\tassertProxyRealm(0, rlm)\n\tchain.Emit(types.EventTypeRecoverClient,\n\t\ttypes.AttributeKeySubjectClientID, subjectClientID,\n\t\ttypes.AttributeKeySubstituteClientID, substituteClientID,\n\t\ttypes.AttributeKeyClientType, clientType,\n\t)\n}\n\n// EventTypeMigrateClient is the event of a client migration (MigrateClient).\nconst EventTypeMigrateClient = \"migrate_client\"\n\nfunc EmitMigrateClient(_ int, rlm realm, clientID, clientType, migration string) {\n\tassertProxyRealm(0, rlm)\n\tchain.Emit(EventTypeMigrateClient,\n\t\ttypes.AttributeKeyClientID, clientID,\n\t\ttypes.AttributeKeyClientType, clientType,\n\t\t\"migration\", migration,\n\t)\n}\n\nfunc EmitSendPacket(_ int, rlm realm, packet types.Packet) {\n\tassertProxyRealm(0, rlm)\n\temitPacketEvent(types.EventTypeSendPacket, packet)\n}\n\nfunc EmitRecvPacket(_ int, rlm realm, packet types.Packet) {\n\tassertProxyRealm(0, rlm)\n\temitPacketEvent(types.EventTypeRecvPacket, packet)\n}\n\nfunc EmitAcknowledgePacket(_ int, rlm realm, packet types.Packet) {\n\tassertProxyRealm(0, rlm)\n\temitPacketEvent(types.EventTypeAcknowledgePacket, packet)\n}\n\nfunc EmitTimeoutPacket(_ int, rlm realm, packet types.Packet) {\n\tassertProxyRealm(0, rlm)\n\temitPacketEvent(types.EventTypeTimeoutPacket, packet)\n}\n\nfunc EmitWriteAck(_ int, rlm realm, packet types.Packet, ack types.Acknowledgement) {\n\tassertProxyRealm(0, rlm)\n\tchain.Emit(types.EventTypeWriteAck,\n\t\ttypes.AttributeKeySrcClient, packet.SourceClient,\n\t\ttypes.AttributeKeyDstClient, packet.DestinationClient,\n\t\ttypes.AttributeKeySequence, ufmt.Sprintf(\"%d\", packet.Sequence),\n\t\ttypes.AttributeKeyTimeoutTimestamp, ufmt.Sprintf(\"%d\", packet.TimeoutTimestamp),\n\t\ttypes.AttributeKeyEncodedPacketHex, hex.EncodeToString(packet.ProtoMarshal()),\n\t\ttypes.AttributeKeyEncodedAckHex, hex.EncodeToString(ack.ProtoMarshal()),\n\t)\n}\n\n// emitPacketEvent emits the shape shared by the send, recv, acknowledge and\n// timeout packet events (they differ only by event type).\nfunc emitPacketEvent(eventType string, packet types.Packet) {\n\tchain.Emit(eventType,\n\t\ttypes.AttributeKeySrcClient, packet.SourceClient,\n\t\ttypes.AttributeKeyDstClient, packet.DestinationClient,\n\t\ttypes.AttributeKeySequence, ufmt.Sprintf(\"%d\", packet.Sequence),\n\t\ttypes.AttributeKeyTimeoutTimestamp, ufmt.Sprintf(\"%d\", packet.TimeoutTimestamp),\n\t\ttypes.AttributeKeyEncodedPacketHex, hex.EncodeToString(packet.ProtoMarshal()),\n\t)\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/aib/ibc/core\"\ngno = \"0.9\"\n"},{"name":"proposals.gno","body":"package core\n\nimport \"gno.land/r/gov/dao\"\n\n// GovDAO is a member of the authority (p/aib/authority.GovDAOPath) but cannot\n// call a realm directly: it executes a callback. The constructors below build\n// the ProposalRequest whose callback, declared in this realm, runs the gated\n// operation through its non-crossing helper; inside the callback cur.Previous()\n// is gno.land/r/gov/dao, so the gate sees the DAO's address. The callbacks are\n// unexported closures capturing their arguments, so this realm exports no bare\n// func(realm) error that the DAO could be made to invoke on someone else's\n// behalf.\n//\n// Only the tightening operations have a direct GovDAO path: they take effect\n// at once and can only stop things. Every loosening operation (unpausing,\n// relayers, recovery, approvals, upgrades, migrations, membership) is a\n// decision of gno.land/r/aib/ibc/ibcgov, with its delay (ADR pr6375 R14).\n//\n// A GovDAO member submits one with MsgRun, members vote, anyone executes:\n//\n//\tdao.MustCreateProposal(cross(cur), core.NewPauseProposalRequest(cross(cur)))\n//\tgnokey maketx call -pkgpath gno.land/r/gov/dao -func MustVoteOnProposalSimple -args \u003cpid\u003e -args YES ...\n//\tgnokey maketx call -pkgpath gno.land/r/gov/dao -func ExecuteProposal -args \u003cpid\u003e ...\n//\n// A failing callback panics, which aborts the execution transaction and leaves\n// the proposal executable again once the cause is fixed.\n\nconst proposalTitlePrefix = \"gno.land/r/aib/ibc/core: \"\n\nfunc newProposalRequest(_ int, rlm realm, title, description string, cb func(cur realm) error) dao.ProposalRequest {\n\treturn dao.NewProposalRequest(proposalTitlePrefix+title, description, dao.NewSimpleExecutor(0, rlm, cb, \"\"))\n}\n\n// NewPauseProposalRequest proposes to suspend the packet path.\nfunc NewPauseProposalRequest(cur realm) dao.ProposalRequest {\n\treturn newProposalRequest(0, cur, \"pause\",\n\t\t\"Suspend the IBC core packet path (SendPacket, RecvPacket, WriteAcknowledgement, Acknowledgement, Timeout). Client operations keep working. Resuming it is a decision of gno.land/r/aib/ibc/ibcgov.\",\n\t\tfunc(cur realm) error { pause(0, cur); return nil })\n}\n\n// NewRejectAppProposalRequest proposes to drop a pending app registration.\nfunc NewRejectAppProposalRequest(cur realm, portID, pkgPath string) dao.ProposalRequest {\n\treturn newProposalRequest(0, cur, \"reject app \"+pkgPath+\" on port \"+portID,\n\t\t\"Drop the pending registration of the realm \"+pkgPath+\" for port \"+portID+\" of the IBC core.\",\n\t\tfunc(cur realm) error { rejectApp(0, cur, portID, pkgPath); return nil })\n}\n\n// NewPauseAppProposalRequest proposes to suspend the application on portID.\nfunc NewPauseAppProposalRequest(cur realm, portID string) dao.ProposalRequest {\n\treturn newProposalRequest(0, cur, \"pause app \"+portID,\n\t\t\"Suspend the IBC application registered on port \"+portID+\": sends for that port are refused and incoming packets are answered with an error acknowledgement, so counterparties refund their senders. Acknowledgement and timeout callbacks keep running. Resuming it is a decision of gno.land/r/aib/ibc/ibcgov.\",\n\t\tfunc(cur realm) error { pauseApp(0, cur, portID); return nil })\n}\n"},{"name":"recover-client.md","body":"# RecoverClient\n\n`core.RecoverClient` is the governance escape hatch that revives a client that\nhas become unusable — either **Frozen** (valid misbehaviour was submitted via\n`UpdateClient`) or **Expired** (no valid header was submitted within the\n`TrustingPeriod`) — by copying state from a healthy **substitute** client that\ntracks the same counterparty chain.\n\n```gno\ncore.RecoverClient(cross, subjectClientID, substituteClientID string)\n```\n\nOnly the governance realm `gno.land/r/aib/ibc/ibcgov` can call it (see\n`admin.gno` and the README's Authority section): a recovery trusts the\nsubstitute, so it is a decision with ibcgov's 7-day delay\n(`ibcgov.NewRecoverClientRequest`), made by the governor before its yield and\nby GovDAO after.\n\n## End-to-end flow\n\n### 1. A client becomes unusable\n\n- **Frozen**: a relayer submitted valid misbehaviour via `UpdateClient` (two\n  conflicting signed headers for the same chain). The client's `FrozenHeight`\n  becomes non-zero and `Status()` returns `Frozen`.\n- **Expired**: no valid header was submitted within `TrustingPeriod`, so\n  `Status()` returns `Expired` because the latest consensus state's timestamp\n  is too old.\n\nFrom this point `SendPacket`, `RecvPacket`, `Acknowledgement`, `Timeout` and\n`UpdateClient` all panic for this client. Any in-flight user packets are\nstuck, and any inbound packets cannot be acknowledged. Channels using this\nclient are frozen on this side until the client is recovered.\n\n### 2. Off-chain coordination\n\nStakeholders agree to recover rather than migrate to a brand-new client.\nRecovery is preferable because it preserves the client ID, packet\ncommitments / receipts / acknowledgements, counterparty registration and\nchannel state — users don't need to migrate anything.\n\n### 3. Create a substitute client\n\nA relayer calls `core.CreateClient` with a fresh, **Active** client targeting\nthe *same counterparty chain*. The substitute must satisfy\n`isMatchingClientState` with the subject, i.e. these fields must match:\n\n- `TrustLevel`\n- `UnbondingPeriod`\n- `MaxClockDrift`\n- `ProofSpecs`\n- `UpgradePath`\n\nThe following are allowed to differ and are **adopted from the substitute** by\nthe subject during recovery:\n\n- `ChainID` (typically the same, but the code supports a change — for example\n  a genesis-restart on a new chain ID tracking the same state). `ChainID` and\n  `LatestHeight` are always adopted together, so their revision numbers stay\n  aligned: `ClientState.ValidateBasic` requires\n  `LatestHeight.RevisionNumber == ParseChainID(ChainID)`, and since both sides\n  of that equality come from the substitute (which passed `ValidateBasic` at\n  `CreateClient`), the invariant is preserved on the subject post-recovery.\n- `LatestHeight`\n- `TrustingPeriod` — this is the parameter-tweaking knob: if the original\n  `TrustingPeriod` was set too aggressively (and partly caused the expiry),\n  governance can choose a larger value on the substitute and that new value is\n  copied into the subject. Same mechanism as ibc-go.\n- `FrozenHeight` (always reset to zero)\n\n### 4. (Optional) Fast-forward the substitute\n\nRelayers call `UpdateClient(substituteID, header)` until the substitute's\n`LatestHeight` is at the desired recovery height. The substitute must be\n`Active` at the moment recovery executes.\n\n### 5. Governance proposal\n\nA proposal asks to run:\n\n```gno\ncore.RecoverClient(cross, subjectID, substituteID)\n```\n\nGated on the governance realm; the decision is built with\n`ibcgov.NewRecoverClientRequest(cross(cur), subjectID, substituteID)`.\n\n### 6. `RecoverClient` executes\n\n`r/aib/ibc/core/client.gno`:\n\n1. `assertGovernance(cur, \"recover_client\")`: the caller must be\n   `gno.land/r/aib/ibc/ibcgov`; neither client may be retired.\n2. Subject and substitute IDs must differ; both must resolve; `typ` must match.\n3. Subject status ∈ {`Frozen`, `Expired`}; substitute status must be `Active`.\n4. Delegates to `subject.lightClient.RecoverClient(substitute.lightClient)`.\n\n`p/aib/ibc/lightclient/tendermint/v0/tendermint.gno`:\n\n1. Type-assert substitute to `*TMLightClient`.\n2. `isMatchingClientState` check.\n3. Fetch `substitute.GetConsensusState(substitute.LatestHeight)`.\n4. Copy into subject: `ChainID`, `LatestHeight`, `TrustingPeriod`; reset\n   `FrozenHeight`.\n5. Store the substitute's consensus state at the substitute's latest height in\n   the subject.\n\n### 7. Post-recovery state\n\n- Subject's `Status() == Active`. `UpdateClient`, packet verification, etc.\n  resume.\n- **Packet commitments, receipts, acknowledgements, `sendSeq`,\n  `counterpartyClientID`, `counterpartyMerklePrefix` are untouched** — that is\n  the point: channels keep working with their existing identifiers and\n  in-flight state.\n- Pre-recovery consensus states remain in the subject's tree but are below the\n  new `LatestHeight` and are not used to verify new packets.\n- The substitute client is **not** deleted and remains `Active`. It can be\n  reused for a future recovery or left idle.\n- `recover_client` event is emitted.\n\n### 8. Counterparty side (symmetric)\n\nIf the counterparty chain's client tracking this chain is also Frozen/Expired\n(common when misbehaviour or a long halt affects both sides), the counterparty\nruns its own governance-level recovery. Packet relaying cannot resume on that\npath until both sides are `Active`.\n\n### 9. Relayer resumes\n\nOnce both sides are `Active`, relayers submit headers via `UpdateClient` and\nthe normal packet lifecycle resumes. No re-`RegisterCounterparty`, no new\nchannel.\n\n## Changing parameters during recovery\n\nBecause the substitute's `TrustingPeriod` and `ChainID` are adopted by the\nsubject, creating the substitute is also the opportunity to adjust those\nparameters through the same governance action:\n\n- **Lengthening `TrustingPeriod`** to reduce the risk of future expiry — for\n  example after learning that the counterparty's block production is slower\n  than originally assumed.\n- **Adopting a new `ChainID`** after a counterparty genesis restart that kept\n  the same consensus state tree — the subject starts verifying headers signed\n  under the new chain ID without being migrated to a new client ID.\n\nOther parameters (`TrustLevel`, `UnbondingPeriod`, `MaxClockDrift`,\n`ProofSpecs`, `UpgradePath`) **cannot** be changed by recovery — the match\ncheck rejects the substitute. Changing those requires `UpgradeClient` (or a\nfresh client migration).\n\n## Caveats\n\n- The substitute's `LatestHeight` is not required to be greater than the\n  subject's. Same as ibc-go — nothing enforces a \"forward\" recovery, though in\n  practice the substitute is always ahead.\n- Only the substitute's consensus state **at its `LatestHeight`** is copied\n  into the subject. Earlier substitute consensus states are not migrated.\n- Recovery does not reset packet sequences or clear commitments — those are\n  packet-layer concerns and stay intact.\n"},{"name":"render.gno","body":"package core\n\n// Render is forwarded to the implementation: the pages for humans and the\n// JSON routes alike. Forwarding the JSON routes rather than freezing them\n// here keeps them upgradeable with the verifier they read (a frozen\n// consensus-state route could not follow a migration to a new light-client\n// type). The routes the relayer consumes, listed on Logic.Render, are a\n// compatibility contract pinned by the z0c filetest.\nfunc Render(path string) string {\n\tif impl == nil {\n\t\treturn \"# IBC core\\n\\nNo implementation installed.\\n\"\n\t}\n\treturn impl.Render(path)\n}\n"},{"name":"store.gno","body":"package core\n\nimport (\n\t\"chain/params\"\n\t\"chain/runtime\"\n\n\t\"gno.land/p/aib/ibc/app/v0\"\n\t\"gno.land/p/aib/ibc/host/v0\"\n\t\"gno.land/p/aib/ibc/lightclient/v0\"\n\t\"gno.land/p/aib/ibc/types/v0\"\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)\n\n// The state of the IBC core lives in this realm forever (see ADR 0001): the\n// implementation realm that runs the lifecycle logic only holds a reference\n// to it, obtained through RegisterImpl, and reads and writes it through the\n// Store and Client interfaces below. Every mutator takes the proxy's own live\n// realm value and asserts it (assertProxyRealm): the implementation is always\n// invoked non-crossing by the dispatchers, so the value it forwards is this\n// realm's cur, and nobody else can present one. The proxy keeps the only\n// params.SetBytes calls, inside the commitment, receipt and acknowledgement\n// setters, which is what keeps the packet state provable under\n// vm:gno.land/r/aib/ibc/core: across upgrades.\n\n// Store is the proxy-owned state as seen by an implementation.\ntype Store interface {\n\t// AddClient allocates the next client identifier for typ and stores a\n\t// client bound to the verifier newLightClient returns. The constructor is\n\t// called here so the verifier is allocated in this realm: a /p/-declared\n\t// function such as tendermint.NewTMLightClient runs with the caller's\n\t// storage authority, whereas an object the implementation allocated\n\t// itself would belong to the implementation's realm.\n\tAddClient(_ int, rlm realm, typ string, creator address, newLightClient func() lightclient.Interface) Client\n\t// Client returns the client with the given identifier, or nil.\n\tClient(id string) Client\n\t// ClientIDs returns the known client identifiers, sorted lexically.\n\tClientIDs() []string\n\t// Route returns the application registered on portID; panics if none.\n\tRoute(portID string) App\n\t// Ports returns the ports with an active application, in registration\n\t// order.\n\tPorts() []string\n\t// Clients is a read-only view of the clients keyed by id; values are\n\t// Client.\n\tClients() bptree.ITree\n\t// PendingApps is a read-only view of the registrations awaiting\n\t// approval, keyed by port:pkgPath; values are App.\n\tPendingApps() bptree.ITree\n}\n\n// Client is a stored client as seen by an implementation.\ntype Client interface {\n\tID() string\n\tType() string\n\tCreator() address\n\tCounterpartyClientID() string\n\tCounterpartyMerklePrefix() [][]byte\n\tSetCounterparty(_ int, rlm realm, clientID string, merklePrefix [][]byte)\n\tLightClient() lightclient.Interface\n\t// SetLightClient replaces the verifier with convert(current), for a\n\t// migration to a new light-client version. As for AddClient, convert must\n\t// be a /p/-declared function so the new verifier is allocated here.\n\tSetLightClient(_ int, rlm realm, convert func(current lightclient.Interface) lightclient.Interface)\n\t// TrustGeneration counts the verifier's recoveries, freezes, upgrades,\n\t// migrations and substitutions: the core starts a new generation on each.\n\tTrustGeneration() uint64\n\t// AllocateSendSequence allocates and returns the next send sequence.\n\tAllocateSendSequence(_ int, rlm realm) uint64\n\t// NextSequenceSend returns the sequence AllocateSendSequence would return\n\t// next, without allocating it.\n\tNextSequenceSend() uint64\n\n\tPacketCommitment(sequence uint64) []byte\n\t// PacketCommitments is a read-only view keyed by seqid binary; values\n\t// are []byte. Same for PacketReceipts and PacketAcknowledgements.\n\tPacketCommitments() bptree.ITree\n\tSetPacketCommitment(_ int, rlm realm, packet types.Packet)\n\tDeletePacketCommitment(_ int, rlm realm, sequence uint64)\n\n\tHasPacketReceipt(sequence uint64) bool\n\t// PacketReceipt returns the receipt stored at sequence\n\t// (types.CommitPacketReceipt of the received packet), or nil.\n\tPacketReceipt(sequence uint64) []byte\n\tPacketReceipts() bptree.ITree\n\t// SetPacketReceipt stores types.CommitPacketReceipt(packet) at the\n\t// packet's sequence, so the receipt identifies the packet (ADR pr6375\n\t// R19).\n\tSetPacketReceipt(_ int, rlm realm, packet types.Packet)\n\tDeletePacketReceipt(_ int, rlm realm, sequence uint64)\n\n\tHasPacketAcknowledgement(sequence uint64) bool\n\tPacketAcknowledgements() bptree.ITree\n\tPacketAcknowledgement(sequence uint64) []byte\n\tSetPacketAcknowledgement(_ int, rlm realm, sequence uint64, ackHash []byte)\n\tDeletePacketAcknowledgement(_ int, rlm realm, sequence uint64)\n\n\tPendingAsyncAck(sequence uint64) (packet types.Packet, appPkgPath string, found bool)\n\tSavePendingAsyncAck(_ int, rlm realm, packet types.Packet, appPkgPath string)\n\tDeletePendingAsyncAck(_ int, rlm realm, sequence uint64)\n}\n\n// App is a registered application: the IBCApp value plus the realm that\n// registered it and its pause flag.\ntype App struct {\n\tapp.IBCApp\n\tPkgPath string\n\tAddress address\n\t// Paused is set by PauseApp: SendPacket refuses the port and RecvPacket\n\t// answers with the error acknowledgement without invoking the app.\n\tPaused bool\n}\n\nvar store *coreStore\n\nfunc init() {\n\tstore = newStore()\n}\n\ntype coreStore struct {\n\tclientSeq  seqid.ID\n\tclientByID *bptree.BPTree // id:client\n\n\troutes map[string]App\n\t// pendingApps holds registrations from realms outside this project's\n\t// namespace until the authority approves them (see app.gno). Keyed by\n\t// pendingKey(port, pkgPath); a B+ tree, because anyone may add to it and\n\t// a map would be loaded whole on every access.\n\tpendingApps *bptree.BPTree // port:pkgPath -\u003e App\n\t// retired holds the clients RetireClient retired, and under\n\t// clientID|port the ports RetireClientPort retired a client for (see\n\t// admin.gno; an ICS24 identifier holds no '|').\n\tretired *bptree.BPTree // clientID or clientID|port -\u003e true\n}\n\nvar _ Store = (*coreStore)(nil)\n\nfunc newStore() *coreStore {\n\treturn \u0026coreStore{\n\t\tclientByID:  bptree.NewBPTree32(),\n\t\troutes:      make(map[string]App),\n\t\tpendingApps: bptree.NewBPTree32(),\n\t\tretired:     bptree.NewBPTree32(),\n\t}\n}\n\nfunc (cs *coreStore) isRetired(clientID string) bool {\n\treturn cs.retired.Has(clientID)\n}\n\n// isRetiredAny reports whether clientID is retired, or retired for any port\n// ('}' follows '|': the keys clientID|port sort below clientID}).\nfunc (cs *coreStore) isRetiredAny(clientID string) bool {\n\tif cs.retired.Has(clientID) {\n\t\treturn true\n\t}\n\tfound := false\n\tcs.retired.Iterate(clientID+\"|\", clientID+\"}\", func(string, any) bool {\n\t\tfound = true\n\t\treturn true\n\t})\n\treturn found\n}\n\n// isRetiredFor reports whether clientID is retired, or retired for port.\nfunc (cs *coreStore) isRetiredFor(clientID, port string) bool {\n\treturn cs.retired.Has(clientID) || cs.retired.Has(clientID+\"|\"+port)\n}\n\ntype client struct {\n\tid                       string\n\ttyp                      string\n\tcreator                  address\n\tcounterpartyClientID     string\n\tcounterpartyMerklePrefix [][]byte\n\n\tlightClient lightclient.Interface\n\n\tsendSeq                     seqid.ID\n\tpacketCommitmentsBySeq      *bptree.BPTree // sequence:commitment\n\tpacketReceiptsBySeq         *bptree.BPTree // sequence:receipt\n\tpacketAcknowledgementsBySeq *bptree.BPTree // sequence:ack\n\tpendingAsyncAcksBySeq       *bptree.BPTree // sequence:pendingAsyncAck\n\t// trustGen counts the changes of whom the verifier trusts: a recovery,\n\t// a freeze, an upgrade, a migration or a substitution of the verifier\n\t// each start a new generation, and an outcome proven under an older one\n\t// is never settled on (ADR pr6375 §5.4 \"Proven outcomes\").\n\ttrustGen uint64\n}\n\nvar _ Client = (*client)(nil)\n\ntype pendingAsyncAck struct {\n\tpacket     types.Packet\n\tappPkgPath string\n\theight     int64 // the block height of the receive\n}\n\nfunc (cs *coreStore) AddClient(_ int, rlm realm, typ string, creator address, newLightClient func() lightclient.Interface) Client {\n\tassertProxyRealm(0, rlm)\n\tif newLightClient == nil {\n\t\tpanic(\"AddClient: nil light client constructor\")\n\t}\n\tlc := newLightClient()\n\tif lc == nil {\n\t\tpanic(\"AddClient: nil light client\")\n\t}\n\treturn cs.addClient(typ, creator, lc)\n}\n\nfunc (cs *coreStore) addClient(typ string, creator address, lc lightclient.Interface) *client {\n\tid := ufmt.Sprintf(\"%s-%d\", typ, uint64(cs.clientSeq.Next()))\n\tc := \u0026client{\n\t\tid:          id,\n\t\ttyp:         typ,\n\t\tcreator:     creator,\n\t\tlightClient: lc,\n\n\t\tpacketCommitmentsBySeq:      bptree.NewBPTree32(),\n\t\tpacketReceiptsBySeq:         bptree.NewBPTree32(),\n\t\tpacketAcknowledgementsBySeq: bptree.NewBPTree32(),\n\t\tpendingAsyncAcksBySeq:       bptree.NewBPTree32(),\n\t}\n\tcs.clientByID.Set(id, c)\n\treturn c\n}\n\nfunc (cs *coreStore) Client(id string) Client {\n\tc := cs.getClient(id)\n\tif c == nil {\n\t\treturn nil\n\t}\n\treturn c\n}\n\nfunc (cs *coreStore) getClient(id string) *client {\n\tcli := cs.clientByID.Get(id)\n\tif cli == nil {\n\t\treturn nil\n\t}\n\treturn cli.(*client)\n}\n\nfunc (cs *coreStore) ClientIDs() []string {\n\tids := make([]string, 0, cs.clientByID.Size())\n\tcs.clientByID.IterateByOffset(0, cs.clientByID.Size(), func(k string, _ any) bool {\n\t\tids = append(ids, k)\n\t\treturn false\n\t})\n\treturn ids\n}\n\nfunc (cs *coreStore) Clients() bptree.ITree     { return readOnlyTree{cs.clientByID} }\nfunc (cs *coreStore) PendingApps() bptree.ITree { return readOnlyTree{cs.pendingApps} }\n\nfunc (cs *coreStore) Ports() []string {\n\tports := make([]string, 0, len(cs.routes))\n\tfor port := range cs.routes {\n\t\tports = append(ports, port)\n\t}\n\treturn ports\n}\n\nfunc (cs *coreStore) Route(portID string) App {\n\ta, ok := cs.routes[portID]\n\tif !ok {\n\t\tpanic(ufmt.Sprintf(\"no registered app for port %s\", portID))\n\t}\n\treturn a\n}\n\nfunc (c *client) ID() string                         { return c.id }\nfunc (c *client) Type() string                       { return c.typ }\nfunc (c *client) Creator() address                   { return c.creator }\nfunc (c *client) CounterpartyClientID() string       { return c.counterpartyClientID }\nfunc (c *client) CounterpartyMerklePrefix() [][]byte { return copyPrefix(c.counterpartyMerklePrefix) }\nfunc (c *client) LightClient() lightclient.Interface { return c.lightClient }\n\nfunc (c *client) SetCounterparty(_ int, rlm realm, clientID string, merklePrefix [][]byte) {\n\tassertProxyRealm(0, rlm)\n\tc.counterpartyClientID = clientID\n\t// Both levels are copied: the caller's slice stays the caller's, and a\n\t// later change to it must not move the client's proof namespace.\n\tc.counterpartyMerklePrefix = copyPrefix(merklePrefix)\n}\n\n// copyPrefix deep-copies a merkle prefix into this realm's storage.\nfunc copyPrefix(p [][]byte) [][]byte {\n\tif p == nil {\n\t\treturn nil\n\t}\n\tout := make([][]byte, len(p))\n\tfor i, e := range p {\n\t\tout[i] = append([]byte{}, e...)\n\t}\n\treturn out\n}\n\n// copyPacket deep-copies a packet (its payloads and their values).\nfunc copyPacket(p types.Packet) types.Packet {\n\tout := p\n\tout.Payloads = make([]types.Payload, len(p.Payloads))\n\tfor i, pl := range p.Payloads {\n\t\tpl.Value = append([]byte{}, pl.Value...)\n\t\tout.Payloads[i] = pl\n\t}\n\treturn out\n}\n\nfunc (c *client) SetLightClient(_ int, rlm realm, convert func(lightclient.Interface) lightclient.Interface) {\n\tassertProxyRealm(0, rlm)\n\tif convert == nil {\n\t\tpanic(\"SetLightClient: nil converter\")\n\t}\n\tlc := convert(c.lightClient)\n\tif lc == nil {\n\t\tpanic(\"SetLightClient: nil light client\")\n\t}\n\tc.lightClient = lc\n\tc.trustGen++\n}\n\n// TrustGeneration is the verifier's trust generation (see client.trustGen).\nfunc (c *client) TrustGeneration() uint64 { return c.trustGen }\n\nfunc (c *client) AllocateSendSequence(_ int, rlm realm) uint64 {\n\tassertProxyRealm(0, rlm)\n\treturn uint64(c.sendSeq.Next())\n}\n\nfunc (c *client) NextSequenceSend() uint64 {\n\treturn uint64(c.sendSeq) + 1\n}\n\nfunc (c *client) PacketCommitments() bptree.ITree { return readOnlyTree{c.packetCommitmentsBySeq} }\n\nfunc (c *client) PacketCommitment(sequence uint64) []byte {\n\tv := c.packetCommitmentsBySeq.Get(seqid.ID(sequence).Binary())\n\tif v == nil {\n\t\treturn nil\n\t}\n\treturn v.([]byte)\n}\n\n// SetPacketCommitment stores the packet commitment in the client data and in\n// the params.\nfunc (c *client) SetPacketCommitment(_ int, rlm realm, packet types.Packet) {\n\tassertProxyRealm(0, rlm)\n\tcommitment := types.CommitPacket(packet)\n\n\t// store in client data\n\tc.packetCommitmentsBySeq.Set(seqid.ID(packet.Sequence).Binary(), commitment)\n\n\t// store in params, so it is provable by the counterparty\n\t// The key will be: (abci path=.store/main/key)\n\t// \"/pv/vm:gno.land/r/aib/ibc/core:\" + clientID + \"1\" + sequence\n\tkey := host.PacketCommitmentKey(c.id, packet.Sequence)\n\tsetChainParam(key, commitment)\n}\n\nfunc (c *client) DeletePacketCommitment(_ int, rlm realm, sequence uint64) {\n\tassertProxyRealm(0, rlm)\n\tc.packetCommitmentsBySeq.Remove(seqid.ID(sequence).Binary())\n\n\tkey := host.PacketCommitmentKey(c.id, sequence)\n\tsetChainParam(key, nil)\n}\n\nfunc (c *client) PacketReceipts() bptree.ITree { return readOnlyTree{c.packetReceiptsBySeq} }\n\n// HasPacketReceipt returns true if the packet receipt exists, otherwise false.\nfunc (c *client) HasPacketReceipt(sequence uint64) bool {\n\treturn c.packetReceiptsBySeq.Has(seqid.ID(sequence).Binary())\n}\n\n// PacketReceipt returns the receipt stored at sequence, or nil.\nfunc (c *client) PacketReceipt(sequence uint64) []byte {\n\tv := c.packetReceiptsBySeq.Get(seqid.ID(sequence).Binary())\n\tif v == nil {\n\t\treturn nil\n\t}\n\treturn v.([]byte)\n}\n\n// SetPacketReceipt writes the packet receipt under the receipt path, the\n// public path the IBC v2 specification standardizes (ICS24). ibc-go stores the\n// constant 0x02 there; this core stores types.CommitPacketReceipt(packet), a\n// sha256 commitment to the sequence, the source client and the packet's IBC v2\n// commitment (destination client, timeout, payloads), so that the receipt\n// says which packet occupies the sequence (ADR pr6375 R19). Counterparties\n// only prove a receipt's absence (timeouts), which the value does not affect.\nfunc (c *client) SetPacketReceipt(_ int, rlm realm, packet types.Packet) {\n\tassertProxyRealm(0, rlm)\n\tvalue := types.CommitPacketReceipt(packet)\n\n\t// store in client data\n\tc.packetReceiptsBySeq.Set(seqid.ID(packet.Sequence).Binary(), value)\n\n\t// store in params, so it is provable by the counterparty\n\t// The key will be: (abci path=.store/main/key)\n\t// \"/pv/vm:gno.land/r/aib/ibc/core:\" + clientID + \"2\" + sequence\n\tkey := host.PacketReceiptKey(c.id, packet.Sequence)\n\tsetChainParam(key, value)\n}\n\nfunc (c *client) DeletePacketReceipt(_ int, rlm realm, sequence uint64) {\n\tassertProxyRealm(0, rlm)\n\tc.packetReceiptsBySeq.Remove(seqid.ID(sequence).Binary())\n\n\tkey := host.PacketReceiptKey(c.id, sequence)\n\tsetChainParam(key, nil)\n}\n\nfunc (c *client) PacketAcknowledgements() bptree.ITree {\n\treturn readOnlyTree{c.packetAcknowledgementsBySeq}\n}\n\n// HasPacketAcknowledgement checks if the packet ack hash is already on the\n// store.\nfunc (c *client) HasPacketAcknowledgement(sequence uint64) bool {\n\treturn c.packetAcknowledgementsBySeq.Has(seqid.ID(sequence).Binary())\n}\n\n// PacketAcknowledgement fetches the packet acknowledgement from the store.\nfunc (c *client) PacketAcknowledgement(sequence uint64) []byte {\n\tv := c.packetAcknowledgementsBySeq.Get(seqid.ID(sequence).Binary())\n\tif v == nil {\n\t\treturn nil\n\t}\n\treturn v.([]byte)\n}\n\n// SetPacketAcknowledgement writes the acknowledgement hash under the\n// acknowledgement path\n// This is a public path that is standardized by the IBC V2 specification.\nfunc (c *client) SetPacketAcknowledgement(_ int, rlm realm, sequence uint64, ackHash []byte) {\n\tassertProxyRealm(0, rlm)\n\t// store in client data\n\tc.packetAcknowledgementsBySeq.Set(seqid.ID(sequence).Binary(), ackHash)\n\n\t// store in params, so it is provable by the counterparty\n\t// The key will be: (abci path=.store/main/key)\n\t// \"/pv/vm:gno.land/r/aib/ibc/core:\" + clientID + \"3\" + sequence\n\tkey := host.PacketAcknowledgementKey(c.id, sequence)\n\tsetChainParam(key, ackHash)\n}\n\nfunc (c *client) DeletePacketAcknowledgement(_ int, rlm realm, sequence uint64) {\n\tassertProxyRealm(0, rlm)\n\tc.packetAcknowledgementsBySeq.Remove(seqid.ID(sequence).Binary())\n\n\tkey := host.PacketAcknowledgementKey(c.id, sequence)\n\tsetChainParam(key, nil)\n}\n\n// pendingAsyncAckHeight is the block height at which the packet waiting for\n// its asynchronous acknowledgement at sequence was received, 0 if none.\nfunc (c *client) pendingAsyncAckHeight(sequence uint64) int64 {\n\tv := c.pendingAsyncAcksBySeq.Get(seqid.ID(sequence).Binary())\n\tif v == nil {\n\t\treturn 0\n\t}\n\treturn v.(*pendingAsyncAck).height\n}\n\nfunc (c *client) PendingAsyncAck(sequence uint64) (types.Packet, string, bool) {\n\tv := c.pendingAsyncAcksBySeq.Get(seqid.ID(sequence).Binary())\n\tif v == nil {\n\t\treturn types.Packet{}, \"\", false\n\t}\n\tp := v.(*pendingAsyncAck)\n\treturn p.packet, p.appPkgPath, true\n}\n\nfunc (c *client) SavePendingAsyncAck(_ int, rlm realm, packet types.Packet, appPkgPath string) {\n\tassertProxyRealm(0, rlm)\n\tsequence := packet.Sequence\n\tif c.hasPendingAsyncAck(sequence) {\n\t\tpanic(ufmt.Sprintf(\"pending async ack already exists for sequence %d\", sequence))\n\t}\n\tif c.HasPacketAcknowledgement(sequence) {\n\t\tpanic(ufmt.Sprintf(\"acknowledgement already written for sequence %d\", sequence))\n\t}\n\tc.pendingAsyncAcksBySeq.Set(seqid.ID(sequence).Binary(), \u0026pendingAsyncAck{\n\t\tpacket:     copyPacket(packet),\n\t\tappPkgPath: appPkgPath,\n\t\theight:     runtime.ChainHeight(),\n\t})\n}\n\nfunc (c *client) DeletePendingAsyncAck(_ int, rlm realm, sequence uint64) {\n\tassertProxyRealm(0, rlm)\n\tc.pendingAsyncAcksBySeq.Remove(seqid.ID(sequence).Binary())\n}\n\nfunc (c *client) hasPendingAsyncAck(sequence uint64) bool {\n\treturn c.pendingAsyncAcksBySeq.Has(seqid.ID(sequence).Binary())\n}\n\nfunc setChainParam(key, value []byte) {\n\tparams.SetBytes(string(key), value)\n}\n\n// readOnlyTree is the view of a proxy-owned B+ tree handed to the\n// implementation for rendering. Handing the tree itself would hand a write\n// capability: a /p/ method on a foreign receiver runs with the receiver's\n// storage authority, so bptree.Set from the implementation would mutate it.\n// The view implements bptree.ITree, which jsonpage paginates, and panics on\n// the mutators.\ntype readOnlyTree struct {\n\tt *bptree.BPTree\n}\n\nvar _ bptree.ITree = readOnlyTree{}\n\nfunc (r readOnlyTree) Size() int                      { return r.t.Size() }\nfunc (r readOnlyTree) Has(key string) bool            { return r.t.Has(key) }\nfunc (r readOnlyTree) Get(key string) any             { return r.t.Get(key) }\nfunc (r readOnlyTree) GetByIndex(i int) (string, any) { return r.t.GetByIndex(i) }\nfunc (r readOnlyTree) Iterate(start, end string, cb bptree.IterCbFn) bool {\n\treturn r.t.Iterate(start, end, cb)\n}\nfunc (r readOnlyTree) ReverseIterate(start, end string, cb bptree.IterCbFn) bool {\n\treturn r.t.ReverseIterate(start, end, cb)\n}\nfunc (r readOnlyTree) IterateByOffset(offset, count int, cb bptree.IterCbFn) bool {\n\treturn r.t.IterateByOffset(offset, count, cb)\n}\nfunc (r readOnlyTree) ReverseIterateByOffset(offset, count int, cb bptree.IterCbFn) bool {\n\treturn r.t.ReverseIterateByOffset(offset, count, cb)\n}\nfunc (readOnlyTree) Set(string, any) bool      { panic(\"core: read-only tree\") }\nfunc (readOnlyTree) Remove(string) (any, bool) { panic(\"core: read-only tree\") }\n"},{"name":"upgrade-client.md","body":"# UpgradeClient\n\n`core.UpgradeClient` follows a counterparty chain through a **breaking**\nupgrade — an upgrade that `UpdateClient` cannot absorb because the chain\nitself changes shape. Typical triggers:\n\n- `ChainID` change (signed in every header).\n- Revision-number bump (encoded in `Height`, propagates into header\n  signing).\n- Validator-set discontinuity (a hard fork that doesn't preserve the\n  trust graph).\n- Security-parameter changes the client carries — `UnbondingPeriod`,\n  `ProofSpecs`, `UpgradePath`.\n\n```gno\ncore.UpgradeClient(cross, clientID,\n    upgradedClient, upgradedConsState,\n    proofUpgradeClient, proofUpgradeConsState)\n```\n\nGated by `ensureAuthorizedRelayer()`.\n\n## Lifecycle\n\n### 1. Counterparty schedules the upgrade\n\nBefore the upgrade height the counterparty chain commits the upgraded\n`ClientState` and `ConsensusState` to its upgrade store at well-known\npaths derived from the client's `UpgradePath` (the SDK upgrade module\ndefault is `[\"upgrade\", \"upgradedIBCState\"]`):\n\n- `{UpgradePath[0]}/{UpgradePath[1]}/{H}/upgradedClient`\n- `{UpgradePath[0]}/{UpgradePath[1]}/{H}/upgradedConsState`\n\n`H` is the upgrade plan height (just the height number, not a revision\npair). The committed bytes are `cdc.MarshalInterface(...)`-style:\nwrapped in `google.protobuf.Any` with the corresponding type URL.\n\nThe committed `ClientState` has its client-customizable fields\n(`TrustLevel`, `TrustingPeriod`, `MaxClockDrift`, `FrozenHeight`) zeroed\nout — only the chain-specified fields (`ChainID`, `UnbondingPeriod`,\n`LatestHeight`, `ProofSpecs`, `UpgradePath`) are committed. The\ncommitted `ConsensusState` carries a sentinel `Root` because the\nupgraded chain hasn't produced any blocks yet at commit time.\n\n### 2. Relayer reads and submits\n\nOnce the chain has reached the upgrade height, a relayer reads the\ncommitted states and the corresponding ICS-23 membership proofs and\ncalls `UpgradeClient`. Each proof is a chained `[]ics23.CommitmentProof`\nmatching the client's `ProofSpecs` (typically 2 proofs: an IAVL\nexistence proof for the inner key and a multistore existence proof for\nthe store name).\n\n### 3. Verification\n\nThe realm checks:\n\n- `UpgradePath` on the current client is non-empty.\n- `LatestHeight` of the upgraded client is greater than the current\n  latest height.\n- The relayer's submitted client/consensus states reconstruct the bytes\n  the chain committed: the `ClientState` is wrapped in\n  `google.protobuf.Any` with its customizable fields zeroed\n  (`ZeroCustomFields`), the `ConsensusState` is wrapped in `Any` as-is.\n- Both chained proofs verify membership under the current client's\n  latest consensus root.\n\n### 4. State transition\n\nA new `ClientState` is built by mixing fields (see below). The stored\nconsensus state has its `Root` overwritten with the sentinel value so it\ncannot be used to verify packet proofs — by construction. Real roots\narrive afterwards via `UpdateClient` once the new chain is producing\nheaders.\n\n### 5. Resume\n\nSubsequent `UpdateClient` calls bring real headers from the new chain;\npacket flow resumes against those real consensus states.\n\n## Preconditions\n\n- The client exists and the caller is in the relayer whitelist.\n- The client's `UpgradePath` is set.\n- The client's status is `Active`.\n- The upgraded client's `LatestHeight` is greater than the current\n  latest height.\n- Both proofs verify membership of the upgraded client and consensus\n  state at the upgrade path under the current latest consensus root.\n\n## Field mapping\n\nOn success the new `ClientState` is built by combining:\n\n- **Taken from the upgraded client** (chain-specified): `ChainID`,\n  `UnbondingPeriod`, `LatestHeight`, `ProofSpecs`, `UpgradePath`.\n- **Preserved from the current client** (customizable): `TrustLevel`,\n  `TrustingPeriod`, `MaxClockDrift`.\n- **Reset**: `FrozenHeight`.\n\nIf the unbonding period shrank, `TrustingPeriod` is scaled\nproportionally so the security ratio is preserved:\n\n    newTrusting = trusting * newUnbonding / currentUnbonding (truncated)\n\nThe math is performed in seconds; sub-second precision is irrelevant for\ntrusting periods which are measured in days.\n\n## Generating proofs for testing\n\n`cmd/gen-proof` provides an `upgrade` subcommand that mounts an\n`upgrade` IAVL store, commits the upgraded client/consensus state at\nthe SDK upgrade keys (`upgradedIBCState/{H}/upgradedClient`,\n`upgradedIBCState/{H}/upgradedConsState`), and dumps Go literals for the\nchained proofs:\n\n```bash\ngo run -C ./cmd/gen-proof . upgrade\n```\n\nThe output is suitable for embedding in a filetest. See\n`z10c_upgrade_client_filetest.gno` for the full happy-path scenario.\n"},{"name":"upgrade.gno","body":"package core\n\nimport (\n\t\"chain\"\n\t\"strings\"\n\n\t\"gno.land/p/aib/ibc/lightclient/v0\"\n\t\"gno.land/p/aib/ibc/types/v0\"\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// corePkgPath is the permanent package path of this proxy realm. It is part of\n// the on-chain contract with counterparties and relayers: chain params are\n// namespaced \"vm:\u003ccurrent realm\u003e:\u003ckey\u003e\" and events carry the emitting package,\n// so the packet state must always be written, and the events emitted, by this\n// realm (see ADR 0001). That is why the lifecycle logic lives behind the Logic\n// interface in a separate implementation realm, invoked non-crossing, while\n// this realm stays a thin, frozen proxy at this path.\nconst corePkgPath = \"gno.land/r/aib/ibc/core\"\n\n// implPathPrefix is the namespace implementations must live in.\nconst implPathPrefix = corePkgPath + \"/impl/\"\n\nconst (\n\tEventTypeImplUpdated = \"impl_updated\"\n\tAttributeKeyOldPath  = \"old_path\"\n\tAttributeKeyNewPath  = \"new_path\"\n\tAttributeKeyVersion  = \"version\"\n)\n\n// Logic is the swappable client and packet lifecycle implementation. Methods\n// take the non-crossing (_ int, rlm realm, ...) form on purpose: the\n// dispatchers forward this realm's own cur without a realm transition, so the\n// current realm stays corePkgPath throughout. The implementation therefore\n// runs \"as\" this proxy: rlm.Previous() is the proxy's caller, app callbacks\n// are issued with cross(rlm), and the gated store accessors and Emit* wrappers\n// accept rlm.\ntype Logic interface {\n\t// Version identifies the implementation, shown in Render and in the\n\t// impl_updated event.\n\tVersion() string\n\t// OnInstall runs inside UpdateImpl, after the implementation is built\n\t// against the store and before any entry point reaches it. It is the\n\t// migration hook. prevVersion is the version of the implementation that\n\t// was active, which is also the layout of the data as long as versions\n\t// activate in sequence; it is empty at bootstrap. The contract:\n\t//   - prevVersion is this version: a re-install, do nothing;\n\t//   - prevVersion is the version this one was written to follow: run the\n\t//     migration, at most linear in the number of clients, with anything\n\t//     proportional to packets left to lazy conversion on access;\n\t//   - anything else: panic.\n\t// A panic aborts the whole transaction, so neither the switch nor any\n\t// write the hook already made survives: the previous implementation\n\t// stays active and the candidate stays registered.\n\tOnInstall(_ int, rlm realm, prevPath, prevVersion string)\n\t// Render serves every path: the proxy's Render forwards to it. The JSON\n\t// routes the relayer reads through vm/qrender (clients, clients/{id},\n\t// its consensus states, packet commitments and receipts) are a\n\t// compatibility contract: their shape is pinned by the z0c filetest and\n\t// changing it means a relayer release.\n\tRender(path string) string\n\n\tCreateClient(_ int, rlm realm, relayer address, clientState lightclient.ClientState, consensusState lightclient.ConsensusState) string\n\tRegisterCounterparty(_ int, rlm realm, clientID string, counterpartyMerklePrefix [][]byte, counterpartyClientID string)\n\tUpdateClient(_ int, rlm realm, clientID string, clientMessage lightclient.ClientMessage)\n\tUpgradeClient(_ int, rlm realm, clientID string, clientState, consensusState, proofUpgradeClient, proofUpgradeConsensusState any)\n\tRecoverClient(_ int, rlm realm, subjectClientID, substituteClientID string)\n\tSendPacket(_ int, rlm realm, msg types.MsgSendPacket) (sequence uint64)\n\tRecvPacket(_ int, rlm realm, msg types.MsgRecvPacket) types.ResponseResultType\n\tWriteAcknowledgement(_ int, rlm realm, clientID string, sequence uint64, ack types.Acknowledgement)\n\tAcknowledgement(_ int, rlm realm, msg types.MsgAcknowledgement) types.ResponseResultType\n\tTimeout(_ int, rlm realm, msg types.MsgTimeout) types.ResponseResultType\n}\n\nvar (\n\t// impl is the active implementation; nil until the first registration.\n\timpl        Logic\n\timplPath    string\n\timplVersion string\n\t// implCtors holds the registered constructors keyed by implementation\n\t// path. A constructor receives the proxy-owned store so a swapped\n\t// implementation reads and writes the same persistent state: clients,\n\t// commitments, receipts and acknowledgements survive an upgrade.\n\timplCtors = bptree.NewBPTree32() // path -\u003e func(Store) Logic\n\t// implAddrs indexes every registered implementation by its realm\n\t// address, superseded ones included (a registry only grows): bridge\n\t// membership is one keyed lookup (IsImplAddress), whatever the number\n\t// registered.\n\timplAddrs = bptree.NewBPTree32() // realm address -\u003e path\n\t// implManifests holds what each registered implementation declared\n\t// about itself, store-free: the preflight reads it and never builds a\n\t// candidate (ADR 0004, R14).\n\timplManifests = bptree.NewBPTree32() // path -\u003e manifest\n)\n\n// manifest is what an implementation declares about itself when it\n// registers, as plain values. CheckUpdateImpl checks it, then builds the\n// candidate against a fresh, empty store (never the live one) and checks the\n// built version against it: candidate code runs in the preflight, but nothing\n// is handed the live store before a governed installation. The installation\n// checks the built implementation against it again.\ntype manifest struct {\n\t// Version is the Version() the implementation reports.\n\tVersion string\n\t// Over lists the versions of an active implementation it installs over\n\t// (\"\" at bootstrap); it always re-installs over its own version. nil\n\t// installs over any.\n\tOver []string\n}\n\n// accepts reports whether m installs over an active implementation of\n// version prev.\nfunc (m manifest) accepts(prev string) bool {\n\tif m.Over == nil || prev == m.Version {\n\t\treturn true\n\t}\n\tfor _, v := range m.Over {\n\t\tif v == prev {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// newManifest builds a manifest in this realm's storage: the caller's slice\n// stays the caller's.\nfunc newManifest(version string, over []string) manifest {\n\tm := manifest{Version: version}\n\tif over != nil {\n\t\tm.Over = append([]string{}, over...)\n\t}\n\treturn m\n}\n\n// assertProxyRealm guards the exported store mutators and Emit* wrappers. The\n// active implementation is always invoked non-crossing by the dispatchers, so\n// the realm value it forwards is this realm's own live cur: rlm.IsCurrent() is\n// true and rlm.PkgPath() == corePkgPath. A foreign realm cannot satisfy both\n// predicates, and a stale copy of this realm's cur fails the first.\nfunc assertProxyRealm(_ int, rlm realm) {\n\tif !rlm.IsCurrent() || rlm.PkgPath() != corePkgPath {\n\t\tpanic(\"core: access denied for non-proxy realm \" + rlm.PkgPath())\n\t}\n}\n\n// RegisterImpl records an implementation constructor. It must be called from\n// a realm under corePkgPath + \"/impl/\" (e.g. gno.land/r/aib/ibc/core/impl/v1),\n// normally from that realm's init, so the set of installable implementations\n// is confined to this realm's own namespace, which only the owner of the aib\n// namespace can deploy under. Registration alone activates nothing, except\n// for the very first one while no implementation is installed (bootstrap):\n// genesis needs no extra transaction and tests only a blank import. Every\n// later switch requires UpdateImpl. version and over are its manifest: the\n// version it reports and the versions of an active implementation it\n// installs over (\"\" at bootstrap; its own version always; nil for any),\n// which the preflight reads instead of building it.\nfunc RegisterImpl(cur realm, ctor func(Store) Logic, version string, over []string) {\n\tcaller := cur.Previous().PkgPath()\n\tif !strings.HasPrefix(caller, implPathPrefix) {\n\t\tpanic(ufmt.Sprintf(\"RegisterImpl: caller %s is not under %s\", caller, implPathPrefix))\n\t}\n\tif ctor == nil {\n\t\tpanic(\"RegisterImpl: nil constructor\")\n\t}\n\tif version == \"\" {\n\t\tpanic(\"RegisterImpl: the manifest names no version\")\n\t}\n\t// A registered implementation is a realm of the bridge: one at an\n\t// address a transfer route pays its fees to would turn that collector\n\t// into one. The collector is changed first, by a governed decision.\n\tif isFeeCollector(chain.PackageAddress(caller)) {\n\t\tpanic(ufmt.Sprintf(\"RegisterImpl: %s is a transfer route's fee collector: change the collector first\", caller))\n\t}\n\timplCtors.Set(caller, ctor)\n\timplManifests.Set(caller, newManifest(version, over))\n\timplAddrs.Set(string(chain.PackageAddress(caller)), caller)\n\tif impl == nil {\n\t\tinstall(0, cur, caller)\n\t}\n}\n\n// UpdateImpl activates a registered implementation, identified by its realm\n// path. Governance only (ADR pr6375 R14): an upgrade is a 7-day decision in\n// ibcgov. The implementation is built against the proxy-owned store and its\n// OnInstall hook runs before the switch, so a panic there leaves the previous\n// implementation active. Rollback is UpdateImpl with the previous path.\nfunc UpdateImpl(cur realm, path string) {\n\tassertGovernance(cur, \"update_impl\")\n\tinstall(0, cur, path)\n}\n\nfunc install(_ int, rlm realm, path string) {\n\tv := implCtors.Get(path)\n\tif v == nil {\n\t\tpanic(ufmt.Sprintf(\"no implementation registered at %s\", path))\n\t}\n\t// Only here, in an authorized installation (bootstrap or a governed\n\t// UpdateImpl), is candidate code given the store.\n\tctor := v.(func(Store) Logic)\n\tl := ctor(store)\n\tif l == nil {\n\t\tpanic(ufmt.Sprintf(\"implementation %s returned a nil Logic\", path))\n\t}\n\tif m := implManifests.Get(path).(manifest); l.Version() != m.Version {\n\t\tpanic(ufmt.Sprintf(\"implementation %s declared version %s, not %s\", path, m.Version, l.Version()))\n\t}\n\t// A panic in the hook unwinds the transaction: nothing below runs and\n\t// nothing the hook wrote persists.\n\tl.OnInstall(0, rlm, implPath, implVersion)\n\toldPath := implPath\n\timpl = l\n\timplPath = path\n\timplVersion = l.Version()\n\tchain.Emit(EventTypeImplUpdated,\n\t\tAttributeKeyOldPath, oldPath,\n\t\tAttributeKeyNewPath, path,\n\t\tAttributeKeyVersion, implVersion,\n\t)\n}\n\n// InstallChecker is implemented by an implementation whose OnInstall\n// refuses some predecessors: CheckInstall is why it would, or nil, as a\n// pure function of the plain values the proxy passes (the active\n// implementation's path and version), so the preflight evaluates exactly\n// what OnInstall will.\ntype InstallChecker interface {\n\tCheckInstall(prevPath, prevVersion string) error\n}\n\n// CheckUpdateImpl is why UpdateImpl(path) would fail now, or \"\", with no\n// change: path is not registered, its manifest does not install over the\n// active version, or the implementation its constructor builds disagrees\n// with its manifest or refuses the active implementation (InstallChecker).\n// A governed switch that would fail is refused (and its decision spent)\n// instead of aborting and staying pending. The candidate is built against a\n// fresh, empty store (newStore), holding no live object and no reference to\n// this realm's store: the live store reaches candidate code only in an\n// authorized installation.\nfunc CheckUpdateImpl(path string) string {\n\tv := implManifests.Get(path)\n\tif v == nil {\n\t\treturn \"no implementation registered at \" + path\n\t}\n\tm := v.(manifest)\n\tif !m.accepts(implVersion) {\n\t\treturn ufmt.Sprintf(\"core impl %s cannot install over version %s\", m.Version, implVersion)\n\t}\n\tl := implCtors.Get(path).(func(Store) Logic)(newStore())\n\tif l == nil {\n\t\treturn \"implementation \" + path + \" returned a nil Logic\"\n\t}\n\tif l.Version() != m.Version {\n\t\treturn ufmt.Sprintf(\"implementation %s declared version %s but builds version %s\", path, m.Version, l.Version())\n\t}\n\tif ic, ok := l.(InstallChecker); ok {\n\t\tif err := ic.CheckInstall(implPath, implVersion); err != nil {\n\t\t\treturn err.Error()\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// ImplPath returns the realm path of the active implementation, \"\" if none.\nfunc ImplPath() string {\n\treturn implPath\n}\n\n// ImplVersion returns the Version() of the active implementation, \"\" if none.\nfunc ImplVersion() string {\n\treturn implVersion\n}\n\n// IsRegisteredImpl reports whether an implementation is registered at path:\n// a keyed lookup.\nfunc IsRegisteredImpl(path string) bool { return implCtors.Get(path) != nil }\n\n// IsImplAddress reports whether a is the realm address of a registered\n// implementation, superseded ones included: a keyed lookup, whatever the\n// number registered.\nfunc IsImplAddress(a address) bool { return implAddrs.Get(string(a)) != nil }\n\n// RegisteredImplPaths returns the paths of the registered implementations,\n// active or not, sorted lexically.\nfunc RegisteredImplPaths() []string {\n\tpaths := make([]string, 0, implCtors.Size())\n\timplCtors.IterateByOffset(0, implCtors.Size(), func(k string, _ any) bool {\n\t\tpaths = append(paths, k)\n\t\treturn false\n\t})\n\treturn paths\n}\n\nfunc mustImpl() Logic {\n\tif impl == nil {\n\t\tpanic(\"core: no implementation installed\")\n\t}\n\treturn impl\n}\n\n// transferPkgPath is the transfer proxy, whose app keeps the route controls'\n// fee collectors.\nconst transferPkgPath = \"gno.land/r/aib/ibc/apps/transfer\"\n\n// isFeeCollector asks the transfer proxy's app, if registered, whether a is\n// a route's fee collector (a read of its index).\nfunc isFeeCollector(a address) bool {\n\tfor _, entry := range store.routes {\n\t\tif entry.PkgPath != transferPkgPath {\n\t\t\tcontinue\n\t\t}\n\t\tif fc, ok := entry.IBCApp.(interface{ IsFeeCollector(address) bool }); ok \u0026\u0026 fc.IsFeeCollector(a) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"},{"name":"verifiers.gno","body":"package core\n\nimport (\n\t\"gno.land/p/aib/ibc/lightclient/v0\"\n)\n\n// VersionedVerifier is a verifier that names its package and version, which\n// a verifier replacement converts from and to (ADR pr6375 R16). The\n// conversions themselves are code of a core implementation, installed by a\n// governed implementation upgrade: no other code converts a verifier.\ntype VersionedVerifier interface {\n\tVerifierVersion() string\n}\n\n// VerifierVersion is the version clientID's verifier names, \"\" if the\n// client is unknown or its verifier names none.\nfunc VerifierVersion(clientID string) string {\n\tc := store.getClient(clientID)\n\tif c == nil {\n\t\treturn \"\"\n\t}\n\treturn verifierVersionOf(c.lightClient)\n}\n\n// IdentifiedVerifier is a verifier that names the history it authenticates,\n// what a route keeps across replacements (ADR pr6375 §7.12): the Ethereum\n// verifier, and any later version embedding it.\ntype IdentifiedVerifier interface {\n\tRouteIdentity() (identity, router string)\n}\n\n// ClientRouteIdentity is the history clientID's verifier authenticates and\n// the router it reads (\"\", \"\" if the client is unknown or its verifier names\n// none): for an Ethereum client its chain id, genesis validators root, ICS26\n// router and commitments slot.\nfunc ClientRouteIdentity(clientID string) (identity, router string) {\n\tc := store.getClient(clientID)\n\tif c == nil {\n\t\treturn \"\", \"\"\n\t}\n\tif v, ok := c.lightClient.(IdentifiedVerifier); ok {\n\t\treturn v.RouteIdentity()\n\t}\n\treturn \"\", \"\"\n}\n\nfunc verifierVersionOf(lc lightclient.Interface) string {\n\tif v, ok := lc.(VersionedVerifier); ok {\n\t\treturn v.VerifierVersion()\n\t}\n\treturn \"\"\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"Lu3k4molzxmJHWIs5iaRhNI5BVHUPD1hhSrfQmD9IoxYgnJfd14+5RbCqPx4WIf4Wy1/1HQ5doNQP61BuQJG9A=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"transfer","path":"gno.land/r/aib/ibc/apps/transfer","files":[{"name":"README.md","body":"# IBC Transfer App\n\nICS-20 Fungible Token Transfer implementation for IBC v2 on Gno. Enables\ncross-chain token transfers between Gno and any IBC v2 compatible chain.\n\n## Transfer\n\n`Transfer` is the single entry point for sending any token to another chain. The\ntoken type is detected automatically from the `denom` argument:\n\n| Token type  | Denom format | Example                   | Mechanism                                                |\n| ----------- | ------------ | ------------------------- | -------------------------------------------------------- |\n| Native coin | plain string | `ugnot`                   | Escrowed from the coins sent with the call               |\n| IBC voucher | `ibc/{hash}` | `ibc/CAEF9C...`           | Burned from sender                                       |\n| GRC20 token | grc20reg key | `gno.land/r/demo/foo.FOO` | Escrowed via `TransferFrom`                              |\n\nSignature:\n\n```gno\nfunc Transfer(\n    cur realm,\n    clientID string,           // IBC client on this chain (e.g. \"07-tendermint-1\")\n    receiver string,           // recipient address on destination chain\n    denom string,              // token denomination (see table above)\n    amount uint256,            // amount to transfer, in base units\n    timeoutTimestamp uint64,   // unix seconds after which the packet times out\n    memo string,               // arbitrary memo included in the IBC packet\n) (packet types.MsgSendPacket, sequence uint64)\n```\n\nWhatever the token, `Transfer` accepts only a direct user call (`gnokey maketx\ncall`): it checks `cur.Previous().IsUserCall()` before classifying the token,\nso calls from intermediate code realms and from `maketx run` ephemeral realms\nare rejected, for vouchers and GRC20 tokens as well as native coins. The\npacket's sender is the transaction's origin caller.\n\n`timeoutTimestamp` is in Unix seconds and the core enforces its bounds: it\nmust be after the current block time and at most 24 hours after it\n(`now \u003c timeout \u003c= now + 24h`), and it must be greater than the latest\nconsensus timestamp the client holds for the counterparty. A literal\ntimestamp in a script goes stale; compute it at submission, e.g.\n`$(( $(date +%s) + 3600 ))` for one hour from now, as the examples below do.\n\n### Native coin\n\nThe coin must be attached to the transaction via the `-send` flag and must match\n`denom` and `amount` (on an Ethereum route, the amount plus the route fee; see\nthe route controls section). `Transfer` reads the attached coins with\n`unsafe.OriginSend()` (`chain/runtime/unsafe`), which is meaningful only\nbecause of the direct-user-call check above: it ensures the coins actually\nlanded at this realm.\n\n```\ngnokey maketx call -pkgpath gno.land/r/aib/ibc/apps/transfer -func Transfer \\\n    -send 100ugnot \\\n    -args \"07-tendermint-1\" -args \"atone1abc...\" -args \"ugnot\" -args \"100\" \\\n    -args \"$(( $(date +%s) + 3600 ))\" -args \"\" \\\n    -gas-fee 1000000ugnot -gas-wanted 90000000 \\\n    -broadcast -chainid gnoland-1 ADDRESS\n```\n\n### IBC voucher\n\nIBC vouchers are GRC20 tokens minted when receiving tokens from another chain.\nThey are burned on transfer back to the source chain.\n\n`Transfer` accepts the local `ibc/{HASH}` spelling and resolves it to the trace\nitself. The counterparty direction is not symmetric: a hand-built ICS-20 payload\n(raw `MsgSendPacket`) has no such resolution step and must carry the expanded\ntrace — see the header of `scripts/transfer-atomone-to-gno.sh`.\n\n```\ngnokey maketx call -pkgpath gno.land/r/aib/ibc/apps/transfer -func Transfer \\\n    -args \"07-tendermint-1\" -args \"atone1abc...\" \\\n    -args \"ibc/CAEF9CA8CE6C302D73A831A49E34E59149D3A9AD96CCEBDFBF62F6D5710D92D8\" \\\n    -args \"100\" -args \"$(( $(date +%s) + 3600 ))\" -args \"memo\" \\\n    -gas-fee 1000000ugnot -gas-wanted 90000000 \\\n    -broadcast -chainid gnoland-1 ADDRESS\n```\n\n### GRC20 token\n\nThe caller must first approve the transfer app realm to spend the tokens,\nand attach no native coins to the `Transfer` call. On an Ethereum route the\nroute fee is drawn from the same allowance, separately and first, so the\nallowance and the balance must cover `amount + fee` (the denom's current\n`fee`, shown at `routes/{route}`); on other clients `amount` is enough.\n\nFrom Gno code (e.g. in a `MsgRun` script):\n\n```gno\ntransferAddr := chain.PackageAddress(\"gno.land/r/aib/ibc/apps/transfer\")\nmytoken.Approve(cross(cur), caller, transferAddr, amount)\n```\n\nOr via `gnokey` directly, where `g1tp3gk4quumurav4858hjfdy6hxtyffwmnxyr00` is\nthe transfer app realm address:\n\n```\ngnokey maketx call -pkgpath gno.land/r/demo/foo -func Approve \\\n    -args \"g1caller...\" -args \"g1tp3gk4quumurav4858hjfdy6hxtyffwmnxyr00\" -args \"100\" \\\n    -gas-fee 1000000ugnot -gas-wanted 90000000 \\\n    -broadcast -chainid gnoland-1 ADDRESS\n```\n\nThen call Transfer with the grc20reg key as denom:\n\n```\ngnokey maketx call -pkgpath gno.land/r/aib/ibc/apps/transfer -func Transfer \\\n    -args \"07-tendermint-1\" -args \"atone1abc...\" \\\n    -args \"gno.land/r/demo/foo.FOO\" -args \"100\" \\\n    -args \"$(( $(date +%s) + 3600 ))\" -args \"\" \\\n    -gas-fee 1000000ugnot -gas-wanted 90000000 \\\n    -broadcast -chainid gnoland-1 ADDRESS\n```\n\n\u003e [!IMPORTANT]\n\u003e GRC20 denoms are encoded into slash-free aliases for IBC packets by replacing\n\u003e `/` with `:` (e.g. `gno.land/r/demo/foo.FOO` becomes\n\u003e `gno.land:r:demo:foo.FOO`). The counterparty chain sees the aliased form, not\n\u003e the original grc20reg key.\n\n## Authority\n\nAdministration follows the core realm's split (ADR pr6375 R14). The\ntightening operation `BlockAddress` acts at once and is gated by an authority\n(`p/aib/authority`). Its initial members are the GovDAO proxy and the\ngovernance realm `gno.land/r/aib/ibc/ibcgov` (`IBCGovPath`); there is no\ndeployer member. ibcgov can remove GovDAO at once\n(`NewTransferRemoveMemberRequest`); only ibcgov itself is irremovable, so\nGovDAO's tightening power lasts only while it remains a member. GovDAO submits it with `NewBlockAddressProposalRequest`, as\ndescribed in the core README. Every other operation (`UnblockAddress`,\n`UpdateImpl`, `AddAuthorityMember`, `RemoveAuthorityMember`, and the Ethereum\nroute controls of `GovApply`) is a decision of `gno.land/r/aib/ibc/ibcgov`\nalone, with that decision's own delay: `UnblockAddress`, `UpdateImpl` and\n`AddAuthorityMember` wait 7 days, while `RemoveAuthorityMember` (a\ntightening) applies at once, as does ibcgov's own block decision; the route\ncontrols' delays are listed in ADR 0004. Pausing is not the app's job: the core suspends any\napplication with `core.PauseApp(\"transfer\")` (sends refused, incoming packets\nanswered with an error acknowledgement so the counterparty refunds, refunds\nstill delivered), and the home page shows a banner while that is the case; the\nincident tool of the Ethereum route is its per-client flags (see the Ethereum\nroute controls section). The authority of this realm keeps a blocklist of\naddresses, the equivalent of ibc-go's blocked addresses:\n\n- `BlockAddress(addr)`: a blocked address can no longer send (`OnSendPacket`\n  rejects it, so `Transfer` fails) nor receive (`OnRecvPacket` answers with an\n  error acknowledgement, so the counterparty refunds its sender). Refunds to a\n  blocked sender still go through: blocking never confiscates funds already in\n  flight.\n- `UnblockAddress(addr)`: removes it (an ibcgov decision, after 7 days).\n- `IsBlocked(addr)`: reports the state; the `admin` page lists the blocklist.\n\nThe blocklist holds canonical (lowercase) addresses: bech32 also accepts the\nall-uppercase spelling of an address, so `BlockAddress` and `UnblockAddress`\nnormalize their argument (and reject an invalid address), and `IsBlocked`\nmatches either spelling. On receive, `OnRecvPacket` decodes the packet's\nreceiver as ibc-go does, and every check and transfer uses the canonical form\n(`CanonicalAddress`). On a non-Ethereum client, a receiver that is not a\nvalid gno address gets an error acknowledgement. On an Ethereum route it does\nnot: the transfer is booked as an owed sweep entry that keeps the raw\nreceiver, with a success acknowledgement, and only a governed sweep decision\nnaming a payee pays it (see the route controls section).\n\nAll of these are `MsgCall`-compatible and emit `block` / `unblock` /\n`authority_member_added` / `authority_member_removed` events.\n\n## Ethereum route controls\n\nWith `impl/v1` active (on a fresh deployment it is the transfer app's first\nregistered implementation, so it bootstraps with no decision; `impl/v0`\nregisters nothing), every Ethereum client is under the controls of ADR pr6375 §7; every other client keeps `impl/v0`'s\nbehaviour. The state lives in this realm (`InitExt(\"controls\")`, the\n`gno.land/p/aib/ibc/ics20/controls/v0` registry); the flags and holds live in\n`gno.land/r/aib/ibc/ibcgov`, which also decides every change. Design and rule\nmapping: [ADR 0004](../../../../../../docs/adrs/0004-ethereum-route-controls.md).\n\n- **Routes.** An Ethereum client carries transfers only as the current client\n  of a route ibcgov enrolled, and only in the denoms enabled on it (gno-native:\n  `unescrow`; Ethereum tokens: `mint`, keyed by their voucher's `ibc/` denom).\n  Escrow, vouchers and controls are keyed by the route key (the first client\n  id); the denoms on the wire carry the current client id, so a route\n  replacement re-points the route in constant work.\n- **Sends** of a gno-native token: per-message minimum and maximum, the route\n  fee charged on top (`-send` the amount plus the fee; the fee goes to the\n  route's fee collector), receivers that are valid Ethereum addresses, not\n  zero, not reserved, not a refused bridge contract and not the denom's own\n  Ethereum token; a `sends` flag stops them. A voucher going back is never\n  limited by the send minimum and maximum, charged a fee or stopped by a\n  `sends` flag (or by a loss without a restoring decision), and is taken on\n  a route a replacement closed too (exit only: it lowers the supply and adds\n  no exposure), but waits until its denom is enabled on the route, and its\n  receiver passes the same recipient checks: a valid Ethereum address, not\n  zero, not reserved, not a refused bridge contract and not the token itself\n  (the voucher's base denom). Every send writes an immutable send record (full packet, commitment,\n  sender, amounts, height) in the same transaction; a minted denom keeps the\n  returned vouchers counted against its supply cap until the send settles.\n- **Deployment.** The transfer app is deployed fresh with impl/v1 as its\n  initial implementation; an Ethereum client carries nothing until ibcgov\n  enrolls it as a route (there is no migration from impl/v0).\n- **Receives** are refused with an error acknowledgement, before any state\n  change, under a `credits` flag, on a closed route, for a denom not enabled,\n  past a minted denom's supply cap (what is booked against it,\n  `Outstanding + Returning + Parked + ProvenOwed + Owed + Withheld + Swept`,\n  plus the receive, ADR 0004's one equation; the\n  cap governs admission only: lowering it below what is booked refuses new\n  credits and keeps every booked obligation payable), or for a blocked\n  receiver. Otherwise they\n  are booked as a claim keyed by the packet (or a sweep entry: below the fee,\n  above the free lock, to a bridge realm, or to a receiver that is no gno.land\n  address, kept raw), with a success acknowledgement.\n- **`Claim(clientID, denom, n)`** (anyone; a positive `n` above 20 is taken\n  as 20) pays up to `n` claims round-robin, looking at no more than `4n`\n  recipients, and returns how many it paid (claims it moved to the owed\n  sweep, their recipient having become a bridge realm, are not counted). A\n  zero return does not mean every remaining claim is unpayable: the\n  recipients it looked at may have had empty meters while others wait. It\n  pays the oldest claim first per recipient, within the bridge meter (day cap\n  `max(capFloor, lock/10)`, or the mint cap) and the recipient's meter (a\n  quarter of it). A claim's first payout has a fee portion,\n  `min(remainder, the claim's fee, the denom's fee now)` (a claim reduced\n  below its fee can finish as a fee payment alone), and, in the same turn, a\n  recipient portion if the meters hold more (at least `min(what remains,\n  fee)`, or nothing); no slivers (a later\n  installment needs at least `min(remainder, fee)` in both meters). A claim's\n  fee is locked when it is credited (`fee` in the claims JSON), and the fee\n  it pays is the lower of its fee and the denom's fee now, written back at\n  each payout turn: the fee locked at credit is a ceiling a later raise\n  never passes, and a cut applies and, once a payout turn has seen it, is\n  kept.\n  Meters start empty and restart empty when a `payouts` flag is cleared;\n  nothing is paid under one.\n- **Refunds** (error acknowledgement, timeout) are paid at once only when the\n  whole refund fits: no `refunds` hold and the route not closed, no older\n  refund of the denom waiting, the refund meter (`RefundCap` a day) holding\n  the whole amount, and, for an `unescrow` denom, the escrow holding it.\n  Otherwise the refund is queued. Queued refunds are paid first in, first\n  out by **`ClaimRefunds(clientID, denom, n)`** (anyone; a positive `n`\n  above 20 is taken as 20; nothing\n  under a `refunds` hold or on a closed route), in installments: each takes\n  what the meter (and, for `unescrow`, the escrow) allows of the head\n  refund's remainder, at least `min(remainder, max(1, RefundCap/24))`, which\n  the meter must hold for any installment, a minted one included (a minted\n  installment the meter allows is then cut to the supply's headroom when\n  that is less, any positive amount: headroom clips the payment, never the\n  meter minimum), and the call stops at a head it cannot pay. The outcome, the\n  local processing height and the payment state are attached to the send\n  record. Queued refunds are reserved: no receive or claim payout takes\n  their escrow.\n- **Closed routes.** A route replacement closes the route: no gno-native\n  send, credit, claim payout, acknowledgement or timeout refund payout, or\n  `ClaimRefunds` payout. A voucher return is still taken over the route's\n  new client (exit only), and its \"send back\" link stays on the home page.\n  A governed reconciliation's refunds are the other exception: a\n  settlement pays them on the closed route (that is how the retired\n  client's sends are settled), subject to its own checks and deferred under\n  a `refunds` hold. A genuine refund the settlement cannot pay then is\n  parked instead of refused: its record is settled, the refund owed and\n  queued. A minted one (the outstanding supply cannot hold it) is reserved\n  against the supply cap (apart from the returns in flight, so it never\n  refuses a return), and `ClaimRefunds` pays it after the reopening in\n  installments as the supply has room (a voucher return makes some). An\n  escrowed one (the free escrow is short, a forged receive having drained\n  it, say) is reserved by the queued refunds, so no claim, sweep or\n  receive takes the escrow it waits for, and `ClaimRefunds` pays it after\n  the reopening once the escrow is whole again (a recapitalization); the\n  route can reopen meanwhile, its losses accepted as a deficit. The replacement client must authenticate the route's identity, the\n  Ethereum chain, router and commitments slot recorded at the enrolment\n  (`ibcgov.RouteIdentity`). It reopens by a governed decision, refused\n  while a send record of a retired client is still open (its commitment\n  outstanding or its refund queued, not settled by a reconciliation) or was\n  processed (acknowledged or timed out) at or after the replacement's\n  suspect height and is not settled; records completed and paid before the\n  suspect interval need no settlement. The reopening records the remaining\n  losses as an accepted deficit and revokes unpaid recapitalization\n  authorizations, in batches of at most 64 incidents, then at most 64\n  authorizations (`ReopenBatch`), continued by anyone with\n  **`ContinueReopen(route)`**. A loss of any denom without a\n  restoring decision stops new exposure only (gno-native sends, credits,\n  claim payouts, sweep payments): voucher returns, acknowledgements, timeouts\n  and refunds go on.\n- **Governance** (`GovApply`, ibcgov only): enrolment, denom limits, fee\n  collector, refused recipients, voids and restores of withheld claims, credit\n  by vote, sweep payments (live entries of one denom, each paid whole to the\n  decision's payee, with no claim fee and outside the payout meters; for an\n  `unescrow` denom their total must fit the free escrow, what open and withheld claims,\n  queued refunds and proven reservations do not reserve; deferred under a `payouts` flag), losses,\n  recapitalization authorizations (only for\n  an `unescrow` denom, the only kind recapitalized on gno.land; paid by\n  anyone with **`Recapitalize(route, id)`**: for a native coin, a direct\n  user call sending exactly the authorized amount; for a GRC20, no coins\n  attached, the amount taken from the immediate caller's allowance to this\n  realm), reductions, accepted deficits,\n  settlements of a retired client's sends, route replacement and reopening.\n- **Renders**: `routes` (a JSON array of every enrolled route, each\n  element the `routes/{route}` object, not paginated), `routes/{route}` (one JSON\n  object of the route with every enabled denom and its totals, not\n  paginated), and `routes/{route}/claims`, `routes/{route}/sweeps`,\n  `routes/{route}/sends`, `routes/{route}/losses` (JSON pages of at most\n  100 entries, `{\"entries\":[...],\"next\":\u003ccursor\u003e}`: pass `next` as\n  `?from=` for the following page, `\"\"` when there is none; losses are\n  ordered by direction then packet key), and the `route_*` events (`credit`,\n  `claim`, `settle`, `refund`, `recapitalize`, `gov`).\n\n## Query endpoints\n\nAll endpoints return JSON and are accessible via gnoweb or `gnokey query vm/qrender`.\n\n| Path                                | Description                                                                        |\n| ----------------------------------- | ---------------------------------------------------------------------------------- |\n| `denoms`                            | List the registered IBC denominations (paginated)                                  |\n| `denoms/ibc/{hash}`                 | Details for a specific IBC denom (base, path, denom)                               |\n| `total_escrow/{denom}`              | Total escrowed amount for a native denom                                           |\n| `vouchers`                          | List all voucher tokens (paginated, includes `grc20reg_key`)                       |\n| `voucher/ibc/{hash}`                | Voucher token metadata (denom, grc20reg_key, name, symbol, decimals, total supply) |\n| `voucher/ibc/{hash}/balance/{addr}` | Voucher balance for an address                                                     |\n\n`denoms` and `vouchers` are paginated: `?page=N\u0026limit=M` (page 1 and 20\nentries by default, at most 100 entries a page), answered as\n`{\"items\":[...],\"page\":N,\"total\":T}` where `total` is the number of pages.\n\nExample:\n\n```\ngnokey query vm/qrender -data \"gno.land/r/aib/ibc/apps/transfer:total_escrow/ugnot\"\n```\n\n```json\n{ \"denom\": \"ugnot\", \"amount\": 100, \"clients\": [{ \"client\": \"07-tendermint-1\", \"amount\": 100 }] }\n```\n\nAmounts in these JSON renders are written as JSON numbers converted from\nfloat64: an integer above 2^53 (9007199254740992) can lose precision\n(9007199254740993 renders as 9007199254740992). Read exact values from Gno\ncode: `VoucherBalanceOf(ibcDenom, addr)` for a balance, the voucher token's\n`TotalSupply()` (via grc20reg) for a supply, and the route controls' totals\nfor an Ethereum route.\n\n## Helper functions\n\n### VoucherBalanceOf\n\nQuery the balance of a voucher token for a given address. Returns 0 if the\nvoucher does not exist.\n\n```gno\nbalance := transfer.VoucherBalanceOf(\"ibc/CAEF9C...\", addr)\n```\n\n### VoucherSend\n\nSend an IBC voucher token to another Gno address:\n\n```gno\ntransfer.VoucherSend(cross(cur), \"ibc/CAEF9C...\", recipient, 100)\n```\n\n```\ngnokey maketx call -pkgpath gno.land/r/aib/ibc/apps/transfer -func VoucherSend \\\n    -args \"ibc/CAEF9CA8CE6C302D73A831A49E34E59149D3A9AD96CCEBDFBF62F6D5710D92D8\" \\\n    -args \"g1recipient...\" -args \"100\" \\\n    -gas-fee 1000000ugnot -gas-wanted 90000000 \\\n    -broadcast -chainid gnoland-1 ADDRESS\n```\n\n### VoucherApprove\n\nSet a spender allowance for an IBC voucher token:\n\n```gno\ntransfer.VoucherApprove(cross(cur), \"ibc/CAEF9C...\", spender, 100)\n```\n\n```\ngnokey maketx call -pkgpath gno.land/r/aib/ibc/apps/transfer -func VoucherApprove \\\n    -args \"ibc/CAEF9CA8CE6C302D73A831A49E34E59149D3A9AD96CCEBDFBF62F6D5710D92D8\" \\\n    -args \"g1spender...\" -args \"100\" \\\n    -gas-fee 1000000ugnot -gas-wanted 90000000 \\\n    -broadcast -chainid gnoland-1 ADDRESS\n```\n\n### GRC20Alias\n\nConvert a grc20reg key to the slash-free alias used in IBC packets.\n\n```gno\nalias := transfer.GRC20Alias(\"gno.land/r/demo/foo.FOO\")\n// \"gno.land:r:demo:foo.FOO\"\n```\n\n### NewDenom / NewHop\n\nConstruct a `Denom` (package `gno.land/p/aib/ibc/ics20/v0`, which holds the\nICS-20 types shared by the realm and its implementations) to compute the\nexpected IBC denom hash:\n\n```gno\ndenom := ics20.NewDenom(\"uphoton\", ics20.NewHop(transfer.PortID, \"07-tendermint-1\"))\nibcDenom := denom.IBCDenom()\n// \"ibc/CAEF9CA8CE6C302D73A831A49E34E59149D3A9AD96CCEBDFBF62F6D5710D92D8\"\n```\n\n## DEX integration for voucher tokens\n\nVoucher tokens minted on `RecvPacket` are standard GRC20 tokens registered in\n`grc20reg`. A DEX (e.g. Gnoswap) can discover and list them like any other\nGRC20 token.\n\n### Discovery\n\nVoucher tokens can be discovered in two ways:\n\n1. **From the transfer app** — query the `vouchers` render endpoint to list all\n   voucher tokens with their metadata. Each entry includes a `grc20reg_key`\n   field with the token's grc20reg key, ready for direct lookup. Supports\n   pagination.\n\n2. **From `grc20reg`** — `grc20reg` keys tokens by `rlmPath.symbol`, so voucher\n   tokens are registered as `gno.land/r/aib/ibc/apps/transfer.{SYMBOL}`, where\n   `{SYMBOL}` is allocated from a counter when the voucher is created\n   (`IBC-` and the voucher's id in base 36, upper case: `IBC-1`, `IBC-2`,\n   ...), not derived from its hash: read it from the `vouchers` render or\n   `VoucherSymbol(ibcDenom)`. Enumerate entries with the\n   `gno.land/r/aib/ibc/apps/transfer.` prefix.\n\nThe token name is the base denomination (e.g. `uatom`) and the symbol is\nallocated when the voucher is created (e.g. `IBC-1`). An ICS20 packet carries\nno decimal metadata, so balances are always the unscaled base-unit amounts the\npackets carried. A voucher's decimals only tell wallets how to show them: on\nan Ethereum route they are the denom's governed configuration\n(`ibcgov.NewSetDenomRequest`'s `decimals`, 0 to 18, ADR 0005 \"Voucher\ndecimals\"), fixed when the first receive creates the voucher; a voucher of a\nclient the route controls do not cover (a Tendermint chain) has zero. For the\nfull denom and richer metadata (trace hops, total supply), query the render\nendpoint `voucher/ibc/{HASH}`.\n\n### Trading\n\n1. A user receives voucher tokens via an IBC `RecvPacket`.\n2. The user approves the DEX realm address to spend their voucher tokens\n   (`Approve`).\n3. The DEX interacts with the voucher via the standard GRC20 teller interface\n   (`TransferFrom`, `BalanceOf`, etc.).\n\n### Multiple vouchers for the same base denom\n\nThe same base denomination (e.g. `uatom`) arriving from different chains\nproduces distinct voucher tokens with different IBC hashes. A DEX should use the\nfull `ibc/{HASH}` denom to distinguish them — the token symbol is an\nallocated label, so read the full denom from the `denom` field of the\n`vouchers` render endpoint — and query `denoms/ibc/{HASH}` to resolve the\ntrace path for display purposes. No two vouchers share a symbol, or a\nregistry key, whatever their hashes (the full hash is kept as the grc20reg\nslug).\n\n## Events\n\n### `ibc_transfer`\n\nEmitted in `OnSendPacket` for every outgoing transfer packet.\n\n| Attribute  | Description                           |\n| ---------- | ------------------------------------- |\n| `sender`   | Sender address on source chain        |\n| `receiver` | Receiver address on destination chain |\n| `denom`    | Full denom path                       |\n| `amount`   | Transfer amount                       |\n| `memo`     | Memo text                             |\n\n### `fungible_token_packet`\n\nEmitted on `OnRecvPacket`:\n\n| Attribute  | Description                |\n| ---------- | -------------------------- |\n| `sender`   | Sender address             |\n| `receiver` | Receiver address           |\n| `denom`    | Full denom path            |\n| `amount`   | Amount                     |\n| `memo`     | Memo text                  |\n| `success`  | `\"true\"` or `\"false\"`      |\n| `error`    | Error message (on failure) |\n\nAlso emitted on `OnAcknowledgementPacket`:\n\n| Attribute         | Description                                                          |\n| ----------------- | -------------------------------------------------------------------- |\n| `sender`          | Sender address                                                       |\n| `receiver`        | Receiver address                                                     |\n| `denom`           | Full denom path (Ethereum route: the send record's accounting denom) |\n| `amount`          | Amount                                                               |\n| `memo`            | Memo text (not emitted on an Ethereum route)                         |\n| `acknowledgement` | Acknowledgement bytes                                                |\n\nfollowed by a second `fungible_token_packet` event carrying only `success`\n(the acknowledgement's result) or `error` (its error).\n\n### `denomination`\n\nEmitted on every `OnRecvPacket` that credits a voucher (a token not\noriginating on gno.land), not only when its denom is first created; on an\nEthereum route, on every receive of a `mint` denom that passes the refusal\nchecks.\n\n| Attribute    | Description                      |\n| ------------ | -------------------------------- |\n| `denom_hash` | Uppercase hex SHA256 hash        |\n| `denom`      | JSON representation of the Denom |\n\n### `timeout`\n\nEmitted on `OnTimeoutPacket`.\n\n| Attribute  | Description                                                                 |\n| ---------- | --------------------------------------------------------------------------- |\n| `receiver` | Refund recipient (original sender)                                          |\n| `denom`    | Full denom path (Ethereum route: the send record's accounting denom)        |\n| `amount`   | Non-Ethereum client: the refunded amount; Ethereum route: the whole send    |\n| `memo`     | Non-Ethereum client: the original memo; Ethereum route: empty               |\n\nOn a non-Ethereum client the tokens are refunded in the same transaction. On\nan Ethereum route the refund follows the refund rules of the route controls\nsection: `amount` is the send's whole entitlement even when the refund is\nqueued. What was actually paid is in the `route_settle` event (`refunded`,\nthe amount paid now, and `payment`, the record's payment state) and, for a\nqueued refund, in each later `route_refund` event of `ClaimRefunds`. The same\n`route_settle` event follows an error acknowledgement.\n\n## IBC packet lifecycle\n\n1. **Transfer** -- Caller invokes `Transfer`. Tokens are escrowed (native/GRC20)\n   or burned (voucher). An IBC packet is sent via `core.SendPacket`.\n\n2. **RecvPacket** -- The destination chain receives the packet. On a\n   non-Ethereum client, value moves at once: if the token originated on the\n   destination chain, the escrowed tokens are returned; otherwise a voucher\n   GRC20 token is minted and registered in `grc20reg`. On an Ethereum route,\n   the receive only books a claim (or an owed sweep entry) with its success\n   acknowledgement; the value moves later, when `Claim` pays the claim\n   within the meters or a governed decision pays the sweep entry.\n\n3. **Acknowledgement** -- On success, a non-Ethereum client emits two\n   `fungible_token_packet` events (the acknowledgement's attributes, then\n   `success`); on an Ethereum route the send record is first settled as\n   delivered (its supply reservation released), emitting `route_settle`\n   (`outcome`, `payment`, `refunded` 0), then the same two\n   `fungible_token_packet` events. On failure, the sender is refunded (re-mint vouchers, unescrow\n   native/GRC20): at once on a non-Ethereum client; on an Ethereum route,\n   at once only when the whole refund fits, otherwise queued and paid by\n   `ClaimRefunds` (see the route controls section).\n\n4. **Timeout** -- If the packet is not received before `timeoutTimestamp`, the\n   sender is refunded using the same mechanism as a failed acknowledgement.\n\nOn an Ethereum route the acknowledgement and timeout callbacks complete the\npacket's transport (the core deletes its commitment) whether or not the\nrefund is paid in the same transaction; the send record tracks the payment.\nA queued refund is a deliberate deferral; a payment that fails is not: if\nthe token transfer, unescrow or voucher mint of a refund paid at once\nfails, the callback panics and the whole transaction rolls back, transport\ncompletion included, so the relayer retries after the token is repaired.\nSuch a payment is never parked or skipped.\n\n### Payload checks\n\nThe core's packet limits come first (core README, Packet limits: among them\n262,144 bytes of payload data and, on send, a timeout at most 24 hours\nahead; a receive only checks that its timeout has not arrived). The\ntransfer app then requires:\n\n- port `transfer` at both ends, version `ics20-1`, and encoding\n  `application/x-protobuf` or `application/x-solidity-abi`;\n- packet client ids, stricter than the core's length and alphabet rule:\n  over a non-Ethereum client both the source and the destination client id\n  pass `types.IsValidClientID` (`{string}-{number}`, so an id like `peer`,\n  which the core accepts, is refused here); over an Ethereum client the\n  local client id passes `IsValidClientID` and the router's client id\n  `types.IsValidRouterClientID` (eureka's grammar: `client-N` or a custom\n  id of up to 128 characters);\n- an amount that is a decimal integer of at most 2^256 - 1 and strictly\n  positive, read as a uint256 (digits only: no sign, space or separator).\n  A native coin's amount must also fit an int64 (at most\n  9,223,372,036,854,775,807), the chain's coin maximum; vouchers and GRC20\n  escrow (`gno.land/p/nt/grc20/v1`) count in uint256. On an Ethereum route\n  every amount is at most 2^96 - 1, the most Ethereum's transfer app takes:\n  a send above it is refused and a receive above it error-acked;\n- a sender that is not blank, and a receiver that is not blank except on an\n  Ethereum route, which books an undecodable receiver, blank included, as an\n  owed sweep entry;\n- a denom whose hops (`port/client`) are valid (port 2 to 128 characters,\n  client id 4 to 64 characters, or on an Ethereum route also a router client\n  id) and whose base is not blank and contains no `/`.\n\nThe memo has no limit of its own: only the payload size bounds it. A send\nthat fails a check aborts the transaction; a receive that fails one is\nanswered with an error acknowledgement, and the sender is refunded.\n\n## Escrow accounting\n\nNative coins and non-IBC GRC20 tokens are escrowed at the transfer realm's\nsingle account (Gno realms cannot own multiple addresses, so the ibc-go\nper-channel escrow address approach is not available). Per-client isolation is\ntracked in the realm store instead. For non-Ethereum clients:\n\n- Escrow is tracked per `(client, denom)` in the realm store, not as a single\n  global per-denom pool.\n- `OnSendPacket` records escrow under the packet's `SourceClient` (the client on\n  this chain that the token was sent over).\n- `OnRecvPacket` debits the packet's `DestinationClient` (the client the\n  returning token arrives on), which is the client that originally escrowed it.\n  `checkEscrowForClient` rejects the release if that client never locked enough\n  of the denom, so a packet arriving on a different client cannot drain escrow\n  that belongs to another client. The matching `debitEscrowForClient` runs only\n  after the token transfer succeeds, so a failed transfer leaves the accounting\n  intact.\n- A failed release returns an error acknowledgement; the escrow is untouched.\n\nThis prevents the shared-escrow drain: an attacker who creates their own client\nand forges a valid proof against it cannot unescrow funds that honest users\nlocked under a different client. See\n`z4f_on_recv_packet_drain_filetest.gno` for the regression test.\n\nOn an Ethereum route the escrow is keyed by the route's stable key (its first\nclient id), so it follows the route across client replacements. A send\nescrows under it; a receive moves no value: it books a claim or a sweep\nentry, and the escrow is released later, by `Claim` within the meters or by\na governed sweep payment. A receive above the free escrow (what open and withheld claims,\nqueued refunds and proven reservations do not reserve) is not refused: it becomes an owed sweep\nentry, acknowledged with success.\n\n## Upgrades\n\nThis realm is a permanent, thin proxy (see\n[ADR 0002](../../../../../../docs/adrs/0002-proxy-realm-for-transfer-upgrades.md)):\nit holds the escrow, the voucher tokens with their ledgers and the known\ndenominations, and forwards `Transfer`, the four IBC callbacks and `Render` to\nan implementation realm that holds the ICS-20 logic. On a fresh deployment\nthe first, and so the bootstrapping, registration is\n`gno.land/r/aib/ibc/apps/transfer/impl/v1`; `impl/v0` holds the ICS-20 logic\n`impl/v1` embeds for non-Ethereum clients and registers nothing. The lifecycle is the core's\n(`RegisterImpl(ctor, version, over, keepsControls)` from the implementation's\n`init`, `UpdateImpl` by an ibcgov decision, `ImplPath` and `ImplVersion`); see\nthe core README's Upgrades section. Here the manifest also declares whether\nthe implementation keeps the route controls: once any route exists,\n`CheckUpdateImpl` refuses an implementation that does not. The ICS-20 types\nshared by every implementation live in `gno.land/p/aib/ibc/ics20/v0`.\n"},{"name":"address.gno","body":"package transfer\n\nimport \"crypto/bech32\"\n\n// CanonicalAddress decodes s as a gno address and returns its canonical\n// encoding, the lowercase one, re-encoded from the decoded bytes. It reports\n// false if s is not a valid address on this chain.\n//\n// bech32 accepts an address spelled all lowercase or all uppercase, and the\n// banker credits the same account for both, but a string compared or used as\n// a key as is (the blocklist, the GRC20 ledger) tells them apart. Addresses\n// that come from outside the chain, such as an ICS-20 packet receiver, must\n// go through this function before use, as ibc-go decodes them with\n// addressCodec.StringToBytes.\nfunc CanonicalAddress(s string) (address, bool) {\n\t// The chain's own check: checksum, the chain's address prefix, 20 bytes.\n\tif !address(s).IsValid() {\n\t\treturn \"\", false\n\t}\n\thrp, bz, err := bech32.DecodeToBase256(s) // hrp comes back lowercase\n\tif err != nil {\n\t\treturn \"\", false\n\t}\n\tenc, err := bech32.EncodeFromBase256(hrp, bz)\n\tif err != nil {\n\t\treturn \"\", false\n\t}\n\treturn address(enc), true\n}\n"},{"name":"admin.gno","body":"package transfer\n\nimport (\n\t\"chain\"\n\n\t\"gno.land/p/aib/authority/v0\"\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nconst (\n\tEventTypeBlock                  = \"block\"\n\tEventTypeUnblock                = \"unblock\"\n\tEventTypeAuthorityMemberAdded   = \"authority_member_added\"\n\tEventTypeAuthorityMemberRemoved = \"authority_member_removed\"\n)\n\n// IBCGovPath is the governance realm of the IBC realms (ADR pr6375 R11,\n// R14). Every power that loosens something here (an implementation upgrade,\n// unblocking an address, the authority's membership, the Ethereum route\n// controls of GovApply) is gated on a call from it, so no decision skips its\n// delay.\nconst IBCGovPath = \"gno.land/r/aib/ibc/ibcgov\"\n\nvar (\n\t// auth gates the immediate, tightening operation BlockAddress. Its members\n\t// are fixed at init to the GovDAO proxy, which acts through the proposal\n\t// constructors in proposals.gno, and the ibcgov realm; there is no\n\t// deployer member (ADR pr6375 R14). Pausing the app is the IBC core's job\n\t// (core.PauseApp); the per-route flags of the Ethereum route are ibcgov's.\n\tauth    *authority.Authority\n\tblocked = bptree.NewBPTree32() // address -\u003e true\n)\n\nfunc init(cur realm) {\n\tauth = authority.NewWithMembers(\n\t\tchain.PackageAddress(authority.GovDAOPath),\n\t\tchain.PackageAddress(IBCGovPath),\n\t)\n}\n\n// assertGovernance panics unless the caller of the crossing function whose\n// cur it is passed (a non-crossing call) is the ibcgov realm.\nfunc assertGovernance(cur realm, op string) {\n\tif p := cur.Previous().PkgPath(); p != IBCGovPath {\n\t\tcaller := p\n\t\tif caller == \"\" {\n\t\t\tcaller = string(cur.Previous().Address())\n\t\t}\n\t\tpanic(ufmt.Sprintf(\"%s: only %s decides this, not %s\", op, IBCGovPath, caller))\n\t}\n}\n\n// BlockAddress, the tightening operation, is a thin crossing wrapper over a\n// non-crossing helper, which the GovDAO proposal callback invokes; see\n// r/aib/ibc/core/admin.gno for why.\n\n// AddAuthorityMember adds a member to the authority. Governance only.\nfunc AddAuthorityMember(cur realm, addr address) {\n\tassertGovernance(cur, \"add_authority_member\")\n\taddr = mustCanonicalAddress(addr)\n\tauth.AddMember(0, cur, addr)\n\tchain.Emit(EventTypeAuthorityMemberAdded, \"address\", string(addr))\n}\n\n// RemoveAuthorityMember removes a member; the last one cannot be removed.\n// Governance only.\nfunc RemoveAuthorityMember(cur realm, addr address) {\n\tassertGovernance(cur, \"remove_authority_member\")\n\taddr = mustCanonicalAddress(addr)\n\tif addr == chain.PackageAddress(IBCGovPath) {\n\t\t// ibcgov changes membership as a member: removing it would freeze\n\t\t// the membership and its tightening powers for good.\n\t\tpanic(\"remove_authority_member: the governance realm stays a member\")\n\t}\n\tauth.RemoveMember(0, cur, addr)\n\tchain.Emit(EventTypeAuthorityMemberRemoved, \"address\", string(addr))\n}\n\n// AuthorityMembers returns the members of the authority.\nfunc AuthorityMembers() []address {\n\treturn auth.Members()\n}\n\n// IsAuthorityMember reports whether addr may block addresses.\nfunc IsAuthorityMember(addr address) bool {\n\tif canonical, ok := CanonicalAddress(string(addr)); ok {\n\t\taddr = canonical\n\t}\n\treturn auth.IsMember(addr)\n}\n\n// BlockAddress adds addr to the blocklist. A blocked address can no longer\n// send (OnSendPacket rejects it, so Transfer fails) nor receive (OnRecvPacket\n// answers with an error acknowledgement, so the counterparty refunds its\n// sender). Refunds to a blocked sender through OnAcknowledgementPacket and\n// OnTimeoutPacket still go through: blocking never confiscates funds that were\n// already in flight. Authority members only (GovDAO or ibcgov).\nfunc BlockAddress(cur realm, addr address) {\n\tblockAddress(0, cur, addr)\n}\n\nfunc blockAddress(_ int, rlm realm, addr address) {\n\tauth.Assert(0, rlm, \"block_address\")\n\tcanonical := mustCanonicalAddress(addr)\n\tblocked.Set(string(canonical), true)\n\tchain.Emit(EventTypeBlock, \"address\", string(canonical))\n}\n\n// UnblockAddress removes addr from the blocklist. Governance only.\nfunc UnblockAddress(cur realm, addr address) {\n\tassertGovernance(cur, \"unblock_address\")\n\tcanonical := mustCanonicalAddress(addr)\n\tblocked.Remove(string(canonical))\n\tchain.Emit(EventTypeUnblock, \"address\", string(canonical))\n}\n\n// IsBlocked reports whether addr is on the blocklist, whatever its spelling.\n// The blocklist is keyed by canonical addresses (see CanonicalAddress); an\n// invalid address is never blocked.\nfunc IsBlocked(addr address) bool {\n\tcanonical, ok := CanonicalAddress(string(addr))\n\treturn ok \u0026\u0026 blocked.Has(string(canonical))\n}\n\nfunc mustCanonicalAddress(addr address) address {\n\tcanonical, ok := CanonicalAddress(string(addr))\n\tif !ok {\n\t\tpanic(\"invalid address: \" + string(addr))\n\t}\n\treturn canonical\n}\n\n// BlockedAddresses returns the blocklist, sorted.\nfunc BlockedAddresses() []address {\n\tout := make([]address, 0, blocked.Size())\n\tblocked.Iterate(\"\", \"\", func(k string, _ any) bool {\n\t\tout = append(out, address(k))\n\t\treturn false\n\t})\n\treturn out\n}\n"},{"name":"app.gno","body":"package transfer\n\nimport (\n\t\"gno.land/p/aib/ibc/app/v0\"\n\t\"gno.land/p/aib/ibc/types/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/aib/ibc/core\"\n)\n\n// App is the IBCApp value registered in the IBC core for PortID. It is this\n// realm's forever (the core refuses a second registration for the port), so\n// its callbacks forward to the active implementation.\ntype App struct{}\n\nconst (\n\t// NOTE we must use the same portID as the ibc-go transfer IBC app\n\tPortID = \"transfer\"\n\t// V1 defines first version of the IBC transfer module\n\tV1               = \"ics20-1\"\n\tEncodingProtobuf = \"application/x-protobuf\"\n)\n\nfunc init(cur realm) {\n\t// Register the app in the IBC router. On chain init runs once; the guard\n\t// is for the gno test harness, which instantiates this realm a second\n\t// time when it loads the implementation realm (whose import of this realm\n\t// resolves outside the package under test), and the core refuses a\n\t// second registration of the port.\n\tif core.AppPkgPath(PortID) == PkgPath {\n\t\treturn\n\t}\n\tcore.RegisterApp(cross(cur), PortID, \u0026App{})\n}\n\nvar _ app.IBCApp = \u0026App{}\n\n// IsFeeCollector reports whether a is the fee collector of any route of the\n// route controls: the core reads it before registering an implementation,\n// whose realm would become a realm of the bridge (ADR 0004). A read only.\nfunc (a *App) IsFeeCollector(addr address) bool { return isFeeCollector(addr) }\n\n// isFeeCollector reads the route controls' index of fee collectors, if the\n// controls exist.\nfunc isFeeCollector(addr address) bool {\n\tfc, ok := store.ExtView(\"controls\").(interface{ IsFeeCollector(address) bool })\n\treturn ok \u0026\u0026 fc.IsFeeCollector(addr)\n}\n\n// corePkgPath is the only realm allowed to invoke the IBCApp callbacks below.\nconst corePkgPath = \"gno.land/r/aib/ibc/core\"\n\n// assertCoreCaller panics unless the callback is being invoked by the IBC core\n// realm. The App methods are exported, and any realm can obtain a usable *App\n// (a zero-value `var app transfer.App` needs no composite literal), so without\n// this check a foreign realm could call OnRecvPacket to mint vouchers, or the\n// refund paths of OnTimeoutPacket and OnAcknowledgementPacket to release\n// escrow, with no proof and no core involvement. Core crosses into the app, so\n// cur.Previous() is the core realm on every legitimate call.\nfunc assertCoreCaller(_ int, rlm realm) {\n\tif caller := rlm.Previous().PkgPath(); caller != corePkgPath {\n\t\tpanic(ufmt.Sprintf(\"transfer: callback must be invoked by %s, got %s\", corePkgPath, caller))\n\t}\n}\n\n// Implements app.IBCApp\nfunc (a *App) OnSendPacket(\n\tcur realm,\n\tsourceClient string,\n\tdestinationClient string,\n\tsequence uint64,\n\tpayload types.Payload,\n) error {\n\tassertCoreCaller(0, cur)\n\treturn mustImpl().OnSendPacket(0, cur, sourceClient, destinationClient, sequence, payload)\n}\n\n// Implements app.IBCApp\nfunc (a *App) OnRecvPacket(\n\tcur realm,\n\tsourceClient string,\n\tdestinationClient string,\n\tsequence uint64,\n\tpayload types.Payload,\n) types.RecvPacketResult {\n\tassertCoreCaller(0, cur)\n\treturn mustImpl().OnRecvPacket(0, cur, sourceClient, destinationClient, sequence, payload)\n}\n\n// Implements app.IBCApp\nfunc (a *App) OnAcknowledgementPacket(\n\tcur realm,\n\tsourceClient string,\n\tdestinationClient string,\n\tsequence uint64,\n\tacknowledgement []byte,\n\tpayload types.Payload,\n) error {\n\tassertCoreCaller(0, cur)\n\treturn mustImpl().OnAcknowledgementPacket(0, cur, sourceClient, destinationClient, sequence, acknowledgement, payload)\n}\n\n// Implements app.IBCApp\nfunc (a *App) OnTimeoutPacket(\n\tcur realm,\n\tsourceClient string,\n\tdestinationClient string,\n\tsequence uint64,\n\tpayload types.Payload,\n) error {\n\tassertCoreCaller(0, cur)\n\treturn mustImpl().OnTimeoutPacket(0, cur, sourceClient, destinationClient, sequence, payload)\n}\n"},{"name":"controls.gno","body":"package transfer\n\nimport (\n\t\"chain\"\n\t\"chain/runtime/unsafe\"\n\n\t\"gno.land/p/aib/ibc/types/v0\"\n)\n\n// The entry points of the Ethereum route controls (ADR pr6375 §7.1, §7.9,\n// §7.13), forwarded to the active implementation's ControlsLogic.\n\n// Claim pays up to n (at most 20) claims of denom waiting on the route of\n// clientID, within the route's meters, and returns how many it paid something\n// on. Anyone may call it: credits from Ethereum are booked as claims and paid\n// only this way. denom is the escrow denom of a gno-native token (\"ugnot\", a\n// GRC20 alias) or the IBC denom of a voucher.\nfunc Claim(cur realm, clientID, denom string, n int) int {\n\treturn mustControls().Claim(0, cur, clientID, denom, n)\n}\n\n// ClaimRefunds pays up to n (at most 20) refunds of denom queued on the route\n// of clientID under its refund meter, and returns how many it paid something\n// on. Anyone may call it.\nfunc ClaimRefunds(cur realm, clientID, denom string, n int) int {\n\treturn mustControls().ClaimRefunds(0, cur, clientID, denom, n)\n}\n\n// ProveOutcome proves, through a replaced route's replacement Ethereum\n// client, what the destination did with a send of a retired client (ADR\n// pr6375 §7.12 \"Proven outcomes\"): packet is the send (checked against its\n// record's commitment), proofHeight a height of the replacement client,\n// receiptProof the destination router's receipt at the packet's sequence\n// (membership or absence) and ackProof its acknowledgement when the receipt\n// is the packet's. The record keeps the outcome (delivered, refundable,\n// collision); a settlement's action must match it. Anyone may call it.\nfunc ProveOutcome(cur realm, packet types.Packet, proofHeight types.Height, receiptProof, ackProof any) {\n\tmustControls().ProveOutcome(0, cur, packet, proofHeight, receiptProof, ackProof)\n}\n\n// ContinueReopen runs the next bounded batch of route's reopening, which an\n// ibcgov reopen decision started when it had more losses to record as\n// accepted deficits, or authorizations to revoke, than one batch takes (ADR\n// pr6375 §5.4 kind 13). It reports whether the route is open. Anyone may call\n// it.\nfunc ContinueReopen(cur realm, route string) bool {\n\treturn mustControls().ContinueReopen(0, cur, route)\n}\n\n// PrepareReplacement walks up to n (1 to 64) send records of the clients a\n// replacement retired from route, from where the last call stopped, and\n// reserves every refund they may owe and nothing reserves: a success in the\n// suspect interval (a forged acknowledgement may have released it) and an\n// escrowed send still pending (ADR pr6375 §7.12). Until the walk is complete\n// no settlement, sweep payment, credit by vote or reopening applies on the\n// route; the replacement itself runs the first batch. It reports whether the\n// preparation is complete. Anyone may call it.\nfunc PrepareReplacement(cur realm, route string, n int) bool {\n\treturn mustControls().PrepareReplacement(0, cur, route, n)\n}\n\n// Recapitalize pays recapitalization id of route, which an ibcgov decision\n// authorized, into the route's escrow (ADR pr6375 §7.9). For a native denom\n// it must be a direct user call sending exactly the amount; for a GRC20, the\n// caller must have approved this realm for the amount. Anyone may pay it.\nfunc Recapitalize(cur realm, route string, id int64) {\n\tvar sent []chain.Coin\n\tif cur.Previous().IsUserCall() {\n\t\tsent = unsafe.OriginSend()\n\t}\n\tmustControls().Recapitalize(0, cur, sent, route, id)\n}\n\n// GovApply applies a governance operation of the route controls (see\n// gno.land/p/aib/ibc/ics20/controls/v0, OpEnroll...). Only the ibcgov realm\n// calls it, with a decision's parameters; it returns \"\" when applied and the\n// reason otherwise.\nfunc GovApply(cur realm, op string, params []byte) string {\n\tassertGovernance(cur, \"gov_apply\")\n\t// An implementation without the controls (a rollback to impl/v0) refuses\n\t// deterministically, so the decision is spent rather than left pending\n\t// until the controls come back.\n\tc, ok := mustImpl().(ControlsLogic)\n\tif !ok {\n\t\treturn \"the active implementation \" + implVersion + \" keeps no route controls\"\n\t}\n\treturn c.GovApply(0, cur, op, params)\n}\n\n// ExtView returns the read-only view of an implementation's extension state\n// (see Store.ExtView), nil if none: for the route controls, under\n// \"controls\", a gno.land/p/aib/ibc/ics20/controls/v0 Reader.\nfunc ExtView(key string) any {\n\treturn store.ExtView(key)\n}\n"},{"name":"emit.gno","body":"package transfer\n\nimport \"chain\"\n\n// The Emit* functions below keep the transfer events attributed to this\n// realm: chain.Emit stamps the event with the package whose code contains the\n// call, so the implementation emits through them. They are exported because\n// the implementation lives in another package, hence the assertProxyRealm\n// guard.\n\n// EmitTransfer emits ibc_transfer, the record of a send accepted by\n// OnSendPacket.\nfunc EmitTransfer(_ int, rlm realm, sender, receiver, denom, amount, memo string) {\n\tassertProxyRealm(0, rlm)\n\tchain.Emit(EventTypeTransfer,\n\t\tAttributeKeySender, sender,\n\t\tAttributeKeyReceiver, receiver,\n\t\tAttributeKeyDenom, denom,\n\t\tAttributeKeyAmount, amount,\n\t\tAttributeKeyMemo, memo,\n\t)\n}\n\n// EmitPacket emits fungible_token_packet with the given key/value pairs: the\n// outcome of a receive or of an acknowledgement.\nfunc EmitPacket(_ int, rlm realm, attrs ...string) {\n\tassertProxyRealm(0, rlm)\n\tchain.Emit(EventTypePacket, attrs...)\n}\n\n// EmitTimeout emits timeout, the refund of a timed-out send.\nfunc EmitTimeout(_ int, rlm realm, receiver, denom, amount, memo string) {\n\tassertProxyRealm(0, rlm)\n\tchain.Emit(EventTypeTimeout,\n\t\tAttributeKeyReceiver, receiver,\n\t\tAttributeKeyDenom, denom,\n\t\tAttributeKeyAmount, amount,\n\t\tAttributeKeyMemo, memo,\n\t)\n}\n\n// EmitDenom emits denomination, the first sighting of a voucher denom.\nfunc EmitDenom(_ int, rlm realm, denomHash, denomJSON string) {\n\tassertProxyRealm(0, rlm)\n\tchain.Emit(EventTypeDenom,\n\t\tAttributeKeyDenomHash, denomHash,\n\t\tAttributeKeyDenom, denomJSON,\n\t)\n}\n\n// EventTypeRoutePrefix prefixes the events of the Ethereum route controls\n// (ADR pr6375 §7): route_claim, route_refund, route_sweep, route_credit,\n// route_gov and the like, emitted under this realm's pkg_path.\nconst EventTypeRoutePrefix = \"route_\"\n\n// EmitRoute emits route_\u003ckind\u003e with the given key/value pairs.\nfunc EmitRoute(_ int, rlm realm, kind string, attrs ...string) {\n\tassertProxyRealm(0, rlm)\n\tchain.Emit(EventTypeRoutePrefix+kind, attrs...)\n}\n"},{"name":"events.gno","body":"package transfer\n\n// IBC transfer events\nconst (\n\tEventTypeTimeout  = \"timeout\"\n\tEventTypePacket   = \"fungible_token_packet\"\n\tEventTypeTransfer = \"ibc_transfer\"\n\tEventTypeDenom    = \"denomination\"\n\n\tAttributeKeySender         = \"sender\"\n\tAttributeKeyReceiver       = \"receiver\"\n\tAttributeKeyDenom          = \"denom\"\n\tAttributeKeyDenomHash      = \"denom_hash\"\n\tAttributeKeyAmount         = \"amount\"\n\tAttributeKeyRefundReceiver = \"refund_receiver\"\n\tAttributeKeyRefundTokens   = \"refund_tokens\"\n\tAttributeKeyAckSuccess     = \"success\"\n\tAttributeKeyAck            = \"acknowledgement\"\n\tAttributeKeyAckError       = \"error\"\n\tAttributeKeyMemo           = \"memo\"\n)\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/aib/ibc/apps/transfer\"\ngno = \"0.9\"\n"},{"name":"proposals.gno","body":"package transfer\n\nimport \"gno.land/r/gov/dao\"\n\n// GovDAO proposal constructor for the tightening operation of this realm; see\n// r/aib/ibc/core/proposals.gno for the mechanism and the submission flow.\n// Every loosening operation (unblocking, upgrades, membership, the Ethereum\n// route controls) is a decision of gno.land/r/aib/ibc/ibcgov (ADR pr6375 R14).\n\nconst proposalTitlePrefix = \"gno.land/r/aib/ibc/apps/transfer: \"\n\nfunc newProposalRequest(_ int, rlm realm, title, description string, cb func(cur realm) error) dao.ProposalRequest {\n\treturn dao.NewProposalRequest(proposalTitlePrefix+title, description, dao.NewSimpleExecutor(0, rlm, cb, \"\"))\n}\n\n// NewBlockAddressProposalRequest proposes to block addr from sending and receiving.\nfunc NewBlockAddressProposalRequest(cur realm, addr address) dao.ProposalRequest {\n\treturn newProposalRequest(0, cur, \"block address \"+string(addr),\n\t\t\"Block \"+string(addr)+\" in the IBC transfer app: it can no longer send, and packets addressed to it are refused with an error acknowledgement. Refunds already owed to it still go through. Unblocking is a decision of gno.land/r/aib/ibc/ibcgov.\",\n\t\tfunc(cur realm) error { blockAddress(0, cur, addr); return nil })\n}\n"},{"name":"render.gno","body":"package transfer\n\n// Render is forwarded to the implementation, JSON routes included (see\n// r/aib/ibc/core/render.gno for the reasoning). The routes the e2e suite and\n// tooling read, listed on Logic.Render, are pinned by the z0e filetest.\nfunc Render(path string) string {\n\tif impl == nil {\n\t\treturn \"# IBC transfer\\n\\nNo implementation installed.\\n\"\n\t}\n\treturn impl.Render(path)\n}\n"},{"name":"sandbox.gno","body":"package transfer\n\nimport (\n\t\"chain\"\n\t\"strconv\"\n\n\t\"gno.land/p/aib/ibc/ics20/v0\"\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/grc20/v1\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// sandboxStore is the Store a candidate implementation is built against in\n// the upgrade preflight (CheckUpdateImpl): empty, holding no live object and\n// no reference to this realm's state, every read answering nothing and every\n// write refused. The live store reaches candidate code only in an authorized\n// installation.\ntype sandboxStore struct{}\n\nvar _ Store = sandboxStore{}\n\nfunc refuseSandbox() {\n\tpanic(\"transfer: the preflight's sandbox store holds nothing and takes no write\")\n}\n\nfunc emptyTree() bptree.ITree { return readOnlyTree{t: bptree.NewBPTree32()} }\n\nfunc (sandboxStore) HasDenom(string) bool                     { return false }\nfunc (sandboxStore) Denom(string) (ics20.Denom, bool)         { return ics20.Denom{}, false }\nfunc (sandboxStore) SetDenom(_ int, rlm realm, d ics20.Denom) { refuseSandbox() }\nfunc (sandboxStore) Denoms() bptree.ITree                     { return emptyTree() }\n\nfunc (sandboxStore) AddEscrow(_ int, rlm realm, clientID, denom string, amount uint256) {\n\trefuseSandbox()\n}\nfunc (sandboxStore) CheckEscrow(clientID, denom string, amount uint256) error {\n\treturn ufmt.Errorf(\"insufficient escrow for client %s denom %s: have 0, want %s\", clientID, denom, strconv.FormatUint256(amount, 10))\n}\nfunc (sandboxStore) IterateEscrow(fn func(clientID, denom string, amount uint256) bool) {}\nfunc (sandboxStore) Escrowed(clientID, denom string) uint256                            { return 0 }\n\nfunc (sandboxStore) EscrowGRC20(_ int, rlm realm, clientID, denomKey string, from address, amount uint256) error {\n\trefuseSandbox()\n\treturn nil\n}\nfunc (sandboxStore) UnescrowGRC20(_ int, rlm realm, clientID, receiver, denom string, amount uint256) error {\n\trefuseSandbox()\n\treturn nil\n}\nfunc (sandboxStore) UnescrowNative(_ int, rlm realm, clientID, receiver, denom string, amount uint256) error {\n\trefuseSandbox()\n\treturn nil\n}\n\nfunc (sandboxStore) VoucherToken(string) *grc20.Token { return nil }\nfunc (sandboxStore) Vouchers() bptree.ITree           { return emptyTree() }\nfunc (sandboxStore) GetOrCreateVoucher(_ int, rlm realm, baseDenom, voucherDenom string, decimals int) *grc20.Token {\n\trefuseSandbox()\n\treturn nil\n}\nfunc (sandboxStore) MintVoucher(_ int, rlm realm, ibcDenom string, to address, amount uint256) error {\n\trefuseSandbox()\n\treturn nil\n}\nfunc (sandboxStore) BurnVoucher(_ int, rlm realm, ibcDenom string, from address, amount uint256) error {\n\trefuseSandbox()\n\treturn nil\n}\n\nfunc (sandboxStore) SetPendingNativeEscrow(_ int, rlm realm, c chain.Coin) { refuseSandbox() }\nfunc (sandboxStore) PendingNativeEscrow() (chain.Coin, bool)               { return chain.Coin{}, false }\nfunc (sandboxStore) SetPendingVoucherSend(_ int, rlm realm, sender, sourceClient, denom string, amount uint256) {\n\trefuseSandbox()\n}\nfunc (sandboxStore) ConsumePendingVoucherSend(_ int, rlm realm) (PendingSend, bool) {\n\treturn PendingSend{}, false\n}\nfunc (sandboxStore) SetPendingGRC20Send(_ int, rlm realm, sender, sourceClient, denom string, amount uint256) {\n\trefuseSandbox()\n}\nfunc (sandboxStore) ConsumePendingGRC20Send(_ int, rlm realm) (PendingSend, bool) {\n\treturn PendingSend{}, false\n}\n\nfunc (sandboxStore) InitExt(_ int, rlm realm, key string, ctor func() any) { refuseSandbox() }\nfunc (sandboxStore) Ext(_ int, rlm realm, key string) any                  { refuseSandbox(); return nil }\nfunc (sandboxStore) ExtView(key string) any                                { return nil }\n"},{"name":"store.gno","body":"package transfer\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"math\"\n\t\"math/overflow\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/aib/ibc/ics20/v0\"\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/grc20/v1\"\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/nt/grc20reg/v1\"\n)\n\n// The state of the transfer app lives in this realm forever (see ADR 0002):\n// the escrow accounting, the known denominations, the voucher tokens with\n// their mint/burn ledgers, and the sends Transfer authorizes for\n// OnSendPacket. The implementation realm reads and writes it through the\n// Store interface. Every mutator and every value move takes the proxy's own\n// live realm value and asserts it (assertProxyRealm): the implementation is\n// always invoked non-crossing by this realm, so the value it forwards is this\n// realm's cur, and nobody else can present one. The value moves stay here\n// because this realm is the home of the voucher tokens and the holder of the\n// escrow, and because each of them keeps the escrow accounting in step with\n// the balances.\n//\n// Amounts are uint256, the range of an ICS20 amount on the wire and of a\n// grc20 v1 token (ADR pr6375 R15; gno ADR pr6387, pr6388): vouchers and\n// GRC20 escrow count in it exactly. Native coins stay int64, so the two\n// native paths convert at the boundary: a coin that landed here widens\n// (CoinAmount), and a release is refused above math.MaxInt64 (coinOf), which\n// the escrow invariant never reaches for a native denom.\n\n// Store is the proxy-owned state and the value-moving services, as seen by an\n// implementation.\ntype Store interface {\n\t// Denominations known to this chain (minted vouchers), keyed by IBC denom.\n\tHasDenom(ibcDenom string) bool\n\tDenom(ibcDenom string) (ics20.Denom, bool)\n\tSetDenom(_ int, rlm realm, d ics20.Denom)\n\t// Denoms is a read-only view; values are ics20.Denom.\n\tDenoms() bptree.ITree\n\n\t// Escrow accounting per (client, denom), in base units. AddEscrow\n\t// records value that already landed at this realm (a native transfer);\n\t// the GRC20 and release paths below keep the accounting in step with\n\t// the moves.\n\tAddEscrow(_ int, rlm realm, clientID, denom string, amount uint256)\n\tCheckEscrow(clientID, denom string, amount uint256) error\n\t// IterateEscrow walks the accounting, clients in key order and their\n\t// denoms in key order, until fn returns true.\n\tIterateEscrow(fn func(clientID, denom string, amount uint256) bool)\n\t// Escrowed is the amount of denom accounted to clientID (a client id, or\n\t// the route key of a controlled route), 0 if none.\n\tEscrowed(clientID, denom string) uint256\n\n\t// EscrowGRC20 moves amount of the grc20reg token denomKey from `from` to\n\t// this realm (TransferFrom, so the owner must have approved this realm)\n\t// and records it under clientID as the token's alias (GRC20Alias).\n\tEscrowGRC20(_ int, rlm realm, clientID, denomKey string, from address, amount uint256) error\n\t// UnescrowGRC20 and UnescrowNative release amount of denom (a GRC20\n\t// alias, or a native coin denom) to receiver out of what clientID\n\t// escrowed: check, move, then debit. A native amount above\n\t// math.MaxInt64 is refused: no coin holds it.\n\tUnescrowGRC20(_ int, rlm realm, clientID, receiver, denom string, amount uint256) error\n\tUnescrowNative(_ int, rlm realm, clientID, receiver, denom string, amount uint256) error\n\n\t// Vouchers: the tokens and their ledgers never leave this realm.\n\tVoucherToken(ibcDenom string) *grc20.Token\n\t// Vouchers is a read-only view; values are *grc20.Token.\n\tVouchers() bptree.ITree\n\t// GetOrCreateVoucher is the voucher of voucherDenom, created with\n\t// decimals (0 to grc20.MaxDecimals) if it did not exist: an existing\n\t// voucher keeps the decimals it was created with, whatever is passed.\n\tGetOrCreateVoucher(_ int, rlm realm, baseDenom, voucherDenom string, decimals int) *grc20.Token\n\tMintVoucher(_ int, rlm realm, ibcDenom string, to address, amount uint256) error\n\tBurnVoucher(_ int, rlm realm, ibcDenom string, from address, amount uint256) error\n\n\t// The sends Transfer authorizes for OnSendPacket. Transfer clears them on\n\t// every exit path (see transfer.gno); the Consume* readers clear their\n\t// slot as well.\n\tSetPendingNativeEscrow(_ int, rlm realm, c chain.Coin)\n\tPendingNativeEscrow() (chain.Coin, bool)\n\tSetPendingVoucherSend(_ int, rlm realm, sender, sourceClient, denom string, amount uint256)\n\tConsumePendingVoucherSend(_ int, rlm realm) (PendingSend, bool)\n\tSetPendingGRC20Send(_ int, rlm realm, sender, sourceClient, denom string, amount uint256)\n\tConsumePendingGRC20Send(_ int, rlm realm) (PendingSend, bool)\n\n\t// Extension state: values an implementation keeps in this realm under a\n\t// key, beside the frozen state above (the Ethereum route controls of\n\t// ADR pr6375 §7). InitExt stores ctor() under key once, calling ctor here\n\t// so the value is allocated in this realm: ctor must be a /p/-declared\n\t// top-level function (as for core.Store.AddClient). Ext returns the\n\t// stored value, mutable, to the implementation only. ExtView returns its\n\t// read-only view (the value's ReadOnly() result), which anyone may read.\n\tInitExt(_ int, rlm realm, key string, ctor func() any)\n\tExt(_ int, rlm realm, key string) any\n\tExtView(key string) any\n}\n\n// ReadOnlyer is implemented by extension values: ReadOnly returns a view\n// that exposes no mutator, which ExtView hands to anyone.\ntype ReadOnlyer interface {\n\tReadOnly() any\n}\n\n// PendingSend records the token send that Transfer authorized for\n// OnSendPacket: the EOA that signed, the client, the denom path and the\n// amount the packet must carry.\ntype PendingSend struct {\n\tSender       string\n\tSourceClient string\n\tDenom        string\n\tAmount       uint256\n}\n\nvar (\n\tdenoms        *bptree.BPTree // ibcDenom:ics20.Denom\n\ttotalEscrow   *bptree.BPTree // clientID:*bptree.BPTree(denom:uint256)\n\tvoucherTokens *bptree.BPTree // ibcDenom:*voucher\n\n\t// nextVoucherID allocates a unique grc20 token id for each voucher, and\n\t// with it the voucher's symbol (see VoucherSymbol).\n\tnextVoucherID seqid.ID\n\n\t// Gno persists package-level vars to the realm store at end of tx. These\n\t// pending values are cleared before Transfer returns (its defer) so a\n\t// leftover never bleeds into a later tx.\n\t//\n\t// pendingNativeEscrow is set by the native branch of Transfer and read by\n\t// OnSendPacket. It bridges the user-call boundary check, which is only\n\t// possible in Transfer where PreviousRealm() is the EOA, to the escrow\n\t// accounting done in OnSendPacket.\n\tpendingNativeEscrow *chain.Coin\n\t// ext holds the implementations' extension state (see Store.InitExt).\n\text = bptree.NewBPTree32() // key -\u003e value\n\n\t// pendingVoucherSend and pendingGRC20Send are set by the voucher and\n\t// GRC20 branches of Transfer after the direct user-call guard passes.\n\t// OnSendPacket consumes them before burning or escrowing user-owned\n\t// tokens, preventing direct core.SendPacket bypasses and binding the\n\t// packet to the asset the user explicitly requested.\n\tpendingVoucherSend *PendingSend\n\tpendingGRC20Send   *PendingSend\n)\n\n// voucher holds a voucher token and its private ledger for mint/burn.\ntype voucher struct {\n\ttoken  *grc20.Token\n\tledger *grc20.PrivateLedger\n\tsymbol string // its grc20 symbol and registry key's symbol, allocated once\n}\n\nfunc init() {\n\tdenoms = bptree.NewBPTree32()\n\ttotalEscrow = bptree.NewBPTree32()\n\tvoucherTokens = bptree.NewBPTree32()\n}\n\n// transferStore implements Store over the package-level state.\ntype transferStore struct{}\n\nvar (\n\t_     Store = transferStore{}\n\tstore Store = transferStore{}\n)\n\nfunc (transferStore) HasDenom(ibcDenom string) bool { return hasDenom(ibcDenom) }\n\nfunc (transferStore) Denom(ibcDenom string) (ics20.Denom, bool) { return getDenom(ibcDenom) }\n\nfunc (transferStore) SetDenom(_ int, rlm realm, d ics20.Denom) {\n\tassertProxyRealm(0, rlm)\n\tsetDenom(d)\n}\n\nfunc (transferStore) Denoms() bptree.ITree { return readOnlyTree{t: denoms} }\n\nfunc (transferStore) AddEscrow(_ int, rlm realm, clientID, denom string, amount uint256) {\n\tassertProxyRealm(0, rlm)\n\taddEscrowForClient(clientID, denom, amount)\n}\n\nfunc (transferStore) CheckEscrow(clientID, denom string, amount uint256) error {\n\treturn checkEscrowForClient(clientID, denom, amount)\n}\n\nfunc (transferStore) IterateEscrow(fn func(clientID, denom string, amount uint256) bool) {\n\ttotalEscrow.IterateByOffset(0, totalEscrow.Size(), func(clientID string, v any) bool {\n\t\tt := v.(*bptree.BPTree)\n\t\tstop := false\n\t\tt.IterateByOffset(0, t.Size(), func(denom string, cv any) bool {\n\t\t\tstop = fn(clientID, denom, cv.(uint256))\n\t\t\treturn stop\n\t\t})\n\t\treturn stop\n\t})\n}\n\nfunc (transferStore) Escrowed(clientID, denom string) uint256 {\n\tt := getEscrowForClient(clientID)\n\tif t == nil {\n\t\treturn 0\n\t}\n\tx := t.Get(denom)\n\tif x == nil {\n\t\treturn 0\n\t}\n\treturn x.(uint256)\n}\n\nfunc (transferStore) InitExt(_ int, rlm realm, key string, ctor func() any) {\n\tassertProxyRealm(0, rlm)\n\tif ext.Has(key) {\n\t\treturn\n\t}\n\tif ctor == nil {\n\t\tpanic(\"InitExt: nil constructor\")\n\t}\n\tv := ctor()\n\tif _, ok := v.(ReadOnlyer); !ok {\n\t\tpanic(\"InitExt: the value has no read-only view\")\n\t}\n\text.Set(key, v)\n}\n\nfunc (transferStore) Ext(_ int, rlm realm, key string) any {\n\tassertProxyRealm(0, rlm)\n\treturn ext.Get(key)\n}\n\nfunc (transferStore) ExtView(key string) any {\n\tv := ext.Get(key)\n\tif v == nil {\n\t\treturn nil\n\t}\n\treturn v.(ReadOnlyer).ReadOnly()\n}\n\nfunc (transferStore) EscrowGRC20(_ int, rlm realm, clientID, denomKey string, from address, amount uint256) error {\n\tassertProxyRealm(0, rlm)\n\ttoken := grc20reg.Get(denomKey)\n\tif token == nil {\n\t\treturn ufmt.Errorf(\"GRC20 token %s not found in grc20reg\", denomKey)\n\t}\n\tteller := token.RealmTeller(0, rlm)\n\tif err := teller.TransferFrom(0, rlm, from, rlm.Address(), amount); err != nil {\n\t\treturn ufmt.Errorf(\"escrow GRC20 %s error: %v\", denomKey, err)\n\t}\n\taddEscrowForClient(clientID, GRC20Alias(denomKey), amount)\n\treturn nil\n}\n\nfunc (transferStore) UnescrowGRC20(_ int, rlm realm, clientID, receiver, denom string, amount uint256) error {\n\tassertProxyRealm(0, rlm)\n\treturn unescrowGRC20(0, rlm, clientID, receiver, denom, amount)\n}\n\nfunc (transferStore) UnescrowNative(_ int, rlm realm, clientID, receiver, denom string, amount uint256) error {\n\tassertProxyRealm(0, rlm)\n\treturn unescrowNative(0, rlm, clientID, receiver, denom, amount)\n}\n\nfunc (transferStore) VoucherToken(ibcDenom string) *grc20.Token {\n\tinst := getVoucher(ibcDenom)\n\tif inst == nil {\n\t\treturn nil\n\t}\n\treturn inst.token\n}\n\nfunc (transferStore) Vouchers() bptree.ITree {\n\treturn readOnlyTree{t: voucherTokens, mapValue: func(v any) any { return v.(*voucher).token }}\n}\n\nfunc (transferStore) GetOrCreateVoucher(_ int, rlm realm, baseDenom, voucherDenom string, decimals int) *grc20.Token {\n\tassertProxyRealm(0, rlm)\n\treturn getOrCreateVoucher(0, rlm, baseDenom, voucherDenom, decimals).token\n}\n\nfunc (transferStore) MintVoucher(_ int, rlm realm, ibcDenom string, to address, amount uint256) error {\n\tassertProxyRealm(0, rlm)\n\tinst := getVoucher(ibcDenom)\n\tif inst == nil {\n\t\treturn ufmt.Errorf(\"voucher token not found for denom %s\", ibcDenom)\n\t}\n\treturn inst.ledger.Mint(to, amount)\n}\n\nfunc (transferStore) BurnVoucher(_ int, rlm realm, ibcDenom string, from address, amount uint256) error {\n\tassertProxyRealm(0, rlm)\n\tinst := getVoucher(ibcDenom)\n\tif inst == nil {\n\t\treturn ufmt.Errorf(\"voucher token not found for denom %s\", ibcDenom)\n\t}\n\treturn inst.ledger.Burn(from, amount)\n}\n\nfunc (transferStore) SetPendingNativeEscrow(_ int, rlm realm, c chain.Coin) {\n\tassertProxyRealm(0, rlm)\n\tpendingNativeEscrow = \u0026c\n}\n\nfunc (transferStore) PendingNativeEscrow() (chain.Coin, bool) {\n\tif pendingNativeEscrow == nil {\n\t\treturn chain.Coin{}, false\n\t}\n\treturn *pendingNativeEscrow, true\n}\n\nfunc (transferStore) SetPendingVoucherSend(_ int, rlm realm, sender, sourceClient, denom string, amount uint256) {\n\tassertProxyRealm(0, rlm)\n\tpendingVoucherSend = \u0026PendingSend{Sender: sender, SourceClient: sourceClient, Denom: denom, Amount: amount}\n}\n\nfunc (transferStore) ConsumePendingVoucherSend(_ int, rlm realm) (PendingSend, bool) {\n\tassertProxyRealm(0, rlm)\n\tif pendingVoucherSend == nil {\n\t\treturn PendingSend{}, false\n\t}\n\tps := *pendingVoucherSend\n\tpendingVoucherSend = nil\n\treturn ps, true\n}\n\nfunc (transferStore) SetPendingGRC20Send(_ int, rlm realm, sender, sourceClient, denom string, amount uint256) {\n\tassertProxyRealm(0, rlm)\n\tpendingGRC20Send = \u0026PendingSend{Sender: sender, SourceClient: sourceClient, Denom: denom, Amount: amount}\n}\n\nfunc (transferStore) ConsumePendingGRC20Send(_ int, rlm realm) (PendingSend, bool) {\n\tassertProxyRealm(0, rlm)\n\tif pendingGRC20Send == nil {\n\t\treturn PendingSend{}, false\n\t}\n\tps := *pendingGRC20Send\n\tpendingGRC20Send = nil\n\treturn ps, true\n}\n\n// clearPendingSends drops whatever Transfer authorized for the current call.\nfunc clearPendingSends() {\n\tpendingNativeEscrow = nil\n\tpendingVoucherSend = nil\n\tpendingGRC20Send = nil\n}\n\n// readOnlyTree is the view of a proxy-owned B+ tree handed to the\n// implementation for rendering. Handing the tree itself would hand a write\n// capability (a /p/ method on a foreign receiver runs with the receiver's\n// storage authority), and the voucher tree holds the mint/burn ledgers, so\n// mapValue substitutes the token for the voucher entry. The view implements\n// bptree.ITree, which jsonpage paginates, and panics on the mutators.\ntype readOnlyTree struct {\n\tt        *bptree.BPTree\n\tmapValue func(any) any\n}\n\nvar _ bptree.ITree = readOnlyTree{}\n\nfunc (r readOnlyTree) value(v any) any {\n\tif v == nil || r.mapValue == nil {\n\t\treturn v\n\t}\n\treturn r.mapValue(v)\n}\n\nfunc (r readOnlyTree) wrap(cb bptree.IterCbFn) bptree.IterCbFn {\n\treturn func(k string, v any) bool { return cb(k, r.value(v)) }\n}\n\nfunc (r readOnlyTree) Size() int           { return r.t.Size() }\nfunc (r readOnlyTree) Has(key string) bool { return r.t.Has(key) }\nfunc (r readOnlyTree) Get(key string) any  { return r.value(r.t.Get(key)) }\nfunc (r readOnlyTree) GetByIndex(i int) (string, any) {\n\tk, v := r.t.GetByIndex(i)\n\treturn k, r.value(v)\n}\nfunc (r readOnlyTree) Iterate(start, end string, cb bptree.IterCbFn) bool {\n\treturn r.t.Iterate(start, end, r.wrap(cb))\n}\nfunc (r readOnlyTree) ReverseIterate(start, end string, cb bptree.IterCbFn) bool {\n\treturn r.t.ReverseIterate(start, end, r.wrap(cb))\n}\nfunc (r readOnlyTree) IterateByOffset(offset, count int, cb bptree.IterCbFn) bool {\n\treturn r.t.IterateByOffset(offset, count, r.wrap(cb))\n}\nfunc (r readOnlyTree) ReverseIterateByOffset(offset, count int, cb bptree.IterCbFn) bool {\n\treturn r.t.ReverseIterateByOffset(offset, count, r.wrap(cb))\n}\nfunc (readOnlyTree) Set(string, any) bool      { panic(\"transfer: read-only tree\") }\nfunc (readOnlyTree) Remove(string) (any, bool) { panic(\"transfer: read-only tree\") }\n\n// CoinAmount is a coin's amount as a uint256: the widening of a native coin\n// that landed at this realm. A coin never holds a negative amount.\nfunc CoinAmount(c chain.Coin) uint256 {\n\tif c.Amount \u003c 0 {\n\t\tpanic(\"transfer: a negative coin amount\")\n\t}\n\treturn uint256(c.Amount)\n}\n\n// coinOf is amount of denom as a coin: refused above math.MaxInt64, the most\n// a coin holds. A native denom's escrow is made of coins, so a release within\n// it always fits; the check keeps the conversion from ever truncating.\nfunc coinOf(denom string, amount uint256) (chain.Coin, error) {\n\tif amount \u003e math.MaxInt64 {\n\t\treturn chain.Coin{}, ufmt.Errorf(\"%s %s is above the chain's coin maximum\", strconv.FormatUint256(amount, 10), denom)\n\t}\n\treturn chain.NewCoin(denom, int64(amount)), nil\n}\n\n// escrowForClient returns the per-client denom-\u003eamount tree, creating it on\n// first access. Escrow is accounted per (client, denom) so a malicious client\n// cannot release funds escrowed under a different client (see shared-escrow\n// drain).\nfunc escrowForClient(clientID string) *bptree.BPTree {\n\tt := getEscrowForClient(clientID)\n\tif t == nil {\n\t\tt = bptree.NewBPTree32()\n\t\ttotalEscrow.Set(clientID, t)\n\t}\n\treturn t\n}\n\n// getEscrowForClient returns the per-client tree without creating it. Returns\n// nil if clientID has never escrowed anything. Used by read-only paths\n// (checkEscrowForClient, rendering) so a query for an unknown client doesn't\n// allocate an empty entry in totalEscrow.\nfunc getEscrowForClient(clientID string) *bptree.BPTree {\n\tv := totalEscrow.Get(clientID)\n\tif v == nil {\n\t\treturn nil\n\t}\n\treturn v.(*bptree.BPTree)\n}\n\n// addEscrowForClient records amount of denom escrowed under clientID. Used\n// when this chain sends tokens over clientID (OnSendPacket); the matching\n// release can only come back through the same client.\nfunc addEscrowForClient(clientID, denom string, amount uint256) {\n\tt := escrowForClient(clientID)\n\tif x := t.Get(denom); x != nil {\n\t\ts, ok := overflow.Addu256(x.(uint256), amount)\n\t\tif !ok {\n\t\t\tpanic(\"transfer: the escrow accounting would overflow\")\n\t\t}\n\t\tamount = s\n\t}\n\tt.Set(denom, amount)\n}\n\n// checkEscrowForClient verifies clientID locked enough of denom, without\n// mutating the accounting. Read-only so callers can move tokens between the\n// check and the debit (see unescrowGRC20/unescrowNative).\nfunc checkEscrowForClient(clientID, denom string, amount uint256) error {\n\tvar have uint256\n\tif t := getEscrowForClient(clientID); t != nil {\n\t\tif x := t.Get(denom); x != nil {\n\t\t\thave = x.(uint256)\n\t\t}\n\t}\n\tif have \u003c amount {\n\t\treturn ufmt.Errorf(\"insufficient escrow for client %s denom %s: have %s, want %s\", clientID, denom,\n\t\t\tstrconv.FormatUint256(have, 10), strconv.FormatUint256(amount, 10))\n\t}\n\treturn nil\n}\n\n// debitEscrowForClient subtracts amount from clientID's balance of denom.\n// Assumes checkEscrowForClient already passed.\nfunc debitEscrowForClient(clientID, denom string, amount uint256) {\n\tt := escrowForClient(clientID)\n\thave := t.Get(denom).(uint256)\n\tt.Set(denom, have-amount)\n}\n\n// unescrowGRC20 returns escrowed GRC20 tokens to receiver. The check runs\n// before the transfer (per-client drain defense), the debit only after a\n// successful transfer: OnRecvPacket returning Failure does not roll back realm\n// store writes (IBCApp contract), so debiting first would lock the funds on a\n// transfer failure.\nfunc unescrowGRC20(_ int, rlm realm, clientID, receiver, denom string, amount uint256) error {\n\tdenomKey := ResolveGRC20Alias(denom)\n\ttoken := grc20reg.Get(denomKey)\n\tif token == nil {\n\t\treturn ufmt.Errorf(\"GRC20 token %s not found in grc20reg\", denomKey)\n\t}\n\tif err := checkEscrowForClient(clientID, denom, amount); err != nil {\n\t\treturn err\n\t}\n\tteller := token.RealmTeller(0, rlm)\n\tif err := teller.Transfer(0, rlm, address(receiver), amount); err != nil {\n\t\treturn ufmt.Errorf(\"unescrow GRC20 %s error: %v\", denomKey, err)\n\t}\n\tdebitEscrowForClient(clientID, denom, amount)\n\treturn nil\n}\n\n// unescrowNative returns escrowed native coins to receiver. Same\n// check-before-mutate ordering as unescrowGRC20: banker.SendCoins has no error\n// return, but keeping the debit after the send avoids a lost-debit if that ever\n// changes. Per-client isolation is enforced by checkEscrowForClient's\n// accounting, not the banker, since the realm holds a single physical escrow\n// account (no per-client addresses in Gno).\nfunc unescrowNative(_ int, rlm realm, clientID, receiver, denom string, amount uint256) error {\n\tif err := checkEscrowForClient(clientID, denom, amount); err != nil {\n\t\treturn err\n\t}\n\tcoin, err := coinOf(denom, amount)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar (\n\t\t// TODO no app/clientID specific escrow address ?\n\t\tfrom   = rlm.Address()\n\t\tto     = address(receiver)\n\t\tbanker = banker.NewBanker(banker.BankerTypeRealmSend, rlm)\n\t)\n\tbanker.SendCoins(from, to, chain.NewCoins(coin))\n\tdebitEscrowForClient(clientID, denom, amount)\n\treturn nil\n}\n\nfunc hasDenom(voucherDenom string) bool {\n\treturn denoms.Has(voucherDenom)\n}\n\nfunc setDenom(denom ics20.Denom) {\n\tdenoms.Set(denom.IBCDenom(), denom)\n}\n\nfunc getDenom(ibcDenom string) (ics20.Denom, bool) {\n\td := denoms.Get(ibcDenom)\n\tif d == nil {\n\t\treturn ics20.Denom{}, false\n\t}\n\treturn d.(ics20.Denom), true\n}\n\n// VoucherSymbol returns the grc20 token symbol of the voucher of an IBC\n// denom, \"\" if it has none yet. A voucher's symbol is allocated from a\n// monotonic counter when the voucher is created (\"IBC-\" and the voucher's id\n// in base 36, upper case: IBC-1, IBC-2, ..., within grc20's MaxSymbolLen),\n// so no two vouchers ever share one, whatever their hashes; the full IBC\n// hash stays the voucher's identity (its denom, and the grc20reg slug).\n// grc20reg keys tokens by rlmpath.symbol, so callers locate a voucher via\n// grc20reg.Get(\u003cthis realm's pkgpath\u003e + \".\" + VoucherSymbol(ibcDenom)).\nfunc VoucherSymbol(ibcDenom string) string {\n\tv := voucherTokens.Get(ibcDenom)\n\tif v == nil {\n\t\treturn \"\"\n\t}\n\treturn v.(*voucher).symbol\n}\n\n// VoucherDecimals returns the decimals of the voucher of an IBC denom, and\n// whether it has one yet. A voucher's decimals are fixed when it is created\n// (getOrCreateVoucher): a route's configuration naming others is refused.\nfunc VoucherDecimals(ibcDenom string) (int, bool) {\n\tv := voucherTokens.Get(ibcDenom)\n\tif v == nil {\n\t\treturn 0, false\n\t}\n\treturn v.(*voucher).token.GetDecimals(), true\n}\n\n// voucherSymbolOf is the symbol of the voucher with grc20 id id.\nfunc voucherSymbolOf(id uint64) string {\n\tsym := \"IBC-\" + strings.ToUpper(strconv.FormatUint(id, 36))\n\tif len(sym) \u003e grc20.MaxSymbolLen {\n\t\tpanic(\"transfer: voucher symbols are exhausted\")\n\t}\n\treturn sym\n}\n\n// getOrCreateVoucher returns the existing voucher instance for the given IBC\n// denom, or creates a new one and registers it in grc20reg for DeFi\n// discoverability. Non-crossing helper; rlm is needed to issue the cross\n// call into grc20reg.Register from this realm's frame.\n//\n// The token's name is the base denom, its symbol is allocated from the\n// voucher ids (see VoucherSymbol), and its grc20reg slug is the full hash:\n// the registry key rlmpath.symbol is unique per voucher, so no denom,\n// whatever its hash, can take another's key. Its decimals are the\n// implementation's to name (an ICS20 packet carries no token metadata: on an\n// Ethereum route the denom's governed configuration, 0 elsewhere), and\n// grc20 fixes them at creation: an existing voucher keeps its own.\nfunc getOrCreateVoucher(_ int, rlm realm, baseDenom, voucherDenom string, decimals int) *voucher {\n\tif v := voucherTokens.Get(voucherDenom); v != nil {\n\t\treturn v.(*voucher)\n\t}\n\tid := nextVoucherID.Next()\n\tsym := voucherSymbolOf(uint64(id))\n\ttoken, ledger := grc20.NewToken(baseDenom, sym, decimals, id, rlm)\n\tinst := \u0026voucher{token: token, ledger: ledger, symbol: sym}\n\tvoucherTokens.Set(voucherDenom, inst)\n\tgrc20reg.Register(cross(rlm), token, voucherDenom[len(\"ibc/\"):])\n\treturn inst\n}\n\n// getVoucher returns the voucher instance for the given IBC denom, or nil if not\n// found.\nfunc getVoucher(ibcDenom string) *voucher {\n\tv := voucherTokens.Get(ibcDenom)\n\tif v == nil {\n\t\treturn nil\n\t}\n\treturn v.(*voucher)\n}\n\n// VoucherBalanceOf returns the balance of a voucher token for a given address.\n// Returns 0 if the token does not exist.\nfunc VoucherBalanceOf(ibcDenom string, addr address) uint256 {\n\tinst := getVoucher(ibcDenom)\n\tif inst == nil {\n\t\treturn 0\n\t}\n\treturn inst.token.BalanceOf(addr)\n}\n\n// GRC20Alias returns a slash-free alias for a GRC20 denom (grc20reg key)\n// by replacing \"/\" with \":\". This is safe because grc20reg slugs are\n// alphanumeric (enforced by grc20reg.Register), so colons never appear in\n// grc20reg keys, making the replacement reversible.\n// Examples:\n//\n//\t\"gno.land/r/demo/foo\"      → \"gno.land:r:demo:foo\"\n//\t\"gno.land/r/demo/foo.FOO\"  → \"gno.land:r:demo:foo.FOO\"\nfunc GRC20Alias(grc20regKey string) string {\n\treturn strings.ReplaceAll(grc20regKey, \"/\", \":\")\n}\n\n// ResolveGRC20Alias converts a GRC20 alias back to the grc20reg key\n// by replacing \":\" with \"/\".\nfunc ResolveGRC20Alias(alias string) string {\n\treturn strings.ReplaceAll(alias, \":\", \"/\")\n}\n\n// IsGRC20Alias returns true if the denom is a GRC20 alias.\n// GRC20 aliases always start with \"gno.land:\" (the colon form of \"gno.land/\").\nfunc IsGRC20Alias(denom string) bool {\n\treturn strings.HasPrefix(denom, \"gno.land:\")\n}\n"},{"name":"transfer.gno","body":"package transfer\n\nimport (\n\t\"chain/runtime/unsafe\"\n\n\t\"gno.land/p/aib/ibc/types/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Transfer is the entry point for sending tokens to another chain using the\n// IBC protocol. It supports three token types, detected automatically from the\n// denom argument:\n//   - IBC voucher (denom starts with \"ibc/\"): burns the voucher GRC20 token.\n//   - Non-IBC GRC20 token (registered in grc20reg): escrows via TransferFrom.\n//   - Native coin (everything else): the coin must be sent with the transaction\n//     via the send field, and must match denom and amount.\n//\n// memo is included in the IBC packet payload. amount is in base units, a\n// uint256: a native coin's amount (plus the route fee on an Ethereum route)\n// must fit an int64, the chain's coin maximum.\n//\n// The classification and the packet construction are the implementation's\n// (see upgrade.gno). Three things stay here. The value-moving authority must\n// come from a direct user call, because the packet sender is the tx origin and\n// unsafe.OriginSend describes coins that actually landed at this realm (an\n// intermediate realm or a `maketx run` script can attach coins to the tx and\n// spend them elsewhere). The send envelope is read here and handed to the\n// implementation: the VM attributes the read to the realm whose code performs\n// it, and refuses a MsgCall whose coins the paid realm never observed, so a\n// read from the implementation's code would not count (the coins would be\n// reported as stranded). And the sends the implementation records for\n// OnSendPacket are cleared on every exit path, so nothing bleeds into a later\n// transaction.\nfunc Transfer(cur realm, clientID, receiver, denom string, amount uint256, timeoutTimestamp uint64, memo string) (\n\tpacket types.MsgSendPacket, sequence uint64,\n) {\n\tif !cur.Previous().IsUserCall() {\n\t\tpanic(\"transfer must be a direct user call (maketx call)\")\n\t}\n\tdefer clearPendingSends()\n\treturn mustImpl().Transfer(0, cur, unsafe.OriginSend(), clientID, receiver, denom, amount, timeoutTimestamp, memo)\n}\n\n// VoucherSend is a convenience helper to send an IBC voucher token from the\n// caller to the provided recipient. The recipient is taken in its canonical\n// spelling: the ledger keys balances by the string, so an uppercase one\n// would credit a balance its owner cannot spend.\nfunc VoucherSend(cur realm, ibcDenom string, to address, amount uint256) {\n\tto = mustCanonicalAddress(to)\n\tinst := getVoucher(ibcDenom)\n\tif inst == nil {\n\t\tpanic(ufmt.Errorf(\"voucher token not found for denom %s\", ibcDenom))\n\t}\n\tteller := inst.ledger.ImpersonateTeller(cur.Previous().Address())\n\tif err := teller.Transfer(0, cur, to, amount); err != nil {\n\t\tpanic(ufmt.Errorf(\"transfer voucher %s error: %v\", ibcDenom, err))\n\t}\n}\n\n// VoucherApprove is a convenience helper to set a spender allowance for an IBC\n// voucher token on behalf of the caller, for the spender's canonical spelling.\nfunc VoucherApprove(cur realm, ibcDenom string, spender address, amount uint256) {\n\tspender = mustCanonicalAddress(spender)\n\tinst := getVoucher(ibcDenom)\n\tif inst == nil {\n\t\tpanic(ufmt.Errorf(\"voucher token not found for denom %s\", ibcDenom))\n\t}\n\tteller := inst.ledger.ImpersonateTeller(cur.Previous().Address())\n\tif err := teller.Approve(0, cur, spender, amount); err != nil {\n\t\tpanic(ufmt.Errorf(\"approve voucher %s error: %v\", ibcDenom, err))\n\t}\n}\n"},{"name":"upgrade.gno","body":"package transfer\n\nimport (\n\t\"chain\"\n\t\"strings\"\n\n\t\"gno.land/p/aib/ibc/types/v0\"\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// PkgPath is the permanent package path of this proxy realm. It is part of\n// the on-chain contract (see ADR 0002): this realm's address holds the\n// escrow and the user approvals, the voucher tokens are registered in\n// grc20reg under this path, and the IBC core routes port \"transfer\" to the\n// App value this realm registered. That is why the ICS-20 logic lives behind\n// the Logic interface in a separate implementation realm, invoked\n// non-crossing, while this realm stays a thin, frozen proxy at this path.\nconst PkgPath = \"gno.land/r/aib/ibc/apps/transfer\"\n\n// implPathPrefix is the namespace implementations must live in.\nconst implPathPrefix = PkgPath + \"/impl/\"\n\nconst (\n\tEventTypeImplUpdated = \"impl_updated\"\n\tAttributeKeyOldPath  = \"old_path\"\n\tAttributeKeyNewPath  = \"new_path\"\n\tAttributeKeyVersion  = \"version\"\n)\n\n// Logic is the swappable ICS-20 implementation. Methods take the non-crossing\n// (_ int, rlm realm, ...) form on purpose: the proxy forwards its own cur\n// without a realm transition, so the current realm stays PkgPath throughout.\n// The implementation therefore runs \"as\" this proxy: rlm.Previous() is the\n// proxy's caller (the EOA for Transfer, the IBC core for the callbacks), and\n// the gated Store services and Emit* wrappers accept rlm.\ntype Logic interface {\n\t// Version identifies the implementation, shown in Render and in the\n\t// impl_updated event.\n\tVersion() string\n\t// OnInstall runs inside UpdateImpl, after the implementation is built\n\t// against the store and before any entry point reaches it. It is the\n\t// migration hook, with the contract of the core's Logic.OnInstall: do\n\t// nothing when prevVersion is this version, migrate when it is the\n\t// version this one follows, panic otherwise. A panic aborts the whole\n\t// transaction, so the previous implementation stays active and nothing\n\t// the hook wrote persists. prevPath and prevVersion are empty at\n\t// bootstrap.\n\tOnInstall(_ int, rlm realm, prevPath, prevVersion string)\n\t// Render serves every path: the proxy's Render forwards to it. The\n\t// routes the e2e suite and tooling read (denoms, vouchers, voucher\n\t// balances, total escrow) are pinned by the z0e filetest.\n\tRender(path string) string\n\n\t// Transfer is the user-side logic behind the Transfer entry point: it\n\t// classifies the denom, records the send it authorizes for OnSendPacket\n\t// in the store, builds the packet and sends it through the IBC core.\n\t// sent is the send envelope of the transaction, read by the proxy (see\n\t// transfer.gno for why the implementation must not read it itself).\n\tTransfer(_ int, rlm realm, sent []chain.Coin, clientID, receiver, denom string, amount uint256, timeoutTimestamp uint64, memo string) (packet types.MsgSendPacket, sequence uint64)\n\n\t// The app.IBCApp callbacks, in the non-crossing form.\n\tOnSendPacket(_ int, rlm realm, sourceClient, destinationClient string, sequence uint64, payload types.Payload) error\n\tOnRecvPacket(_ int, rlm realm, sourceClient, destinationClient string, sequence uint64, payload types.Payload) types.RecvPacketResult\n\tOnTimeoutPacket(_ int, rlm realm, sourceClient, destinationClient string, sequence uint64, payload types.Payload) error\n\tOnAcknowledgementPacket(_ int, rlm realm, sourceClient, destinationClient string, sequence uint64, acknowledgement []byte, payload types.Payload) error\n}\n\n// ControlsLogic is implemented by an implementation that keeps the Ethereum\n// route controls of ADR pr6375 §7 (impl/v1): the permissionless payouts of\n// the claim queue and of the queued refunds, the recapitalization of a route's escrow, and the\n// governance operations ibcgov decides. The proxy's entry points Claim,\n// ClaimRefunds, ProveOutcome, ContinueReopen, PrepareReplacement, Recapitalize and GovApply forward to it\n// non-crossing, as the other entry points do; an implementation without it\n// refuses them.\ntype ControlsLogic interface {\n\t// Claim pays up to n claims of denom on the route of clientID and\n\t// returns how many it paid something on.\n\tClaim(_ int, rlm realm, clientID, denom string, n int) int\n\t// ClaimRefunds pays up to n queued refunds of denom on the route of\n\t// clientID and returns how many it paid something on.\n\tClaimRefunds(_ int, rlm realm, clientID, denom string, n int) int\n\t// ProveOutcome stores the destination's outcome of a retired client's\n\t// send, proven through the route's replacement client.\n\tProveOutcome(_ int, rlm realm, packet types.Packet, proofHeight types.Height, receiptProof, ackProof any)\n\t// ContinueReopen runs the next batch of a route's reopening and\n\t// reports whether the route is open.\n\tContinueReopen(_ int, rlm realm, route string) bool\n\t// PrepareReplacement walks the next batch of a replaced route's retired\n\t// records, reserving the refunds they may owe, and reports whether the\n\t// preparation is complete.\n\tPrepareReplacement(_ int, rlm realm, route string, n int) bool\n\t// Recapitalize pays an authorized recapitalization into the escrow of\n\t// route: sent is the send envelope of a direct user call (a native\n\t// denom), empty otherwise (a GRC20, taken from the caller's allowance).\n\tRecapitalize(_ int, rlm realm, sent []chain.Coin, route string, id int64)\n\t// GovApply applies a governance operation; it returns \"\" when applied\n\t// and the reason otherwise, which spends the decision. Only ibcgov's\n\t// calls reach it.\n\tGovApply(_ int, rlm realm, op string, params []byte) string\n}\n\nvar (\n\t// impl is the active implementation; nil until the first registration.\n\timpl        Logic\n\timplPath    string\n\timplVersion string\n\t// implCtors holds the registered constructors keyed by implementation\n\t// path. A constructor receives the proxy-owned store so a swapped\n\t// implementation reads and writes the same persistent state.\n\timplCtors = bptree.NewBPTree32() // path -\u003e func(Store) Logic\n\t// implAddrs indexes every registered implementation by its realm\n\t// address, superseded ones included (a registry only grows): bridge\n\t// membership is one keyed lookup (IsImplAddress), whatever the number\n\t// registered.\n\timplAddrs = bptree.NewBPTree32() // realm address -\u003e path\n\t// implManifests holds what each registered implementation declared\n\t// about itself, store-free: the preflight reads it and never builds a\n\t// candidate (ADR 0004, R14).\n\timplManifests = bptree.NewBPTree32() // path -\u003e manifest\n)\n\n// manifest is what an implementation declares about itself when it\n// registers, as plain values. KeepsControls answers from it alone;\n// CheckUpdateImpl checks it, then builds the candidate against the empty\n// sandboxStore (never the live store) and checks the built version and\n// controls answer against it: candidate code runs in the preflight, but\n// nothing is handed the live store before a governed installation. The\n// installation checks the built implementation against it again.\ntype manifest struct {\n\t// Version is the Version() the implementation reports.\n\tVersion string\n\t// Over lists the versions of an active implementation it installs over\n\t// (\"\" at bootstrap); it always re-installs over its own version. nil\n\t// installs over any.\n\tOver []string\n\t// KeepsControls declares that it implements ControlsLogic.\n\tKeepsControls bool\n}\n\n// accepts reports whether m installs over an active implementation of\n// version prev.\nfunc (m manifest) accepts(prev string) bool {\n\tif m.Over == nil || prev == m.Version {\n\t\treturn true\n\t}\n\tfor _, v := range m.Over {\n\t\tif v == prev {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// newManifest builds a manifest in this realm's storage: the caller's slice\n// stays the caller's.\nfunc newManifest(version string, over []string, keepsControls bool) manifest {\n\tm := manifest{Version: version, KeepsControls: keepsControls}\n\tif over != nil {\n\t\tm.Over = append([]string{}, over...)\n\t}\n\treturn m\n}\n\n// assertProxyRealm guards the exported store mutators, the value moves and\n// the Emit* wrappers. The active implementation is always invoked\n// non-crossing by this realm, so the realm value it forwards is this realm's\n// own live cur: rlm.IsCurrent() is true and rlm.PkgPath() == PkgPath. A\n// foreign realm cannot satisfy both predicates, and a stale copy of this\n// realm's cur fails the first.\nfunc assertProxyRealm(_ int, rlm realm) {\n\tif !rlm.IsCurrent() || rlm.PkgPath() != PkgPath {\n\t\tpanic(\"transfer: access denied for non-proxy realm \" + rlm.PkgPath())\n\t}\n}\n\n// RegisterImpl records an implementation constructor. It must be called from\n// a realm under PkgPath + \"/impl/\", normally from that realm's init, so the\n// set of installable implementations is confined to this realm's own\n// namespace. Registration alone activates nothing, except for the very first\n// one while no implementation is installed (bootstrap). Every later switch\n// requires UpdateImpl. version, over and keepsControls are its manifest: the\n// version it reports, the versions of an active implementation it installs\n// over (\"\" at bootstrap; its own version always; nil for any) and whether it\n// keeps the route controls, which the preflight reads instead of building\n// it.\nfunc RegisterImpl(cur realm, ctor func(Store) Logic, version string, over []string, keepsControls bool) {\n\tcaller := cur.Previous().PkgPath()\n\tif !strings.HasPrefix(caller, implPathPrefix) {\n\t\tpanic(ufmt.Sprintf(\"RegisterImpl: caller %s is not under %s\", caller, implPathPrefix))\n\t}\n\tif ctor == nil {\n\t\tpanic(\"RegisterImpl: nil constructor\")\n\t}\n\tif version == \"\" {\n\t\tpanic(\"RegisterImpl: the manifest names no version\")\n\t}\n\t// A registered implementation is a realm of the bridge: one at an\n\t// address a route pays its fees to would turn that collector into one.\n\t// The collector is changed first, by a governed (delayed) decision.\n\tif isFeeCollector(chain.PackageAddress(caller)) {\n\t\tpanic(ufmt.Sprintf(\"RegisterImpl: %s is a route's fee collector: change the collector first\", caller))\n\t}\n\timplCtors.Set(caller, ctor)\n\timplManifests.Set(caller, newManifest(version, over, keepsControls))\n\timplAddrs.Set(string(chain.PackageAddress(caller)), caller)\n\tif impl == nil {\n\t\tinstall(0, cur, caller)\n\t}\n}\n\n// UpdateImpl activates a registered implementation, identified by its realm\n// path. Governance only (ADR pr6375 R14): an upgrade is a 7-day decision in\n// ibcgov. The implementation is built against the proxy-owned store and its\n// OnInstall hook runs before the switch, so a panic there leaves the previous\n// implementation active. Rollback is UpdateImpl with the previous path.\nfunc UpdateImpl(cur realm, path string) {\n\tassertGovernance(cur, \"update_impl\")\n\tinstall(0, cur, path)\n}\n\nfunc install(_ int, rlm realm, path string) {\n\tv := implCtors.Get(path)\n\tif v == nil {\n\t\tpanic(ufmt.Sprintf(\"no implementation registered at %s\", path))\n\t}\n\t// Only here, in an authorized installation (bootstrap or a governed\n\t// UpdateImpl), is candidate code given the store.\n\tctor := v.(func(Store) Logic)\n\tl := ctor(store)\n\tif l == nil {\n\t\tpanic(ufmt.Sprintf(\"implementation %s returned a nil Logic\", path))\n\t}\n\tm := implManifests.Get(path).(manifest)\n\tif mismatch := manifestMismatch(path, m, l); mismatch != \"\" {\n\t\tpanic(mismatch)\n\t}\n\t// l keeps the controls exactly when its manifest says so.\n\tif refusal := controlsContinuity(m.KeepsControls); refusal != \"\" {\n\t\tpanic(refusal)\n\t}\n\t// A panic in the hook unwinds the transaction: nothing below runs and\n\t// nothing the hook wrote persists.\n\tl.OnInstall(0, rlm, implPath, implVersion)\n\toldPath := implPath\n\timpl = l\n\timplPath = path\n\timplVersion = l.Version()\n\tchain.Emit(EventTypeImplUpdated,\n\t\tAttributeKeyOldPath, oldPath,\n\t\tAttributeKeyNewPath, path,\n\t\tAttributeKeyVersion, implVersion,\n\t)\n}\n\n// manifestMismatch is how l, built by the implementation registered at path,\n// differs from its manifest m (its version, and whether it keeps the route\n// controls), or \"\".\nfunc manifestMismatch(path string, m manifest, l Logic) string {\n\t_, keeps := l.(ControlsLogic)\n\tif l.Version() == m.Version \u0026\u0026 keeps == m.KeepsControls {\n\t\treturn \"\"\n\t}\n\treturn ufmt.Sprintf(\"implementation %s is not what its manifest declared (version %s, keeps controls %t): it builds version %s, keeps controls %t\",\n\t\tpath, m.Version, m.KeepsControls, l.Version(), keeps)\n}\n\n// InstallChecker is implemented by an implementation whose OnInstall\n// refuses some predecessors: CheckInstall is why it would, or nil, as a\n// pure function of the plain values the proxy passes (the active\n// implementation's path and version), so the preflight evaluates exactly\n// what OnInstall will.\ntype InstallChecker interface {\n\tCheckInstall(prevPath, prevVersion string) error\n}\n\n// CheckUpdateImpl is why UpdateImpl(path) would fail now, or \"\", with no\n// change: path is not registered, its manifest does not install over the\n// active version, it drops the route controls, or the implementation its\n// constructor builds disagrees with its manifest or refuses the active\n// implementation (InstallChecker). A governed switch that would fail is\n// refused (and its decision spent) instead of aborting and staying pending.\n// The candidate is built against an empty sandbox store (sandboxStore),\n// never the live one: the live store reaches candidate code only in an\n// authorized installation.\nfunc CheckUpdateImpl(path string) string {\n\tv := implManifests.Get(path)\n\tif v == nil {\n\t\treturn \"no implementation registered at \" + path\n\t}\n\tm := v.(manifest)\n\tif refusal := controlsContinuity(m.KeepsControls); refusal != \"\" {\n\t\treturn refusal\n\t}\n\tif !m.accepts(implVersion) {\n\t\treturn ufmt.Sprintf(\"transfer impl %s cannot install over version %s\", m.Version, implVersion)\n\t}\n\tl := implCtors.Get(path).(func(Store) Logic)(sandboxStore{})\n\tif l == nil {\n\t\treturn \"implementation \" + path + \" returned a nil Logic\"\n\t}\n\tif mismatch := manifestMismatch(path, m, l); mismatch != \"\" {\n\t\treturn mismatch\n\t}\n\tif ic, ok := l.(InstallChecker); ok {\n\t\tif err := ic.CheckInstall(implPath, implVersion); err != nil {\n\t\t\treturn err.Error()\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// controlsContinuity refuses an implementation without the route controls\n// once any route has them (ADR pr6375 §5.4): it would move value on Ethereum\n// routes without keeping their send records, meters and supply accounting,\n// so a reinstalled controls implementation would trust stale state (refund\n// a packet a second time). The controls are never rolled back.\nfunc controlsContinuity(keepsControls bool) string {\n\tif keepsControls {\n\t\treturn \"\"\n\t}\n\tif rd, ok := store.ExtView(\"controls\").(interface{ Routes() []string }); ok \u0026\u0026 len(rd.Routes()) \u003e 0 {\n\t\treturn \"transfer: an implementation without the route controls cannot replace them once a route has them\"\n\t}\n\treturn \"\"\n}\n\n// KeepsControls reports whether the implementation registered at path\n// declared it keeps the route controls (ControlsLogic), from its manifest:\n// nothing is built. The installation checks the declaration.\nfunc KeepsControls(path string) bool {\n\tv := implManifests.Get(path)\n\treturn v != nil \u0026\u0026 v.(manifest).KeepsControls\n}\n\n// ImplPath returns the realm path of the active implementation, \"\" if none.\nfunc ImplPath() string {\n\treturn implPath\n}\n\n// ImplVersion returns the Version() of the active implementation, \"\" if none.\nfunc ImplVersion() string {\n\treturn implVersion\n}\n\n// IsRegisteredImpl reports whether an implementation is registered at path:\n// a keyed lookup.\nfunc IsRegisteredImpl(path string) bool { return implCtors.Get(path) != nil }\n\n// IsImplAddress reports whether a is the realm address of a registered\n// implementation, superseded ones included: a keyed lookup, whatever the\n// number registered.\nfunc IsImplAddress(a address) bool { return implAddrs.Get(string(a)) != nil }\n\n// RegisteredImplPaths returns the paths of the registered implementations,\n// active or not, sorted lexically.\nfunc RegisteredImplPaths() []string {\n\tpaths := make([]string, 0, implCtors.Size())\n\timplCtors.IterateByOffset(0, implCtors.Size(), func(k string, _ any) bool {\n\t\tpaths = append(paths, k)\n\t\treturn false\n\t})\n\treturn paths\n}\n\nfunc mustControls() ControlsLogic {\n\tc, ok := mustImpl().(ControlsLogic)\n\tif !ok {\n\t\tpanic(\"transfer: the active implementation \" + implVersion + \" keeps no route controls\")\n\t}\n\treturn c\n}\n\nfunc mustImpl() Logic {\n\tif impl == nil {\n\t\tpanic(\"transfer: no implementation installed\")\n\t}\n\treturn impl\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"Tv4+9OZATbrCL34QrZoWX3jYtOfBPXESw/TSdF/dCY5js6TNrQeEkWMDsCB/VODJ6goAjJMarsHdcinM3fdCjg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"impl","path":"gno.land/r/aib/ibc/apps/transfer/impl/v0","files":[{"name":"app.gno","body":"package impl\n\nimport (\n\t\"bytes\"\n\t\"chain/runtime/unsafe\"\n\t\"errors\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/aib/ibc/ics20/abi/v0\"\n\t\"gno.land/p/aib/ibc/ics20/v0\"\n\t\"gno.land/p/aib/ibc/types/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/aib/ibc/apps/transfer\"\n)\n\n// validatePayload enforces that the source and destination portIDs are the\n// same and equal to the transfer portID, that the clientIDs are in the\n// {clientid}-{sequence} format transfer expects, and the ICS20 version and\n// encoding. This is necessary for IBC v2 since the portIDs (and thus the\n// application-application connection) are not prenegotiated by a channel\n// handshake. This restriction can be removed in a future where the trace hop\n// on receive commits to **both** the source and destination portIDs rather\n// than just the destination port.\nfunc validatePayload(sourceClient, destinationClient string, payload types.Payload) error {\n\tif payload.SourcePort != transfer.PortID || payload.DestinationPort != transfer.PortID {\n\t\treturn ufmt.Errorf(\"payload port ID is invalid: expected %s, got sourcePort: %s destPort: %s\", transfer.PortID, payload.SourcePort, payload.DestinationPort)\n\t}\n\tif !types.IsValidClientID(sourceClient) || !types.IsValidClientID(destinationClient) {\n\t\treturn ufmt.Errorf(\"client IDs must be in valid format: {string}-{number}\")\n\t}\n\tif payload.Version != transfer.V1 {\n\t\treturn ufmt.Errorf(\"invalid ICS20 version: expected %s, got %s\", transfer.V1, payload.Version)\n\t}\n\tif payload.Encoding != transfer.EncodingProtobuf \u0026\u0026 payload.Encoding != abi.Encoding {\n\t\treturn ufmt.Errorf(\"invalid encoding: expected %s or %s, got %s\", transfer.EncodingProtobuf, abi.Encoding, payload.Encoding)\n\t}\n\treturn nil\n}\n\nfunc (l *logic) OnSendPacket(_ int, rlm realm, sourceClient, destinationClient string, sequence uint64, payload types.Payload) error {\n\tif err := validatePayload(sourceClient, destinationClient, payload); err != nil {\n\t\treturn err\n\t}\n\n\tdata, token, err := unmarshalPayload(payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Mirror Transfer: the packet Sender is the EOA that signed the tx.\n\t// unsafe.OriginCaller is tx-level identity (no cur equivalent).\n\tsigner := unsafe.OriginCaller().String()\n\tif data.Sender != signer {\n\t\treturn ufmt.Errorf(\"invalid FungibleTokenPacketData: sender %s is different from signer %s\", data.Sender, signer)\n\t}\n\tif transfer.IsBlocked(address(data.Sender)) {\n\t\treturn ufmt.Errorf(\"sender %s is blocked\", data.Sender)\n\t}\n\n\t// Enforce that the base denom does not contain any slashes\n\t// Since IBC v2 packets will no longer have channel identifiers, we cannot\n\t// rely on the channel format to easily divide the trace from the base\n\t// denomination in ICS20 v1 packets.\n\t// The simplest way to prevent any potential issues from arising is to simply\n\t// disallow any slashes in the base denomination.\n\t// This prevents such denominations from being sent with IBCV v2 packets,\n\t// however we can still support them in IBC v1 packets.\n\t// If we enforce that IBC v2 packets are sent with ICS20 v2 and above\n\t// versions that separate the trace from the base denomination in the packet\n\t// data, then we can remove this restriction.\n\t// Non-IBC GRC20 tokens use aliases (slashes replaced with colons)\n\t// so they pass this check naturally.\n\tif err := ValidateBaseDenomNoSlash(token); err != nil {\n\t\treturn err\n\t}\n\n\tamount, err := token.AmountUint256()\n\tif err != nil {\n\t\treturn err\n\t}\n\tibcDenom := token.Denom.IBCDenom()\n\tif token.Denom.HasPrefix(payload.SourcePort, sourceClient) {\n\t\texpected, ok := l.store.ConsumePendingVoucherSend(0, rlm)\n\t\tif !ok {\n\t\t\treturn ufmt.Errorf(\"voucher packet must be initiated through transfer.Transfer\")\n\t\t}\n\t\tif err := ValidatePendingSend(expected, data.Sender, sourceClient, token.Denom.Path(), amount); err != nil {\n\t\t\treturn ufmt.Errorf(\"voucher packet %v\", err)\n\t\t}\n\n\t\t// Burn the voucher tokens from sender\n\t\tif err := l.store.BurnVoucher(0, rlm, ibcDenom, address(data.Sender), amount); err != nil {\n\t\t\tif l.store.VoucherToken(ibcDenom) == nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn ufmt.Errorf(\"burn voucher %s%s error: %v\", token.Amount, ibcDenom, err)\n\t\t}\n\t} else if transfer.IsGRC20Alias(token.Denom.Base) {\n\t\texpected, ok := l.store.ConsumePendingGRC20Send(0, rlm)\n\t\tif !ok {\n\t\t\treturn ufmt.Errorf(\"GRC20 packet must be initiated through transfer.Transfer\")\n\t\t}\n\t\tif err := ValidatePendingSend(expected, data.Sender, sourceClient, token.Denom.Path(), amount); err != nil {\n\t\t\treturn ufmt.Errorf(\"GRC20 packet %v\", err)\n\t\t}\n\n\t\t// Non-IBC GRC20 token: escrow via TransferFrom.\n\t\t// The caller must have approved the transfer app realm address.\n\t\tdenomKey := transfer.ResolveGRC20Alias(token.Denom.Base)\n\t\tif err := l.store.EscrowGRC20(0, rlm, sourceClient, denomKey, address(data.Sender), amount); err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\t// Native token: OnSendPacket cannot safely read banker.OriginSend()\n\t\t// here because PreviousRealm() is the core realm (not the EOA), so\n\t\t// the escrow envelope is verified upstream by Transfer() and handed\n\t\t// off through the store. A direct caller of core.SendPacket for a\n\t\t// native packet will find the slot empty and be rejected. A coin is\n\t\t// an int64: the packet's amount must fit one.\n\t\texpected, ok := l.store.PendingNativeEscrow()\n\t\tif !ok {\n\t\t\treturn ufmt.Errorf(\"native packet must be initiated through transfer.Transfer\")\n\t\t}\n\t\tcoin, err := token.ToCoin()\n\t\tif err != nil {\n\t\t\treturn ufmt.Errorf(\"token to coin error: %v\", err)\n\t\t}\n\t\tif coin.Denom != expected.Denom || coin.Amount != expected.Amount {\n\t\t\treturn ufmt.Errorf(\n\t\t\t\t\"escrowed coin %s is not equal to fungible packet data token %s\",\n\t\t\t\texpected, coin,\n\t\t\t)\n\t\t}\n\t\t// Escrow the coin on the proxy's balance, accounted under sourceClient.\n\t\tl.store.AddEscrow(0, rlm, sourceClient, coin.Denom, transfer.CoinAmount(coin))\n\t}\n\n\ttransfer.EmitTransfer(0, rlm, data.Sender, data.Receiver, token.Denom.Path(), token.Amount, data.Memo)\n\treturn nil\n}\n\n// ValidatePendingSend checks that a send matches the transfer.Transfer call\n// that announced it.\nfunc ValidatePendingSend(expected transfer.PendingSend, sender, sourceClient, denom string, amount uint256) error {\n\tif expected.Sender != sender ||\n\t\texpected.SourceClient != sourceClient ||\n\t\texpected.Denom != denom ||\n\t\texpected.Amount != amount {\n\t\treturn ufmt.Errorf(\"does not match transfer.Transfer\")\n\t}\n\treturn nil\n}\n\nfunc (l *logic) OnRecvPacket(_ int, rlm realm, sourceClient, destinationClient string, sequence uint64, payload types.Payload) types.RecvPacketResult {\n\tif err := validatePayload(sourceClient, destinationClient, payload); err != nil {\n\t\treturn types.RecvPacketResult{Status: types.PacketStatus_Failure}\n\t}\n\n\tvar (\n\t\tdata       ics20.FungibleTokenPacketData\n\t\ttoken      ics20.Token\n\t\tackErr     error\n\t\tack        = types.NewResultAppAcknowledgement([]byte{byte(1)})\n\t\trecvResult = types.RecvPacketResult{\n\t\t\tStatus:          types.PacketStatus_Success,\n\t\t\tAcknowledgement: ack.MarshalJSON(),\n\t\t}\n\t)\n\t// we are explicitly wrapping this emit event call in an anonymous function\n\t// so that the packet data is evaluated after it has been assigned a value.\n\tdefer func() {\n\t\tattrs := []string{\n\t\t\ttransfer.AttributeKeySender, data.Sender,\n\t\t\ttransfer.AttributeKeyReceiver, data.Receiver,\n\t\t\ttransfer.AttributeKeyDenom, token.Denom.Path(),\n\t\t\ttransfer.AttributeKeyAmount, token.Amount,\n\t\t\ttransfer.AttributeKeyMemo, data.Memo,\n\t\t\ttransfer.AttributeKeyAckSuccess, strconv.FormatBool(ack.Success()),\n\t\t}\n\t\tif ackErr != nil {\n\t\t\tattrs = append(attrs,\n\t\t\t\t[]string{transfer.AttributeKeyAckError, ackErr.Error()}...,\n\t\t\t)\n\t\t}\n\t\ttransfer.EmitPacket(0, rlm, attrs...)\n\t}()\n\n\tdata, token, ackErr = unmarshalPayload(payload)\n\tif ackErr != nil {\n\t\tack = types.NewErrorAppAcknowledgement(ackErr)\n\t\treturn types.RecvPacketResult{Status: types.PacketStatus_Failure}\n\t}\n\tif ackErr = ValidateBaseDenomNoSlash(token); ackErr != nil {\n\t\tack = types.NewErrorAppAcknowledgement(ackErr)\n\t\treturn types.RecvPacketResult{Status: types.PacketStatus_Failure}\n\t}\n\t// Decode the receiver, as ibc-go does with addressCodec.StringToBytes:\n\t// the counterparty may send any string, and bech32 accepts two spellings\n\t// of one account. Everything below uses the canonical address: the\n\t// blocklist and the GRC20 ledger key by the string, so an uppercase\n\t// spelling would miss a blocked account and strand minted vouchers.\n\treceiver, ok := transfer.CanonicalAddress(data.Receiver)\n\tif !ok {\n\t\tackErr = ufmt.Errorf(\"invalid receiver address %s\", data.Receiver)\n\t\tack = types.NewErrorAppAcknowledgement(ackErr)\n\t\treturn types.RecvPacketResult{Status: types.PacketStatus_Failure}\n\t}\n\t// A blocked receiver gets an error acknowledgement, so the counterparty\n\t// refunds its sender; nothing is minted or released here.\n\tif transfer.IsBlocked(receiver) {\n\t\tackErr = ufmt.Errorf(\"receiver %s is blocked\", data.Receiver)\n\t\tack = types.NewErrorAppAcknowledgement(ackErr)\n\t\treturn types.RecvPacketResult{Status: types.PacketStatus_Failure}\n\t}\n\t// This is the prefix that would have been prefixed to the denomination\n\t// on sender chain IF and only if the token originally came from the\n\t// receiving chain.\n\t//\n\t// NOTE: We use SourcePort and SourceClient here, because the counterparty\n\t// chain would have prefixed with DestPort and DestClient when originally\n\t// receiving this token.\n\tif token.Denom.HasPrefix(payload.SourcePort, sourceClient) {\n\t\t// sender chain is not the source, unescrow tokens\n\n\t\t// remove prefix added by sender chain\n\t\ttoken.Denom.Trace = token.Denom.Trace[1:]\n\t\tvar amount uint256\n\t\tamount, ackErr = token.AmountUint256()\n\t\tif ackErr != nil {\n\t\t\tack = types.NewErrorAppAcknowledgement(ackErr)\n\t\t\treturn types.RecvPacketResult{Status: types.PacketStatus_Failure}\n\t\t}\n\t\tdenom := token.Denom.IBCDenom()\n\n\t\t// The escrow being released was recorded under destinationClient when\n\t\t// this chain originally sent the token out (the packet now arrives on\n\t\t// destinationClient carrying the counterparty's prefix on the denom).\n\t\t// Debiting destinationClient, not sourceClient, is what prevents a\n\t\t// packet arriving on a different client from draining escrow that\n\t\t// belongs to another client.\n\t\tif transfer.IsGRC20Alias(denom) {\n\t\t\tackErr = l.store.UnescrowGRC20(0, rlm, destinationClient, string(receiver), denom, amount)\n\t\t} else {\n\t\t\tackErr = l.store.UnescrowNative(0, rlm, destinationClient, string(receiver), denom, amount)\n\t\t}\n\t\tif ackErr != nil {\n\t\t\tack = types.NewErrorAppAcknowledgement(ackErr)\n\t\t\treturn types.RecvPacketResult{Status: types.PacketStatus_Failure}\n\t\t}\n\n\t} else {\n\t\t// sender chain is the source, mint vouchers\n\n\t\t// since SendPacket did not prefix the denomination, we must add the\n\t\t// destination port and client to the trace\n\t\ttrace := []ics20.Hop{ics20.NewHop(payload.DestinationPort, destinationClient)}\n\t\ttoken.Denom.Trace = append(trace, token.Denom.Trace...)\n\n\t\tvoucherDenom := token.Denom.IBCDenom()\n\t\tif !l.store.HasDenom(voucherDenom) {\n\t\t\tl.store.SetDenom(0, rlm, token.Denom)\n\t\t}\n\n\t\ttransfer.EmitDenom(0, rlm, token.Denom.HashHex(), string(token.Denom.MarshalJSON()))\n\n\t\t// Mint voucher tokens to the receiver\n\t\tvar amount uint256\n\t\tamount, ackErr = token.AmountUint256()\n\t\tif ackErr != nil {\n\t\t\tack = types.NewErrorAppAcknowledgement(ackErr)\n\t\t\treturn types.RecvPacketResult{Status: types.PacketStatus_Failure}\n\t\t}\n\t\t// No configuration names the counterparty's decimals for a client\n\t\t// the route controls do not cover: the voucher counts base units.\n\t\tl.store.GetOrCreateVoucher(0, rlm, token.Denom.Base, voucherDenom, 0)\n\t\tif err := l.store.MintVoucher(0, rlm, voucherDenom, receiver, amount); err != nil {\n\t\t\tackErr = ufmt.Errorf(\"mint voucher error: %v\", err)\n\t\t\tack = types.NewErrorAppAcknowledgement(ackErr)\n\t\t\treturn types.RecvPacketResult{Status: types.PacketStatus_Failure}\n\t\t}\n\t}\n\n\treturn recvResult\n}\n\n// ValidateBaseDenomNoSlash rejects base denominations containing a slash.\n// IBC v2 packets do not carry channel identifiers, so denomination traces\n// cannot be unambiguously separated from a slashed base denomination.\nfunc ValidateBaseDenomNoSlash(token ics20.Token) error {\n\tif strings.Contains(token.Denom.Base, \"/\") {\n\t\treturn ufmt.Errorf(\"base denomination %s cannot contain slashes for IBC v2 packet\", token.Denom.Base)\n\t}\n\treturn nil\n}\n\n// OnAcknowledgementPacket responds to the success or failure of a packet\n// acknowledgment written on the receiving chain.\n//\n// If the acknowledgement was a success then nothing occurs. Otherwise,\n// if the acknowledgement failed, then the sender is refunded their tokens.\nfunc (l *logic) OnAcknowledgementPacket(_ int, rlm realm, sourceClient, destinationClient string, sequence uint64, acknowledgement []byte, payload types.Payload) error {\n\tvar ack types.AppAcknowledgement\n\t// Construct an error acknowledgement if the acknowledgement bytes are the\n\t// sentinel error acknowledgement so we can use the shared transfer logic\n\tif bytes.Equal(acknowledgement, types.UniversalErrorAcknowledgement()) {\n\t\t// the specific error does not matter\n\t\tack = types.NewErrorAppAcknowledgement(errors.New(\"receive packet failed\"))\n\t} else {\n\t\tif err := ack.UnmarshalJSON(acknowledgement); err != nil {\n\t\t\treturn ufmt.Errorf(\"cannot unmarshal ICS-20 transfer packet acknowledgement: %v\", err)\n\t\t}\n\t\tif !ack.Success() {\n\t\t\treturn ufmt.Errorf(\"cannot pass in a custom error acknowledgement with IBC v2\")\n\t\t}\n\t}\n\n\tdata, token, err := unmarshalPayload(payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif ack.Success() {\n\t\t// the acknowledgement succeeded on the receiving chain so nothing\n\t\t// needs to be executed and no error needs to be returned\n\t} else {\n\t\t// refund sender in case of ack error\n\t\tif err := l.refundPacketToken(0, rlm, payload.SourcePort, sourceClient, data.Sender, token); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\ttransfer.EmitPacket(0, rlm,\n\t\ttransfer.AttributeKeySender, data.Sender,\n\t\ttransfer.AttributeKeyReceiver, data.Receiver,\n\t\ttransfer.AttributeKeyDenom, token.Denom.Path(),\n\t\ttransfer.AttributeKeyAmount, token.Amount,\n\t\ttransfer.AttributeKeyMemo, data.Memo,\n\t\ttransfer.AttributeKeyAck, string(acknowledgement),\n\t)\n\tif ack.Success() {\n\t\ttransfer.EmitPacket(0, rlm,\n\t\t\ttransfer.AttributeKeyAckSuccess, string(ack.Response.Result),\n\t\t)\n\t} else {\n\t\ttransfer.EmitPacket(0, rlm,\n\t\t\ttransfer.AttributeKeyAckError, ack.Response.Error,\n\t\t)\n\t}\n\treturn nil\n}\n\n// OnTimeoutPacket processes a transfer packet timeout by refunding the tokens\n// to the sender.\nfunc (l *logic) OnTimeoutPacket(_ int, rlm realm, sourceClient, destinationClient string, sequence uint64, payload types.Payload) error {\n\tdata, token, err := unmarshalPayload(payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := l.refundPacketToken(0, rlm, payload.SourcePort, sourceClient, data.Sender, token); err != nil {\n\t\treturn err\n\t}\n\ttransfer.EmitTimeout(0, rlm, data.Sender, token.Denom.Path(), token.Amount, data.Memo)\n\treturn nil\n}\n\n// refundPacketToken returns to sender what OnSendPacket took: it re-mints\n// burnt vouchers, or releases the escrow recorded under sourceClient.\nfunc (l *logic) refundPacketToken(_ int, rlm realm, sourcePort, sourceClient, sender string, token ics20.Token) error {\n\tamount, err := token.AmountUint256()\n\tif err != nil {\n\t\treturn err\n\t}\n\tdenom := token.Denom.IBCDenom()\n\tif token.Denom.HasPrefix(sourcePort, sourceClient) {\n\t\t// Re-mint voucher tokens that were burnt during OnSendPacket\n\t\tif err := l.store.MintVoucher(0, rlm, denom, address(sender), amount); err != nil {\n\t\t\tif l.store.VoucherToken(denom) == nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn ufmt.Errorf(\"re-mint voucher error: %v\", err)\n\t\t}\n\t} else if transfer.IsGRC20Alias(denom) {\n\t\t// Non-IBC GRC20: return escrowed tokens to sender. These tokens were\n\t\t// escrowed under sourceClient on OnSendPacket, so release there.\n\t\tif err := l.store.UnescrowGRC20(0, rlm, sourceClient, sender, denom, amount); err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\t// Native: return escrowed coins to sender. These coins were escrowed\n\t\t// under sourceClient on OnSendPacket, so release there.\n\t\tif err := l.store.UnescrowNative(0, rlm, sourceClient, sender, denom, amount); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// unmarshalPayload decodes the packet data of payload in its encoding:\n// protobuf, or Solidity ABI from the Ethereum ICS20 application.\nfunc unmarshalPayload(payload types.Payload) (ics20.FungibleTokenPacketData, ics20.Token, error) {\n\tvar (\n\t\tdata ics20.FungibleTokenPacketData\n\t\terr  error\n\t)\n\tif payload.Encoding == abi.Encoding {\n\t\tdata, err = abi.Unmarshal(payload.Value)\n\t} else {\n\t\terr = data.ProtoUnmarshal(payload.Value)\n\t}\n\tif err != nil {\n\t\treturn data, ics20.Token{}, ufmt.Errorf(\"decoding FungibleTokenPacketData: %v\", err)\n\t}\n\tif err := data.ValidateBasic(); err != nil {\n\t\treturn data, ics20.Token{}, ufmt.Errorf(\"invalid FungibleTokenPacketData: %v\", err)\n\t}\n\tdenom := ics20.ExtractDenomFromPath(data.Denom)\n\ttoken := ics20.Token{Denom: denom, Amount: data.Amount}\n\treturn data, token, nil\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/aib/ibc/apps/transfer/impl/v0\"\ngno = \"0.9\"\n"},{"name":"impl.gno","body":"// Package impl is the first implementation of the ICS-20 transfer logic,\n// behind the gno.land/r/aib/ibc/apps/transfer proxy (see docs/adrs/0002). It\n// holds no state of its own: it reads and writes the proxy's store and moves\n// value through the proxy's Store services, emits through the proxy's Emit*\n// wrappers and is always invoked non-crossing by the proxy, so the current\n// realm is the proxy for the whole call and rlm is the proxy's live realm\n// value. It exports nothing that mutates.\npackage impl\n\nimport (\n\t\"gno.land/r/aib/ibc/apps/transfer\"\n)\n\nconst version = \"v0\"\n\ntype logic struct {\n\tstore transfer.Store\n}\n\nvar _ transfer.Logic = (*logic)(nil)\n\n// New builds the implementation against the proxy's store. impl/v0 is no\n// longer an installable implementation: on a fresh deployment the proxy\n// starts with impl/v1, which serves every non-Ethereum client through this\n// logic (it embeds it), so impl/v0 registers nothing.\nfunc New(s transfer.Store) transfer.Logic {\n\treturn \u0026logic{store: s}\n}\n\nfunc (l *logic) Version() string {\n\treturn version\n}\n\n// OnInstall has nothing to migrate: this is the first implementation and the\n// store it expects is the store the proxy was deployed with.\nfunc (l *logic) OnInstall(_ int, rlm realm, prevPath, prevVersion string) {}\n"},{"name":"render.gno","body":"package impl\n\nimport (\n\t\"math/overflow\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/aib/jsonmd/v0\"\n\n\t\"gno.land/p/aib/ibc/ics20/v0\"\n\t\"gno.land/p/aib/ibc/lightclient/v0\"\n\t\"gno.land/p/aib/jsonpage/v0\"\n\t\"gno.land/p/moul/txlink/v0\"\n\t\"gno.land/p/nt/grc20/v1\"\n\t\"gno.land/p/nt/mux/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/onbloc/json/v0\"\n\t\"gno.land/r/aib/ibc/apps/transfer\"\n\t\"gno.land/r/aib/ibc/core\"\n)\n\n// Render serves every path of the transfer realm: the proxy forwards its\n// Render here (see transfer.Render). Escrow is tracked per (client, denom);\n// the routes aggregate across all clients for display.\nfunc (l *logic) Render(path string) string {\n\trouter := mux.NewRouter()\n\tNewRenderer(l.store).Register(router)\n\treturn router.Render(path)\n}\n\n// Renderer serves the routes of the transfer realm from a Store. It is\n// exported, with its handlers and the helpers below, so a later implementation\n// can reuse what it does not change:\n//\n//\trd := implv0.NewRenderer(store)\n//\trouter.HandleFunc(\"\", myHome) // overrides first: mux matches in registration order\n//\trd.Register(router)\n//\n// A later implementation cannot build a Renderer literal (a realm cannot\n// construct another realm's types), hence NewRenderer.\ntype Renderer struct {\n\tstore transfer.Store\n}\n\n// NewRenderer returns a Renderer reading from s.\nfunc NewRenderer(s transfer.Store) *Renderer {\n\treturn \u0026Renderer{store: s}\n}\n\n// Register installs every route of this implementation on router. Routes\n// already registered on router take precedence.\nfunc (rd *Renderer) Register(router *mux.Router) {\n\trouter.HandleFunc(\"\", rd.Home)\n\trouter.HandleFunc(\"admin\", rd.Admin)\n\trouter.HandleFunc(\"denoms\", rd.Denoms)\n\trouter.HandleFunc(\"denoms/ibc/{hash}\", rd.Denom)\n\trouter.HandleFunc(\"total_escrow/{denom}\", rd.TotalEscrowForDenom)\n\trouter.HandleFunc(\"vouchers\", rd.Vouchers)\n\trouter.HandleFunc(\"voucher/ibc/{hash}\", rd.Voucher)\n\trouter.HandleFunc(\"voucher/ibc/{hash}/balance/{addr}\", rd.VoucherBalance)\n}\n\nfunc (rd *Renderer) Home(w *mux.ResponseWriter, r *mux.Request) {\n\tvar out strings.Builder\n\tout.WriteString(\"# IBC transfer\\n\\n\")\n\tout.WriteString(\"ICS-20 style transfer state and voucher token queries.\\n\\n\")\n\tif core.IsAppPaused(transfer.PortID) {\n\t\tout.WriteString(\"\u003e **Paused** by the IBC core: transfers and incoming packets are refused; refunds still go through.\\n\\n\")\n\t}\n\tout.WriteString(RenderImplDisclosure())\n\tout.WriteString(RenderTransferLinks())\n\tdenoms := rd.store.Denoms()\n\tout.WriteString(ufmt.Sprintf(\"## Vouchers (%d)\\n\\n\", denoms.Size()))\n\tif denoms.Size() \u003e 0 {\n\t\tout.WriteString(\"| Denom | Base | Path | Supply | GRC20 | Actions |\\n\")\n\t\tout.WriteString(\"|-------|------|------|--------|-------|---------|\\n\")\n\t\tfor _, d := range rd.DenomsByClientOrder() {\n\t\t\tibcDenom := d.IBCDenom()\n\t\t\tsupply := \"\"\n\t\t\tgrc20link := \"\"\n\t\t\tif token := rd.store.VoucherToken(ibcDenom); token != nil {\n\t\t\t\tsupply = \"[\" + strconv.FormatUint256(token.TotalSupply(), 10) + \"](/r/aib/ibc/apps/transfer:voucher/\" + ibcDenom + \")\"\n\t\t\t\tkey := GRC20RegKey(ibcDenom)\n\t\t\t\t// Shorten the hash suffix: \"gno.land/r/.../transfer.CAEF9C...\" → keep prefix + first 4 … last 4 of hash\n\t\t\t\tdotIdx := strings.LastIndex(key, \".\")\n\t\t\t\tshortKey := key\n\t\t\t\tif dotIdx != -1 {\n\t\t\t\t\thash := key[dotIdx+1:]\n\t\t\t\t\tif len(hash) \u003e 10 {\n\t\t\t\t\t\tshortKey = key[:dotIdx+1] + hash[:4] + \"…\" + hash[len(hash)-4:]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst grc20regPath = \"gno.land/r/nt/grc20reg/v1\"\n\t\t\t\tgrc20link = ufmt.Sprintf(\"[%s](%s:%s)\", shortKey, StripDomain(grc20regPath), key)\n\t\t\t}\n\t\t\t// Actions: IBC \"send back\" (via the voucher's source client) plus\n\t\t\t// local GRC20 send/approve. All pre-fill the voucher denom; the user\n\t\t\t// only fills the counterparty and amount (and timeout for the IBC\n\t\t\t// send) in their wallet.\n\t\t\tsourceClient := d.Trace[0].ClientId\n\t\t\tactions := ufmt.Sprintf(\n\t\t\t\t\"[send back](%s) via `%s` — [send](%s) — [approve](%s)\",\n\t\t\t\tNewTransferLink(sourceClient, \"\", ibcDenom).URL(),\n\t\t\t\tsourceClient,\n\t\t\t\tNewVoucherSendLink(ibcDenom).URL(),\n\t\t\t\tNewVoucherApproveLink(ibcDenom).URL(),\n\t\t\t)\n\t\t\tout.WriteString(ufmt.Sprintf(\n\t\t\t\t\"| [`%s`](/r/aib/ibc/apps/transfer:denoms/%s) | %s | %s | %s | %s | %s |\\n\",\n\t\t\t\tShortDenom(ibcDenom), ibcDenom, jsonmd.Cell(d.Base), jsonmd.Cell(d.Path()), supply, grc20link, actions,\n\t\t\t))\n\t\t}\n\t\tout.WriteString(\"\\n\")\n\t} else {\n\t\tout.WriteString(\"No vouchers yet.\\n\\n\")\n\t}\n\tescrowsPerDenom := rd.TotalEscrowsPerDenom()\n\tout.WriteString(ufmt.Sprintf(\"## Escrow (%d)\\n\\n\", len(escrowsPerDenom)))\n\tif len(escrowsPerDenom) \u003e 0 {\n\t\tout.WriteString(\"| Denom | Amount |\\n\")\n\t\tout.WriteString(\"|-------|--------|\\n\")\n\t\t// Iterate in the stable order the escrow accounting exposes (by\n\t\t// client), emitting each denom the first time it is seen, so the\n\t\t// table matches the underlying storage order rather than map\n\t\t// iteration order.\n\t\tseen := map[string]bool{}\n\t\trd.store.IterateEscrow(func(_, denom string, _ uint256) bool {\n\t\t\tif seen[denom] {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tseen[denom] = true\n\t\t\tout.WriteString(\"| \" + escrowDenomCell(denom) + \" | \" + strconv.FormatUint256(escrowsPerDenom[denom], 10) + \" |\\n\")\n\t\t\treturn false\n\t\t})\n\t\tout.WriteString(\"\\n\")\n\t} else {\n\t\tout.WriteString(\"No escrow yet.\\n\\n\")\n\t}\n\tout.WriteString(\"## JSON endpoints\\n\\n\")\n\tout.WriteString(\"- [`admin`](/r/aib/ibc/apps/transfer:admin): authority, pause state, blocked addresses\\n\")\n\tout.WriteString(\"- [`denoms`](/r/aib/ibc/apps/transfer:denoms): list known IBC denoms (`?page`, `?limit`)\\n\")\n\tout.WriteString(\"- `denoms/ibc/{hash}`: get metadata for an IBC denom\\n\")\n\tout.WriteString(\"- `total_escrow/{denom}`: get total escrow tracked for a base denom\\n\")\n\tout.WriteString(\"- [`vouchers`](/r/aib/ibc/apps/transfer:vouchers): list voucher tokens (`?page`, `?limit`)\\n\")\n\tout.WriteString(\"- `voucher/ibc/{hash}`: get voucher token metadata\\n\")\n\tout.WriteString(\"- `voucher/ibc/{hash}/balance/{addr}`: get a voucher balance for an address\\n\\n\")\n\tw.Write(out.String())\n}\n\n// escrowDenomCell is an escrow table's denom cell: a link to its total when\n// the denom is spelled in the bank's alphabet, else the denom escaped as\n// table text (a GRC20 alias carries a token's chosen symbol).\nfunc escrowDenomCell(denom string) string {\n\tfor i := 0; i \u003c len(denom); i++ {\n\t\tc := denom[i]\n\t\tif !(c \u003e= 'a' \u0026\u0026 c \u003c= 'z' || c \u003e= 'A' \u0026\u0026 c \u003c= 'Z' || c \u003e= '0' \u0026\u0026 c \u003c= '9' || c == '/' || c == ':' || c == '.' || c == '_' || c == '-') {\n\t\t\treturn jsonmd.Cell(denom)\n\t\t}\n\t}\n\treturn \"[`\" + denom + \"`](/r/aib/ibc/apps/transfer:total_escrow/\" + denom + \")\"\n}\n\nfunc (rd *Renderer) Admin(w *mux.ResponseWriter, r *mux.Request) {\n\tvar out strings.Builder\n\tout.WriteString(\"## Authority\\n\\n\")\n\tout.WriteString(\"- **Authority members**:\\n\")\n\tfor _, m := range transfer.AuthorityMembers() {\n\t\tout.WriteString(\"  - \" + string(m) + \"\\n\")\n\t}\n\tout.WriteString(ufmt.Sprintf(\"- **Paused**: %t (set by the IBC core)\\n\", core.IsAppPaused(transfer.PortID)))\n\tout.WriteString(ufmt.Sprintf(\"- **Implementation**: %s\\n\", RenderImplRef(transfer.ImplPath(), transfer.ImplVersion())))\n\tout.WriteString(\"- **Registered implementations**:\\n\")\n\tfor _, p := range transfer.RegisteredImplPaths() {\n\t\tout.WriteString(\"  - \" + p + \"\\n\")\n\t}\n\tblocked := transfer.BlockedAddresses()\n\tif len(blocked) == 0 {\n\t\tout.WriteString(\"- **Blocked addresses**: none\\n\")\n\t} else {\n\t\tout.WriteString(\"- **Blocked addresses**:\\n\")\n\t\tfor _, b := range blocked {\n\t\t\tout.WriteString(\"  - \" + string(b) + \"\\n\")\n\t\t}\n\t}\n\tw.Write(out.String())\n}\n\nfunc (rd *Renderer) Denoms(w *mux.ResponseWriter, r *mux.Request) {\n\tRenderNode(w, jsonpage.Render(rd.store.Denoms(), r, nil))\n}\n\nfunc (rd *Renderer) Denom(w *mux.ResponseWriter, r *mux.Request) {\n\tdenom := \"ibc/\" + r.GetVar(\"hash\")\n\td, found := rd.store.Denom(denom)\n\tif !found {\n\t\tRenderNode(w, NodeError(ufmt.Sprintf(\"denom %s not found\", denom)))\n\t\treturn\n\t}\n\tRenderNode(w, d.RenderJSON())\n}\n\nfunc (rd *Renderer) TotalEscrowForDenom(w *mux.ResponseWriter, r *mux.Request) {\n\tdenom := r.GetVar(\"denom\")\n\t// List the per-client amounts so the client/escrow binding is visible, not\n\t// just the aggregate. The total is kept at the top level for quick checks.\n\tclients := []*json.Node{}\n\tvar total uint256\n\trd.store.IterateEscrow(func(clientID, d string, amount uint256) bool {\n\t\tif d == denom {\n\t\t\ttotal = overflow.Addu256p(total, amount)\n\t\t\tclients = append(clients, json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\t\t\"client\": json.StringNode(\"\", clientID),\n\t\t\t\t\"amount\": amountNode(amount),\n\t\t\t}))\n\t\t}\n\t\treturn false\n\t})\n\tRenderNode(w, json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\"denom\":   json.StringNode(\"\", denom),\n\t\t\"amount\":  amountNode(total),\n\t\t\"clients\": json.ArrayNode(\"\", clients),\n\t}))\n}\n\n// amountNode is an amount in JSON: a decimal string, since a 256-bit value\n// does not survive a reader that parses numbers as doubles.\nfunc amountNode(v uint256) *json.Node { return json.StringNode(\"\", strconv.FormatUint256(v, 10)) }\n\n// TotalEscrowsPerDenom returns one entry per denom that has any escrow\n// across all clients, with the summed amount.\nfunc (rd *Renderer) TotalEscrowsPerDenom() map[string]uint256 {\n\tout := map[string]uint256{}\n\trd.store.IterateEscrow(func(_, denom string, amount uint256) bool {\n\t\tout[denom] = overflow.Addu256p(out[denom], amount)\n\t\treturn false\n\t})\n\treturn out\n}\n\nfunc (rd *Renderer) Vouchers(w *mux.ResponseWriter, r *mux.Request) {\n\tRenderNode(w, jsonpage.Render(rd.store.Vouchers(), r, func(key string, v any) *json.Node {\n\t\treturn VoucherJSON(key, v.(*grc20.Token))\n\t}))\n}\n\nfunc (rd *Renderer) Voucher(w *mux.ResponseWriter, r *mux.Request) {\n\tibcDenom := \"ibc/\" + r.GetVar(\"hash\")\n\ttoken := rd.store.VoucherToken(ibcDenom)\n\tif token == nil {\n\t\tRenderNode(w, NodeError(\"voucher token %s not found\", ibcDenom))\n\t\treturn\n\t}\n\tRenderNode(w, VoucherJSON(ibcDenom, token))\n}\n\n// VoucherJSON is the voucher/ibc/{hash} document, also the items of vouchers.\nfunc VoucherJSON(ibcDenom string, token *grc20.Token) *json.Node {\n\treturn json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\"denom\":        json.StringNode(\"\", ibcDenom),\n\t\t\"grc20reg_key\": json.StringNode(\"\", GRC20RegKey(ibcDenom)),\n\t\t\"name\":         json.StringNode(\"\", token.GetName()),\n\t\t\"symbol\":       json.StringNode(\"\", token.GetSymbol()),\n\t\t\"decimals\":     json.NumberNode(\"\", float64(token.GetDecimals())),\n\t\t\"total_supply\": amountNode(token.TotalSupply()),\n\t})\n}\n\nfunc (rd *Renderer) VoucherBalance(w *mux.ResponseWriter, r *mux.Request) {\n\tibcDenom := \"ibc/\" + r.GetVar(\"hash\")\n\taddr := r.GetVar(\"addr\")\n\ttoken := rd.store.VoucherToken(ibcDenom)\n\tif token == nil {\n\t\tRenderNode(w, NodeError(\"voucher token %s not found\", ibcDenom))\n\t\treturn\n\t}\n\tbalance := token.BalanceOf(address(addr))\n\tRenderNode(w, json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\"denom\":   json.StringNode(\"\", ibcDenom),\n\t\t\"address\": json.StringNode(\"\", addr),\n\t\t\"balance\": amountNode(balance),\n\t}))\n}\n\n// DenomsByClientOrder returns the stored voucher denoms ordered to match the\n// \"Pick the client\" listing (core.ClientIDs() order): each voucher is grouped\n// under its source client (Trace[0].ClientId), following the same client order\n// as the client list, so the vouchers table lines up with it. Within a client,\n// the underlying b+tree order is preserved. Denoms whose source client is not\n// (or no longer) registered are appended last, in b+tree order, so nothing is\n// dropped. Every stored denom is a minted voucher with a non-empty trace, so\n// Trace[0] is always safe to read.\nfunc (rd *Renderer) DenomsByClientOrder() []ics20.Denom {\n\tdenoms := rd.store.Denoms()\n\tbyClient := make(map[string][]ics20.Denom)\n\tall := make([]ics20.Denom, 0, denoms.Size())\n\tdenoms.IterateByOffset(0, denoms.Size(), func(_ string, v any) bool {\n\t\td := v.(ics20.Denom)\n\t\tall = append(all, d)\n\t\tclient := d.Trace[0].ClientId\n\t\tbyClient[client] = append(byClient[client], d)\n\t\treturn false\n\t})\n\n\tout := make([]ics20.Denom, 0, len(all))\n\temitted := make(map[string]bool) // source clients already emitted\n\tfor _, client := range core.ClientIDs() {\n\t\tif ds, ok := byClient[client]; ok {\n\t\t\tout = append(out, ds...)\n\t\t\temitted[client] = true\n\t\t}\n\t}\n\tfor _, d := range all {\n\t\tif !emitted[d.Trace[0].ClientId] {\n\t\t\tout = append(out, d)\n\t\t}\n\t}\n\treturn out\n}\n\n// RenderTransferLinks returns the \"## Transfer\" section: a set of txlinks\n// that wallets can turn into MsgCalls to Transfer. Each link leaves at least\n// one argument empty so the user fills it in their wallet.\nfunc RenderTransferLinks() string {\n\tvar out strings.Builder\n\tout.WriteString(\"## Transfer\\n\\n\")\n\n\tclientIDs := core.ClientIDs()\n\tif len(clientIDs) == 0 {\n\t\tout.WriteString(\"No IBC client registered yet — create one in [`/r/aib/ibc/core`](/r/aib/ibc/core) before transferring.\\n\\n\")\n\t\treturn out.String()\n\t}\n\n\tout.WriteString(\"Trigger an IBC transfer from your wallet. Pick the client of the destination chain:\\n\\n\")\n\tfor _, clientID := range clientIDs {\n\t\tstatus := core.ClientStatus(clientID)\n\t\theight := core.ClientLatestHeight(clientID)\n\t\tif core.IsClientRetiredFor(clientID, transfer.PortID) {\n\t\t\t// A retired client (ADR pr6375 §7.12) carries nothing more: its\n\t\t\t// status stays as it was, so it is checked first.\n\t\t\tout.WriteString(ufmt.Sprintf(\n\t\t\t\t\"- `%s` — retired, trusted height `%s` ([client details](/r/aib/ibc/core:clients/%s))\\n\",\n\t\t\t\tclientID, height, clientID,\n\t\t\t))\n\t\t} else if status == lightclient.Active {\n\t\t\tout.WriteString(ufmt.Sprintf(\n\t\t\t\t\"- [send via `%s`](%s) — %s, trusted height `%s` ([client details](/r/aib/ibc/core:clients/%s))\\n\",\n\t\t\t\tclientID, NewTransferLink(clientID, \"\", \"\").URL(), status, height, clientID,\n\t\t\t))\n\t\t} else {\n\t\t\t// Non-active client: no send link, since packets can't be sent.\n\t\t\tout.WriteString(ufmt.Sprintf(\n\t\t\t\t\"- `%s` — %s, trusted height `%s` ([client details](/r/aib/ibc/core:clients/%s))\\n\",\n\t\t\t\tclientID, status, height, clientID,\n\t\t\t))\n\t\t}\n\t}\n\n\tout.WriteString(\"\\n\")\n\treturn out.String()\n}\n\n// NewVoucherSendLink builds a txlink to VoucherSend with the ibc denom\n// pre-filled. The user fills `to` and `amount` in their wallet.\nfunc NewVoucherSendLink(ibcDenom string) *txlink.TxBuilder {\n\treturn txlink.NewLink(\"VoucherSend\").AddArgs(\n\t\t\"ibcDenom\", ibcDenom,\n\t\t\"to\", \"\",\n\t\t\"amount\", \"\",\n\t)\n}\n\n// NewVoucherApproveLink builds a txlink to VoucherApprove with the ibc denom\n// pre-filled. The user fills `spender` and `amount` in their wallet.\nfunc NewVoucherApproveLink(ibcDenom string) *txlink.TxBuilder {\n\treturn txlink.NewLink(\"VoucherApprove\").AddArgs(\n\t\t\"ibcDenom\", ibcDenom,\n\t\t\"spender\", \"\",\n\t\t\"amount\", \"\",\n\t)\n}\n\n// NewTransferLink builds a txlink to the Transfer function with the given\n// pre-filled arguments. Empty values are left wallet-settable.\n// timeoutTimestamp defaults to one hour from now (in unix seconds), matching\n// what the e2e tests use; the user can still override it in their wallet.\nfunc NewTransferLink(clientID, receiver, denom string) *txlink.TxBuilder {\n\ttimeoutTimestamp := strconv.FormatInt(time.Now().Add(time.Hour).Unix(), 10)\n\treturn txlink.NewLink(\"Transfer\").AddArgs(\n\t\t\"clientID\", clientID,\n\t\t\"receiver\", receiver,\n\t\t\"denom\", denom,\n\t\t\"amount\", \"\",\n\t\t\"timeoutTimestamp\", timeoutTimestamp,\n\t\t\"memo\", \"\",\n\t)\n}\n\n// StripDomain removes the domain from a gno pkg path for use in URLs.\n// \"gno.land/r/demo/foo\" → \"/r/demo/foo\"\nfunc StripDomain(path string) string {\n\ti := strings.Index(path, \"/\")\n\tif i != -1 {\n\t\treturn path[i:]\n\t}\n\treturn path\n}\n\n// ShortDenom shortens an IBC denom hash for display.\n// \"ibc/CAEF9CABC…9D0F\" (ibc/ + first 4 + … + last 4)\nfunc ShortDenom(ibcDenom string) string {\n\tconst prefix = \"ibc/\"\n\tif !strings.HasPrefix(ibcDenom, prefix) {\n\t\treturn ibcDenom\n\t}\n\thash := ibcDenom[len(prefix):]\n\tif len(hash) \u003e 10 {\n\t\treturn prefix + hash[:4] + \"…\" + hash[len(hash)-4:]\n\t}\n\treturn ibcDenom\n}\n\n// RenderNode writes n as JSON, inert in Markdown (jsonmd.Inert): its strings\n// may carry a counterparty's base denom or a path segment of the request.\nfunc RenderNode(w *mux.ResponseWriter, n *json.Node) {\n\tbz, err := json.Marshal(n)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tw.Write(jsonmd.Inert(string(bz)))\n}\n\n// GRC20RegKey returns the grc20reg key for a voucher ibc denom. grc20reg keys\n// tokens by rlmPath.symbol, and a voucher's symbol is allocated when it is\n// created (see transfer.VoucherSymbol), \"\" before.\n// e.g. the first voucher → \"gno.land/r/aib/ibc/apps/transfer.IBC-1\"\nfunc GRC20RegKey(ibcDenom string) string {\n\treturn transfer.PkgPath + \".\" + transfer.VoucherSymbol(ibcDenom)\n}\n\n// NodeError is the JSON error object every route answers on failure.\nfunc NodeError(msg string, args ...any) *json.Node {\n\treturn json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\"error\": json.StringNode(\"\", ufmt.Sprintf(msg, args...)),\n\t})\n}\n\n// RenderImplDisclosure is the upgradeable-realm notice of the home page.\nfunc RenderImplDisclosure() string {\n\treturn ufmt.Sprintf(\"**Upgradeable realm**: the ICS-20 logic runs in the implementation %s, replaceable by the authority with `UpdateImpl` (see [`admin`](/r/aib/ibc/apps/transfer:admin)).\\n\\n\",\n\t\tRenderImplRef(transfer.ImplPath(), transfer.ImplVersion()))\n}\n\nfunc RenderImplRef(path, version string) string {\n\tif path == \"\" {\n\t\treturn \"none installed\"\n\t}\n\treturn ufmt.Sprintf(\"[`%s`](%s) (version %s)\", path, StripDomain(path), version)\n}\n"},{"name":"transfer.gno","body":"package impl\n\nimport (\n\t\"chain\"\n\t\"chain/runtime/unsafe\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/aib/ibc/ics20/abi/v0\"\n\t\"gno.land/p/aib/ibc/ics20/v0\"\n\t\"gno.land/p/aib/ibc/lightclient/ethereum/v0\"\n\t\"gno.land/p/aib/ibc/types/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/aib/ibc/apps/transfer\"\n\t\"gno.land/r/aib/ibc/core\"\n\t\"gno.land/r/nt/grc20reg/v1\"\n)\n\n// Transfer classifies the denom, records in the proxy's store the send it\n// authorizes for OnSendPacket, builds the packet and sends it through the IBC\n// core. The proxy already checked that the caller is a direct user call and\n// read the send envelope (sent), so sent describes coins that landed at the\n// proxy and unsafe.OriginCaller is the packet sender. The proxy clears the\n// recorded send on every exit path.\nfunc (l *logic) Transfer(_ int, rlm realm, sent []chain.Coin, clientID, receiver, denom string, amount uint256, timeoutTimestamp uint64, memo string) (\n\tpacket types.MsgSendPacket, sequence uint64,\n) {\n\tsender := unsafe.OriginCaller().String()\n\tswitch {\n\tcase strings.HasPrefix(denom, \"ibc/\"):\n\t\t// IBC voucher: the voucher GRC20 token is burnt in OnSendPacket.\n\t\t// Native coins sent with a voucher transfer would be stuck at the\n\t\t// proxy.\n\t\tif len(sent) \u003e 0 {\n\t\t\tpanic(\"voucher transfer must not include native coins\")\n\t\t}\n\t\t// Validate it exists and resolve the denom path.\n\t\tif l.store.VoucherToken(denom) == nil {\n\t\t\tpanic(ufmt.Errorf(\"voucher token not found for denom %s\", denom))\n\t\t}\n\t\td, found := l.store.Denom(denom)\n\t\tif !found {\n\t\t\tpanic(ufmt.Sprintf(\"denom %s not found in store\", denom))\n\t\t}\n\t\tl.store.SetPendingVoucherSend(0, rlm, sender, clientID, d.Path(), amount)\n\t\tdenom = d.Path()\n\tcase grc20reg.Get(denom) != nil:\n\t\t// Non-IBC GRC20 token: escrow via TransferFrom happens in\n\t\t// OnSendPacket. Native coins sent with a GRC20 transfer would be\n\t\t// stuck at the proxy.\n\t\tif len(sent) \u003e 0 {\n\t\t\tpanic(\"GRC20 transfer must not include native coins\")\n\t\t}\n\t\t// Replace the denom with a slash-free alias for the IBC packet.\n\t\t// This avoids ibc-go rejecting base denoms containing slashes.\n\t\talias := transfer.GRC20Alias(denom)\n\t\tl.store.SetPendingGRC20Send(0, rlm, sender, clientID, alias, amount)\n\t\tdenom = alias\n\tcase strings.Contains(denom, \"/\"):\n\t\t// This case exists only for a better error message: a denom with\n\t\t// slashes that isn't in grc20reg is likely a mistyped GRC20 path.\n\t\t// Without it, the default (native coin) branch would give a\n\t\t// confusing \"requires one non-zero coin to be sent\" error.\n\t\tpanic(ufmt.Sprintf(\"GRC20 token %s not found in grc20reg\", denom))\n\tdefault:\n\t\t// Native coin: the coins landed at the proxy (direct user call,\n\t\t// checked by the proxy). A coin is an int64, so an amount above the\n\t\t// chain's coin maximum can match no coin sent.\n\t\tif len(sent) != 1 || sent[0].Amount == 0 {\n\t\t\tpanic(\"native transfer requires one non-zero coin to be sent\")\n\t\t}\n\t\tif sent[0].Denom != denom || transfer.CoinAmount(sent[0]) != amount {\n\t\t\tpanic(ufmt.Sprintf(\n\t\t\t\t\"sent coin %s does not match transfer args (denom=%s, amount=%s)\",\n\t\t\t\tsent[0], denom, strconv.FormatUint256(amount, 10),\n\t\t\t))\n\t\t}\n\t\t// Hand the verified escrow off to OnSendPacket, which can no longer\n\t\t// read OriginSend safely (its previous realm is the core realm).\n\t\tl.store.SetPendingNativeEscrow(0, rlm, sent[0])\n\t}\n\n\t// The packet Sender is the EOA that signed the tx; OnSendPacket reads the\n\t// same value back to verify the packet Sender matches the signing EOA.\n\tdata := ics20.NewFungibleTokenPacketData(\n\t\tdenom,\n\t\tstrconv.FormatUint256(amount, 10),\n\t\tsender,\n\t\treceiver,\n\t\tmemo,\n\t)\n\t// The Solidity ICS20 application only reads ABI-encoded packet data, so\n\t// packets to Ethereum use it.\n\tencoding, value := transfer.EncodingProtobuf, data.ProtoMarshal()\n\tif strings.HasPrefix(clientID, ethereum.ClientType+\"-\") {\n\t\tbz, err := abi.Marshal(data)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tencoding, value = abi.Encoding, bz\n\t}\n\tpacket = types.MsgSendPacket{\n\t\tSourceClient: clientID,\n\t\tPayloads: []types.Payload{{\n\t\t\tSourcePort:      transfer.PortID,\n\t\t\tDestinationPort: transfer.PortID,\n\t\t\tEncoding:        encoding,\n\t\t\tValue:           value,\n\t\t\tVersion:         transfer.V1,\n\t\t}},\n\t\tTimeoutTimestamp: timeoutTimestamp,\n\t}\n\tsequence = core.SendPacket(cross(rlm), packet)\n\treturn\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"N5K2SnIHSwq6FxzKbuTg5zX/uA9PGbA5ldmqKHkdWyYgH09eH+ODLsb67FJcNCRvmvLy18padzJvNki+ywr7ng=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"ibcgov","path":"gno.land/r/aib/ibc/ibcgov","files":[{"name":"admin.gno","body":"package ibcgov\n\nimport (\n\t\"chain\"\n\t\"encoding/hex\"\n\t\"strings\"\n\n\t\"gno.land/p/aib/ibc/lightclient/ethereum/v0\"\n\t\"gno.land/r/aib/ibc/apps/transfer\"\n\t\"gno.land/r/aib/ibc/core\"\n)\n\n// The builders of the IBC core's and the transfer app's administration (ADR\n// pr6375 R14). Both realms gate every loosening power on a call from this\n// realm, so each power waits for its decision's delay here: upgrades,\n// relayers, recovery, approvals, client migration and membership 7 days;\n// tightening (pause, block, reject, remove) at once; unpauses at once, spent\n// under a guardian hold.\n\n// adminParams is a decision that makes one administrative call.\ntype adminParams struct {\n\top   string\n\targs []string\n}\n\n// check is why the administrative call cannot apply on the live state, or \"\":\n// every failure the call itself would raise and that a later state could\n// clear, so the decision is spent deterministically instead of aborting and\n// staying pending, to run later under its old approval when the state\n// changes back (Codex review #15).\nfunc (p *adminParams) check() string {\n\ta := p.args\n\tswitch p.op {\n\tcase \"core update impl\":\n\t\treturn core.CheckUpdateImpl(a[0])\n\tcase \"core add relayer\":\n\t\tif isRelayer(address(a[0])) {\n\t\t\treturn a[0] + \" is a listed relayer already\"\n\t\t}\n\tcase \"core remove relayer\":\n\t\tif !isRelayer(address(a[0])) {\n\t\t\treturn a[0] + \" is not a listed relayer\"\n\t\t}\n\tcase \"core recover client\":\n\t\t// The core's checks and the light client's, with no change.\n\t\treturn core.CheckRecoverClient(a[0], a[1])\n\tcase \"core approve app\":\n\t\tif !core.IsAppPending(a[0], a[1]) {\n\t\t\treturn \"no pending registration of \" + a[1] + \" for port \" + a[0]\n\t\t}\n\t\tif core.AppPkgPath(a[0]) != \"\" {\n\t\t\treturn \"port \" + a[0] + \" is registered already\"\n\t\t}\n\tcase \"core reject app\":\n\t\tif !core.IsAppPending(a[0], a[1]) {\n\t\t\treturn \"no pending registration of \" + a[1] + \" for port \" + a[0]\n\t\t}\n\tcase \"core pause\":\n\t\tif core.Paused() {\n\t\t\treturn \"the core is paused already\"\n\t\t}\n\tcase \"core pause app\":\n\t\tif core.AppPkgPath(a[0]) == \"\" {\n\t\t\treturn \"no app on port \" + a[0]\n\t\t}\n\t\tif core.IsAppPaused(a[0]) {\n\t\t\treturn \"app \" + a[0] + \" is paused already\"\n\t\t}\n\tcase \"core add member\":\n\t\tif core.IsAuthorityMember(address(a[0])) {\n\t\t\treturn a[0] + \" is a member already\"\n\t\t}\n\tcase \"core remove member\":\n\t\tif address(a[0]) == chain.PackageAddress(PkgPath) {\n\t\t\treturn \"this realm stays a member of the core's authority\"\n\t\t}\n\t\tif !core.IsAuthorityMember(address(a[0])) {\n\t\t\treturn a[0] + \" is not a member\"\n\t\t}\n\tcase \"core migrate client\":\n\t\tbz, err := hex.DecodeString(a[2])\n\t\tif err != nil {\n\t\t\treturn \"params are not hex\"\n\t\t}\n\t\tif core.ClientType(a[0]) != ethereum.ClientType {\n\t\t\treturn a[0] + \" is no Ethereum client\"\n\t\t}\n\t\treturn core.CheckMigrateClient(a[0], a[1], bz)\n\tcase \"transfer update impl\":\n\t\treturn transfer.CheckUpdateImpl(a[0])\n\tcase \"transfer block\":\n\t\tif transfer.IsBlocked(address(a[0])) {\n\t\t\treturn a[0] + \" is blocked already\"\n\t\t}\n\tcase \"transfer unblock\":\n\t\tif !transfer.IsBlocked(address(a[0])) {\n\t\t\treturn a[0] + \" is not blocked\"\n\t\t}\n\tcase \"transfer add member\":\n\t\tif transfer.IsAuthorityMember(address(a[0])) {\n\t\t\treturn a[0] + \" is a member already\"\n\t\t}\n\tcase \"transfer remove member\":\n\t\tif address(a[0]) == chain.PackageAddress(PkgPath) {\n\t\t\treturn \"this realm stays a member of the transfer app's authority\"\n\t\t}\n\t\tif !transfer.IsAuthorityMember(address(a[0])) {\n\t\t\treturn a[0] + \" is not a member\"\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc contains(list []string, s string) bool {\n\tfor _, x := range list {\n\t\tif x == s {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (p *adminParams) apply(cur realm, t int64) (string, string) {\n\tif refusal := p.check(); refusal != \"\" {\n\t\treturn \"\", refusal\n\t}\n\ta := p.args\n\tswitch p.op {\n\tcase \"core update impl\":\n\t\tcore.UpdateImpl(cross(cur), a[0])\n\tcase \"core add relayer\":\n\t\tcore.AddRelayer(cross(cur), address(a[0]))\n\tcase \"core remove relayer\":\n\t\tcore.RemoveRelayer(cross(cur), address(a[0]))\n\tcase \"core recover client\":\n\t\tcore.RecoverClient(cross(cur), a[0], a[1])\n\tcase \"core approve app\":\n\t\tcore.ApproveApp(cross(cur), a[0], a[1])\n\tcase \"core reject app\":\n\t\tcore.RejectApp(cross(cur), a[0], a[1])\n\tcase \"core pause\":\n\t\tcore.Pause(cross(cur))\n\tcase \"core pause app\":\n\t\tcore.PauseApp(cross(cur), a[0])\n\tcase \"core add member\":\n\t\tcore.AddAuthorityMember(cross(cur), address(a[0]))\n\tcase \"core remove member\":\n\t\tcore.RemoveAuthorityMember(cross(cur), address(a[0]))\n\tcase \"core migrate client\":\n\t\tbz, _ := hex.DecodeString(a[2])\n\t\tif refusal := core.MigrateClient(cross(cur), a[0], a[1], bz); refusal != \"\" {\n\t\t\treturn \"\", refusal\n\t\t}\n\t\t// A migrated verifier is a new trust: the meters of the client's\n\t\t// route start empty, as after a replacement (ADR pr6375 §7.1).\n\t\tif route := routeKeyOf(a[0]); routes.Has(route) {\n\t\t\tbumpPayoutsEpoch(route, t)\n\t\t\treturn p.op + \": route \" + route + \"'s meters emptied\", \"\"\n\t\t}\n\tcase \"transfer update impl\":\n\t\ttransfer.UpdateImpl(cross(cur), a[0])\n\tcase \"transfer block\":\n\t\ttransfer.BlockAddress(cross(cur), address(a[0]))\n\tcase \"transfer unblock\":\n\t\ttransfer.UnblockAddress(cross(cur), address(a[0]))\n\tcase \"transfer add member\":\n\t\ttransfer.AddAuthorityMember(cross(cur), address(a[0]))\n\tcase \"transfer remove member\":\n\t\ttransfer.RemoveAuthorityMember(cross(cur), address(a[0]))\n\tdefault:\n\t\treturn \"\", \"unknown operation \" + p.op\n\t}\n\treturn p.op, \"\"\n}\n\n// unpauseAdminParams is an unpause of the core or of an app: at once, spent\n// under a guardian hold.\ntype unpauseAdminParams struct{ port string }\n\nfunc (*unpauseAdminParams) unpause() {}\n\nfunc (p *unpauseAdminParams) apply(cur realm, t int64) (string, string) {\n\tif p.port == \"\" {\n\t\tif !core.Paused() {\n\t\t\treturn \"\", \"the core is not paused\"\n\t\t}\n\t\tcore.Unpause(cross(cur))\n\t\treturn \"core unpaused\", \"\"\n\t}\n\tif !core.IsAppPaused(p.port) {\n\t\treturn \"\", \"app \" + p.port + \" is not paused\"\n\t}\n\tcore.UnpauseApp(cross(cur), p.port)\n\treturn \"app \" + p.port + \" unpaused\", \"\"\n}\n\nfunc adminRequest(cur realm, op, text string, queued bool, args ...string) Request {\n\tp := \u0026adminParams{op: op, args: args}\n\treturn propose(cur, op, text, func(cur realm, made, t int64) {\n\t\tdecide(cur, op, text, p, made, t, queued)\n\t})\n}\n\n// mustAddr is s as a gno address in its canonical (lowercase) spelling: bech32\n// also accepts the uppercase one, which the banker credits to the same account\n// but which every string comparison and key (seats, the GRC20 ledger, the\n// blocklist, relayers) tells apart (transfer.CanonicalAddress).\nfunc mustAddr(s string) address {\n\ta, ok := transfer.CanonicalAddress(strings.TrimSpace(s))\n\tif !ok {\n\t\tpanic(\"ibcgov: not an address: \" + s)\n\t}\n\treturn a\n}\n\nfunc mustImplPath(prefix, path string) {\n\tif !strings.HasPrefix(path, prefix+\"/impl/v\") {\n\t\tpanic(\"ibcgov: an implementation lives under \" + prefix + \"/impl/v\u003cN\u003e, not \" + path)\n\t}\n\tif (prefix == CorePath \u0026\u0026 core.IsRegisteredImpl(path)) || (prefix == TransferPath \u0026\u0026 transfer.IsRegisteredImpl(path)) {\n\t\treturn\n\t}\n\tpanic(\"ibcgov: no implementation is registered at \" + path)\n}\n\n// NewCoreUpdateImplRequest activates a registered core implementation after\n// 7 days (ADR pr6375 §6.12).\nfunc NewCoreUpdateImplRequest(cur realm, path string) Request {\n\tmustImplPath(CorePath, path)\n\treturn adminRequest(cur, \"core update impl\", \"core: activate the implementation \"+path, true, path)\n}\n\n// NewCoreAddRelayerRequest adds a relayer to the core's whitelist after 7\n// days. It concerns non-Ethereum clients only: relaying for an Ethereum client\n// is permissionless.\nfunc NewCoreAddRelayerRequest(cur realm, addr string) Request {\n\ta := mustAddr(addr)\n\treturn adminRequest(cur, \"core add relayer\", \"core: add relayer \"+string(a), true, string(a))\n}\n\n// NewCoreRemoveRelayerRequest removes a relayer from the core's whitelist: at\n// once while others remain listed, after 7 days when it is the last one (an\n// empty whitelist lets anyone relay). The whitelist is read when the decision\n// is recorded, not when the request is built.\nfunc NewCoreRemoveRelayerRequest(cur realm, addr string) Request {\n\ta := mustAddr(addr)\n\tif !isRelayer(a) {\n\t\tpanic(\"ibcgov: \" + addr + \" is not a listed relayer\")\n\t}\n\top, text := \"core remove relayer\", \"core: remove relayer \"+string(a)\n\tp := \u0026adminParams{op: op, args: []string{string(a)}}\n\treturn propose(cur, op, text, func(cur realm, made, t int64) {\n\t\tdecide(cur, op, text, p, made, t, !isRelayer(a) || len(core.Relayers()) \u003c= 1)\n\t})\n}\n\nfunc isRelayer(a address) bool {\n\tfor _, r := range core.Relayers() {\n\t\tif r == a {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// NewRecoverClientRequest recovers a frozen or expired core client from a\n// substitute after 7 days (see the core's recover-client.md).\nfunc NewRecoverClientRequest(cur realm, subjectClientID, substituteClientID string) Request {\n\tif core.ClientType(subjectClientID) == \"\" || core.ClientType(substituteClientID) == \"\" {\n\t\tpanic(\"ibcgov: unknown client\")\n\t}\n\treturn adminRequest(cur, \"core recover client\", \"core: recover client \"+subjectClientID+\" from \"+substituteClientID, true, subjectClientID, substituteClientID)\n}\n\n// NewCoreApproveAppRequest activates a pending app registration after 7 days.\nfunc NewCoreApproveAppRequest(cur realm, portID, pkgPath string) Request {\n\tif !core.IsAppPending(portID, pkgPath) {\n\t\tpanic(\"ibcgov: no pending registration of \" + pkgPath + \" for port \" + portID)\n\t}\n\treturn adminRequest(cur, \"core approve app\", \"core: approve \"+pkgPath+\" on port \"+portID, true, portID, pkgPath)\n}\n\n// NewCoreRejectAppRequest drops a pending app registration at once.\nfunc NewCoreRejectAppRequest(cur realm, portID, pkgPath string) Request {\n\tif !core.IsAppPending(portID, pkgPath) {\n\t\tpanic(\"ibcgov: no pending registration of \" + pkgPath + \" for port \" + portID)\n\t}\n\treturn adminRequest(cur, \"core reject app\", \"core: reject \"+pkgPath+\" on port \"+portID, false, portID, pkgPath)\n}\n\n// NewCorePauseRequest suspends the core's packet path at once (a core bug:\n// it blocks refunds too, ADR pr6375 §7.3).\nfunc NewCorePauseRequest(cur realm) Request {\n\treturn adminRequest(cur, \"core pause\", \"core: pause the packet path\", false)\n}\n\n// NewCoreUnpauseRequest resumes the core's packet path at once; spent under\n// a guardian hold.\nfunc NewCoreUnpauseRequest(cur realm) Request {\n\ttext := \"core: unpause the packet path\"\n\tp := \u0026unpauseAdminParams{}\n\treturn propose(cur, \"core unpause\", text, func(cur realm, made, t int64) {\n\t\tdecide(cur, \"core unpause\", text, p, made, t, false)\n\t})\n}\n\n// NewCorePauseAppRequest suspends the app on portID at once.\nfunc NewCorePauseAppRequest(cur realm, portID string) Request {\n\tif core.AppPkgPath(portID) == \"\" {\n\t\tpanic(\"ibcgov: no app on port \" + portID)\n\t}\n\treturn adminRequest(cur, \"core pause app\", \"core: pause the app on port \"+portID, false, portID)\n}\n\n// NewCoreUnpauseAppRequest resumes the app on portID at once; spent under a\n// guardian hold.\nfunc NewCoreUnpauseAppRequest(cur realm, portID string) Request {\n\tif core.AppPkgPath(portID) == \"\" {\n\t\tpanic(\"ibcgov: no app on port \" + portID)\n\t}\n\ttext := \"core: unpause the app on port \" + portID\n\tp := \u0026unpauseAdminParams{port: portID}\n\treturn propose(cur, \"core unpause app\", text, func(cur realm, made, t int64) {\n\t\tdecide(cur, \"core unpause app\", text, p, made, t, false)\n\t})\n}\n\n// NewCoreAddMemberRequest adds a member to the core's authority (the\n// tightening operations) after 7 days.\nfunc NewCoreAddMemberRequest(cur realm, addr string) Request {\n\ta := mustAddr(addr)\n\treturn adminRequest(cur, \"core add member\", \"core: add authority member \"+string(a), true, string(a))\n}\n\n// NewCoreRemoveMemberRequest removes a member of the core's authority at once.\nfunc NewCoreRemoveMemberRequest(cur realm, addr string) Request {\n\ta := mustAddr(addr)\n\tif a == chain.PackageAddress(PkgPath) {\n\t\tpanic(\"ibcgov: this realm stays a member of the core's authority\")\n\t}\n\tif !core.IsAuthorityMember(a) {\n\t\tpanic(\"ibcgov: \" + addr + \" is not a member\")\n\t}\n\treturn adminRequest(cur, \"core remove member\", \"core: remove authority member \"+string(a), false, string(a))\n}\n\n// NewMigrateClientRequest migrates the verifier of a live core client after\n// 7 days (ADR pr6375 R16, §6.12): for migration \"ethereum-fork-schedule\",\n// paramsHex is the hex of ethereum.EncodeForkParameters of the new schedule,\n// which the decision's text spells out fork by fork, and it must land before\n// the fork epoch; for \"ethereum-verifier\" (see NewReplaceVerifierRequest),\n// paramsHex is the hex of the target verifier version.\nfunc NewMigrateClientRequest(cur realm, clientID, migration, paramsHex string) Request {\n\tif core.ClientType(clientID) == \"\" {\n\t\tpanic(\"ibcgov: unknown client \" + clientID)\n\t}\n\tbz, err := hex.DecodeString(paramsHex)\n\tif err != nil {\n\t\tpanic(\"ibcgov: params are hex\")\n\t}\n\tvar text string\n\tswitch migration {\n\tcase \"ethereum-fork-schedule\":\n\t\tif core.ClientType(clientID) != ethereum.ClientType {\n\t\t\tpanic(\"ibcgov: \" + clientID + \" is not an Ethereum client\")\n\t\t}\n\t\tfp, err := ethereum.DecodeForkParameters(bz)\n\t\tif err != nil {\n\t\t\tpanic(\"ibcgov: \" + err.Error())\n\t\t}\n\t\ttext = \"core: migrate client \" + clientID + \" to the fork schedule \" + forkScheduleText(fp)\n\tcase \"ethereum-verifier\":\n\t\tif core.ClientType(clientID) != ethereum.ClientType {\n\t\t\tpanic(\"ibcgov: \" + clientID + \" is not an Ethereum client\")\n\t\t}\n\t\tif len(bz) == 0 {\n\t\t\tpanic(\"ibcgov: name the verifier version to convert to\")\n\t\t}\n\t\ttext = \"core: convert the verifier of client \" + clientID + \" (\" + core.VerifierVersion(clientID) + \") to \" + string(bz) + \" with the active core implementation's conversion\"\n\tdefault:\n\t\tpanic(\"ibcgov: unknown migration \" + migration)\n\t}\n\treturn adminRequest(cur, \"core migrate client\", text, true, clientID, migration, hex.EncodeToString(bz))\n}\n\n// NewReplaceVerifierRequest converts the verifier of the live Ethereum\n// client clientID to the verifier version toVersion after 7 days (ADR\n// pr6375 R16, §6.12), with the conversion the active core implementation\n// compiles in (installed beforehand by a governed implementation upgrade).\n// The client keeps its id, every trusted consensus state and its packet\n// state; its trust generation advances and its route's payout meters start\n// empty. An Expired client is converted too and stays Expired (an ordinary\n// governed recovery follows). When it executes, a conversion the active\n// implementation does not declare, a client neither Active nor Expired\n// (Frozen), or a converted verifier failing its checks spends the decision.\nfunc NewReplaceVerifierRequest(cur realm, clientID, toVersion string) Request {\n\treturn NewMigrateClientRequest(cur, clientID, \"ethereum-verifier\", hex.EncodeToString([]byte(toVersion)))\n}\n\nfunc forkScheduleText(p ethereum.ForkParameters) string {\n\tf := func(name string, x ethereum.Fork) string {\n\t\treturn name + \"=\" + hex.EncodeToString(x.Version[:]) + \"@\" + u64s(x.Epoch)\n\t}\n\treturn \"genesis=\" + hex.EncodeToString(p.GenesisForkVersion[:]) + \"@slot\" + u64s(p.GenesisSlot) + \",\" +\n\t\tstrings.Join([]string{\n\t\t\tf(\"altair\", p.Altair), f(\"bellatrix\", p.Bellatrix), f(\"capella\", p.Capella), f(\"deneb\", p.Deneb),\n\t\t\tf(\"electra\", p.Electra), f(\"fulu\", p.Fulu), f(\"gloas\", p.Gloas),\n\t\t}, \",\")\n}\n\n// NewTransferUpdateImplRequest activates a registered transfer implementation\n// after 7 days.\nfunc NewTransferUpdateImplRequest(cur realm, path string) Request {\n\tmustImplPath(TransferPath, path)\n\treturn adminRequest(cur, \"transfer update impl\", \"transfer: activate the implementation \"+path, true, path)\n}\n\n// NewTransferBlockRequest blocks an address in the transfer app at once.\nfunc NewTransferBlockRequest(cur realm, addr string) Request {\n\ta := mustAddr(addr)\n\treturn adminRequest(cur, \"transfer block\", \"transfer: block \"+string(a), false, string(a))\n}\n\n// NewTransferUnblockRequest unblocks an address in the transfer app after 7\n// days.\nfunc NewTransferUnblockRequest(cur realm, addr string) Request {\n\ta := mustAddr(addr)\n\tif !transfer.IsBlocked(a) {\n\t\tpanic(\"ibcgov: \" + addr + \" is not blocked\")\n\t}\n\treturn adminRequest(cur, \"transfer unblock\", \"transfer: unblock \"+string(a), true, string(a))\n}\n\n// NewTransferAddMemberRequest adds a member to the transfer app's authority\n// after 7 days.\nfunc NewTransferAddMemberRequest(cur realm, addr string) Request {\n\ta := mustAddr(addr)\n\treturn adminRequest(cur, \"transfer add member\", \"transfer: add authority member \"+string(a), true, string(a))\n}\n\n// NewTransferRemoveMemberRequest removes a member of the transfer app's\n// authority at once.\nfunc NewTransferRemoveMemberRequest(cur realm, addr string) Request {\n\ta := mustAddr(addr)\n\tif a == chain.PackageAddress(PkgPath) {\n\t\tpanic(\"ibcgov: this realm stays a member of the transfer app's authority\")\n\t}\n\tif !transfer.IsAuthorityMember(a) {\n\t\tpanic(\"ibcgov: \" + addr + \" is not a member\")\n\t}\n\treturn adminRequest(cur, \"transfer remove member\", \"transfer: remove authority member \"+string(a), false, string(a))\n}\n"},{"name":"ethereum.gno","body":"package ibcgov\n\nimport (\n\t\"bytes\"\n\t\"chain\"\n\t\"crypto/sha256\"\n\t\"encoding/binary\"\n\t\"encoding/hex\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/aib/encoding/abi/v0\"\n\t\"gno.land/p/aib/encoding/ethaddr/v0\"\n\t\"gno.land/p/aib/ibc/app/v0\"\n\t\"gno.land/p/aib/ibc/ics20/controls/v0\"\n\t\"gno.land/p/aib/ibc/lightclient/ethereum/v0\"\n\t\"gno.land/p/aib/ibc/types/v0\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/aib/ibc/apps/transfer\"\n\t\"gno.land/r/aib/ibc/core\"\n)\n\n// The governance channel to Ethereum (ADR pr6375 §5.4, R11): this realm is the\n// IBC app on port gov of the gno core, and sends every Ethereum-bound decision\n// to IBCAdmin as one packet over the governance epoch's client:\n//\n//\tpayload: source port \"gov\" (this realm's port on the gno core),\n//\t         destination port \"ibcgov\" (IBCAdmin's on the Ethereum router),\n//\t         version \"ibcgov-1\", encoding \"application/x-ibcgov\"\n//\tvalue:   version u8 (= 1) ‖ epoch u32 ‖ seq u64 ‖ kind u8 ‖ params\n//\n// big-endian, at most 1024 bytes, params the Solidity ABI encoding of the\n// kind's tuple (abi.encode of its fields; see the New…Request builders). seq\n// is the epoch's own contiguous sequence of Ethereum-bound decisions, from 1,\n// separate from the ledger's numbers; a number is bound to its bytes in the\n// transaction that records the decision and dispatches it, so no number is\n// ever skipped, reused or sent with other bytes. Epochs start at 1 when the\n// channel is bound to its client and rise only with a route replacement.\n//\n// Anyone can call the core's SendPacket for port gov, so OnSendPacket accepts\n// a send only against the one-time dispatch authorization this realm created\n// for it, bound to the client, the epoch, the seq and the payload's hash, and\n// consumes it. Every callback accepts the core as its only caller.\n//\n// IBCAdmin answers with an app acknowledgement\n// epoch u32 ‖ seq u64 ‖ status u8 ‖ redispatch u8 (status 1 recorded,\n// 2 buffered, 3 duplicate, 4 conflict, 5 spent), or an error acknowledgement\n// (out of its window, a stale epoch, a forgery). A timeout, an error\n// acknowledgement of the current epoch, a malformed acknowledgement or\n// redispatch = 1 ask for the decision to be sent again; anyone may call\n// Redispatch, which sends the same seq with identical bytes in a fresh\n// packet, after any answer but a conflict (see Redispatch), a success\n// included. A packet of a past epoch is never sent again.\n//\n// The epoch moves in two phases (ADR pr6375 §7.12, \"Governance epoch switch,\n// two-phase and authenticated\"). A route replacement here retires the old\n// client for the transfer port only: the epoch stays over it, so a decision\n// in flight, the kind 10 among them, can still be re-sent. When IBCAdmin\n// executes the matching kind 10 (or the emergency recovery), it sends an\n// epoch notice from its port over the new client to this realm's port:\n//\n//\tpayload: source port \"ibcgov\" (IBCAdmin's), destination port \"gov\",\n//\t         version \"ibcgov-1\", encoding \"application/x-ibcgov\"\n//\tvalue:   version u8 (= 1) ‖ epoch u32 ‖ newClient\n//\n// big-endian, newClient the router's client id the notice comes from (the\n// kind 10's new client), the rest of the bytes. It is the only packet this\n// realm receives. Ethereum raises its epoch by one per kind 10 or emergency\n// recovery it executes, and sends a notice over each step's new client. This\n// realm records the transitions it expects, in router client ids, each\n// bound to the local gno client governance approved to carry the epoch over\n// the new router client (expected, kept apart from the transfer aliases): a\n// kind 10 when it is dispatched, an emergency recovery by\n// NewExpectRecoveryRequest; a client may have up to maxExpected expected\n// successors, as only one of competing kind 10s can execute. A notice of\n// epoch current + k (k from 1 to maxNoticeSteps) is accepted only when the\n// k-th step of a path of expected transitions from the epoch client's\n// counterparty leads to its source and is bound to exactly the local client\n// it arrives on, its proof verified by the core through that client (Active,\n// its counterparty the notice's source): the epoch then moves there, past any\n// step in between, and the old epoch's client is retired for the gov port\n// (for good once its route replacement has executed here too); the path's\n// intermediate clients are kept, off the graph. A gno client on\n// an expected path from the epoch's is never retired for the gov port\n// before. The\n// app acknowledgement is epoch u32 ‖ seq u64 (0) ‖ status u8 ‖ redispatch u8\n// (0): status 1 when it switches, 3 for the notice of the epoch already\n// switched to. Anything else is refused with an error acknowledgement.\n\nconst (\n\t// GovPort is this realm's port on the gno core: the packets' source port.\n\tGovPort = \"gov\"\n\t// GovDestPort is IBCAdmin's port on the Ethereum router: the packets'\n\t// destination port. ADR pr6375 §5.4 names it \"gov\" on both sides, but\n\t// eureka v3.0.2's router refuses a custom port id shorter than four\n\t// characters, so IBCAdmin registers \"ibcgov\" (gno-eth-ibc IBCAdmin.PORT).\n\tGovDestPort = \"ibcgov\"\n\tGovVersion  = \"ibcgov-1\"\n\tGovEncoding = \"application/x-ibcgov\"\n\n\tEnvelopeVersion = uint8(1)\n\tenvelopeHeader  = 14\n\t// MaxEnvelope bounds an envelope. ADR pr6375 §5.4 says 512 bytes, but a\n\t// kind 6 carrying gno.land's two-item merkle prefix (\"main\",\n\t// \"/pv/vm:gno.land/r/aib/ibc/core:\") encodes to 558 bytes, so both sides\n\t// bound it at 1024 (gno-eth-ibc IBCAdminStorage.MAX_ENVELOPE).\n\tMaxEnvelope = 1024\n\n\t// GovPacketTimeout is how far ahead a governance packet times out, in\n\t// seconds (the core accepts up to 24 hours).\n\tGovPacketTimeout = int64(6 * 3600)\n\n\tackLen = 14\n)\n\n// App acknowledgement statuses.\nconst (\n\tStatusRecorded  = uint8(1)\n\tStatusBuffered  = uint8(2)\n\tStatusDuplicate = uint8(3)\n\tStatusConflict  = uint8(4)\n\tStatusSpent     = uint8(5)\n)\n\n// The Ethereum-bound decision kinds (ADR pr6375 §5.4).\nconst (\n\tKindCancel          = uint8(1)\n\tKindUnpause         = uint8(2)\n\tKindSeats           = uint8(3)\n\tKindLimits          = uint8(4)\n\tKindVoucher         = uint8(5)\n\tKindClient          = uint8(6)\n\tKindUpgrade         = uint8(7)\n\tKindRole            = uint8(8)\n\tKindHandOver        = uint8(9)\n\tKindRouteReplace    = uint8(10)\n\tKindRouteSettings   = uint8(11)\n\tKindSettle          = uint8(12)\n\tKindReopen          = uint8(13)\n\tmaxSettleItems      = 8          // what a 1024-byte envelope holds with a 64-byte client id\n\tmaxCancelRanges     = 14         // what a 1024-byte envelope holds: 78 + 64 bytes a range\n\tadminRoleID         = uint64(0)  // AccessManager's ADMIN_ROLE: never granted or revoked by kind 8\n\tpublicRoleID        = ^uint64(0) // AccessManager's PUBLIC_ROLE\n\tmaxEthSeatsPerCall  = 6\n\tmaxEthGuardianSeats = 2\n)\n\n// NoticeVersion is the version byte that opens an epoch notice; noticeHeader\n// is the length of its fixed part, the version and the epoch.\nconst (\n\tNoticeVersion = uint8(1)\n\tnoticeHeader  = 5\n)\n\n// EpochNotice is the canonical epoch notice of IBCAdmin (see above).\nfunc EpochNotice(e uint32, newClient string) []byte {\n\tout := make([]byte, noticeHeader, noticeHeader+len(newClient))\n\tout[0] = NoticeVersion\n\tbinary.BigEndian.PutUint32(out[1:5], e)\n\treturn append(out, newClient...)\n}\n\n// ParseEpochNotice reads an epoch notice; ok is false when it is too short,\n// of another version, or names no client.\nfunc ParseEpochNotice(b []byte) (e uint32, newClient string, ok bool) {\n\tif len(b) \u003c= noticeHeader || b[0] != NoticeVersion {\n\t\treturn 0, \"\", false\n\t}\n\treturn binary.BigEndian.Uint32(b[1:5]), string(b[noticeHeader:]), true\n}\n\n// epochRec is a governance epoch: the gno client its packets go over and the\n// last seq bound in it.\ntype epochRec struct {\n\tclient string\n\tlast   uint64\n}\n\n// dispatch is one Ethereum-bound decision's packet: its bytes, immutable once\n// bound, and what the channel answered.\ntype dispatch struct {\n\tepoch      uint32\n\tseq        uint64\n\tkind       uint8\n\tbody       []byte\n\tdecision   int64\n\tstatus     uint8  // the last app acknowledgement's status, 0 before any\n\tredispatch bool   // the channel asked for it to be sent again (Redispatch takes it after any answer but a conflict)\n\tinFlight   bool   // a packet is out and not yet acknowledged or timed out\n\ttransport  uint64 // the core sequence of the last packet sent\n\tretransmit bool   // a governed, single-use authorization to re-send it after a conflict (NewRetransmitRequest)\n\tsends      int\n\tevent      string // the last thing that happened, in words\n}\n\ntype authorization struct {\n\tclient string\n\tepoch  uint32\n\tseq    uint64\n\thash   [32]byte\n}\n\nvar (\n\tepoch       uint32   // the current governance epoch, 0 until the channel is bound\n\tepochs      avl.Tree // epochKey -\u003e *epochRec\n\tdispatches  avl.Tree // dispatchKey -\u003e *dispatch\n\tpendingAuth *authorization\n\t// expected is the governance transitions this realm expects Ethereum to\n\t// make, kept apart from the transfer app's route aliases (ADR pr6375\n\t// §7.12, \"expected transitions\"), in router client ids: for an old\n\t// client, its expected successors, at most maxExpected, never\n\t// overwritten or removed (an expectation never confirmed by a notice\n\t// costs nothing). A kind 10 records one when it is dispatched, and\n\t// NewExpectRecoveryRequest one for an emergency recovery.\n\texpected avl.Tree // old router client id -\u003e []expEdge\n\t// awaitingGov is the gno clients a route replacement retired for\n\t// transfers only, being on an expected path of the epoch: each is\n\t// retired for good once the epoch moved off every path through it.\n\tawaitingGov avl.Tree // gno client id -\u003e true\n\t// approvedLocal is every local client an expected transition approves;\n\t// proven is the epoch notices the core proved over one of them, more\n\t// than one step on, waiting for ApplyEpochNotice and their path.\n\tapprovedLocal avl.Tree // gno client id -\u003e true\n\tgovPruneAfter string   // where PruneGovRetirements stopped\n\tproven        avl.Tree // provenKey(epoch, client) -\u003e *provenNotice\n\t// tombstones is the transitions removed or released before an epoch\n\t// switch superseded them: each keeps its local client, and those of the\n\t// transitions beyond it, retained for governance (never retired for the\n\t// gov port, so they can be approved again and carry their notices).\n\t// Live transitions and tombstones share one budget of maxLive; a\n\t// transition moving between the two (a removal, a release, a\n\t// re-approval) leaves the count unchanged. A switch drops those whose\n\t// old router client the new epoch client reaches through neither, and a\n\t// governed purge drops one at a time (NewPurgeTombstoneRequest).\n\ttombstones avl.Tree // tombKey(old, next, local) -\u003e *tomb\n)\n\nconst (\n\t// maxNoticeSteps bounds k, the epochs a notice moves through at once:\n\t// the graph's own bound, as a path of expected transitions never has\n\t// more steps than the graph has transitions. Ethereum executes\n\t// transitions with no cap of its own on how many come between two\n\t// notices gno receives, so no shorter bound is assumed of it.\n\tmaxNoticeSteps = maxLive\n\t// maxExpected bounds a router client's expected successors.\n\tmaxExpected = 4\n\t// maxLive bounds the transitions kept, pending, active and tombstoned,\n\t// together: a new transition that would pass it is refused (a dispatch,\n\t// a recovery expectation), and an epoch switch drops every transition\n\t// and tombstone no longer reachable from the new epoch's client. So\n\t// every walk of the graph reads at most maxLive edges, exactly, with no\n\t// \"too many to tell\" case. 64 holds a chain of 64 steps, or 16 steps of\n\t// 4 competing successors, more than any incident needs.\n\tmaxLive = 64\n)\n\n// expEdge is an expected transition from an old router client: the router\n// client replacing it, and the local gno client governance approved to\n// carry the epoch over it (tracking that router client), the only one a\n// notice for it is accepted over.\n//\n// A transition is an intent from the moment its kind 10 is dispatched (or\n// its recovery expectation recorded): it counts against the old client's\n// successors and keeps its clients open for governance at once, and grants\n// notice authority (active) only once its 7-day, hold-deferred expectation\n// decision applies. A refused, spent or cancelled expectation releases it.\ntype expEdge struct {\n\tto, local string\n\tactive    bool\n\tgen       uint64 // this intent's generation: its owners are indexed under it (ownerKey)\n\towners    int    // how many expectation decisions own it, pending: an intent lives while one does\n}\n\n// The owners of every intent, by (transition, generation, decision): each\n// ownership check and release is one keyed lookup, whatever the number of\n// decisions recorded for one transition. A generation is given to an intent\n// when it is created, so an owner of an intent an epoch switch dropped\n// never owns a later one of the same transition. Each decision removes its\n// own entry when it resolves (applied, refused, spent or cancelled).\nvar (\n\towned   avl.Tree // ownerKey -\u003e true\n\tintents uint64   // the last intent generation given\n)\n\nfunc ownerKey(old, next, local string, gen uint64, owner int64) string {\n\treturn old + \"|\" + next + \"|\" + local + \"|\" + u64s(gen) + \"|\" + itoa(owner)\n}\n\nfunc expectedOf(old string) []expEdge {\n\tif v := expected.Get(old); v != nil {\n\t\treturn v.([]expEdge)\n\t}\n\treturn nil\n}\n\n// checkLocal is why the gno client local cannot carry the governance epoch\n// over the router client remote, or \"\": it must be an Active Ethereum client,\n// not retired, its counterparty remote, authenticating the identity of the\n// route the governance channel rides. Its identity alone proves nothing (a\n// fabricated checkpoint can copy it): the decision names the client, and\n// only that client is accepted.\nfunc checkLocal(local, remote string) string {\n\twant := govIdentity\n\tgot, _ := core.ClientRouteIdentity(local)\n\tswitch {\n\tcase core.ClientType(local) != ethereum.ClientType:\n\t\treturn local + \" is not an Ethereum client\"\n\tcase core.IsClientRetiredFor(local, GovPort):\n\t\treturn local + \" is retired\"\n\tcase core.ClientStatus(local) != \"Active\":\n\t\treturn local + \" is not Active\"\n\tcase core.CounterpartyClientID(local) != remote:\n\t\treturn local + \"'s counterparty is not \" + remote\n\tcase want == \"\":\n\t\treturn \"the governance channel's route records no identity\"\n\tcase got != want:\n\t\treturn local + \" does not authenticate the governance route's identity\"\n\t}\n\treturn \"\"\n}\n\n// liveEdges is how many transitions are kept, pending and active.\nvar liveEdges int\n\n// checkExpect is why the transition old → next over the gno client local\n// cannot be expected, or \"\": its old router client must be reachable from\n// the epoch client (through live transitions or tombstones), the local\n// client must qualify (checkLocal), and a transition not already live must\n// fit the old client's successors (live ones only) and, unless it is\n// tombstoned, the joint budget of maxLive.\nfunc checkExpect(old, next, local string) string {\n\tif epoch == 0 {\n\t\treturn \"the governance channel is not bound\"\n\t}\n\tremotes, _ := retained()\n\tif !remotes[old] {\n\t\treturn \"router client \" + old + \" is not reachable from the governance epoch's client: Ethereum is past it\"\n\t}\n\tif refusal := checkLocal(local, next); refusal != \"\" {\n\t\treturn refusal\n\t}\n\tsucc := expectedOf(old)\n\tfor _, x := range succ {\n\t\tif x.to == next \u0026\u0026 x.local == local {\n\t\t\treturn \"\"\n\t\t}\n\t}\n\tif len(succ) \u003e= maxExpected {\n\t\treturn \"router client \" + old + \" has \" + strconv.Itoa(maxExpected) + \" expected successors already, intents included\"\n\t}\n\t// A tombstoned transition approved again moves back into the graph:\n\t// the joint count does not change, nor does the graph's shape.\n\tif tombstones.Has(tombKey(old, next, local)) {\n\t\treturn \"\"\n\t}\n\t// The graph stays acyclic, live transitions and tombstones together:\n\t// Ethereum never returns to a router client it left (a kind 10 or a\n\t// recovery always creates a new one), so a cycle is never a real\n\t// history, and every path of the graph is simple.\n\tif next == old || reaches(next, old) {\n\t\treturn \"router client \" + next + \" reaches \" + old + \" already: the transition would close a cycle\"\n\t}\n\tif liveEdges+tombstones.Size() \u003e= maxLive {\n\t\treturn \"the governance graph holds \" + strconv.Itoa(maxLive) + \" transitions already, intents and tombstones included: an epoch switch frees those it leaves behind, and a purge frees a tombstone\"\n\t}\n\treturn \"\"\n}\n\n// tomb is a removed or released transition kept for retention: its local\n// client, and every transition still recorded beyond its new router client.\ntype tomb struct{ old, to, local string }\n\nfunc tombKey(old, next, local string) string { return old + \"|\" + next + \"|\" + local }\n\n// entomb records the transition old → next over local, just dropped from\n// the graph, as a tombstone: it takes the place the live transition had in\n// the joint budget, so it needs no room.\nfunc entomb(old, next, local string) {\n\tk := tombKey(old, next, local)\n\tif tombstones.Has(k) {\n\t\treturn\n\t}\n\ttombstones.Set(k, \u0026tomb{old: old, to: next, local: local})\n\tchain.Emit(\"GovTombstone\", \"from\", old, \"to\", next, \"client\", local)\n}\n\n// tombsOf is the tombstones leaving router client old.\nfunc tombsOf(old string) []*tomb {\n\tvar out []*tomb\n\ttombstones.Iterate(old+\"|\", old+\"}\", func(_ string, v any) bool { // '}' follows '|'\n\t\tout = append(out, v.(*tomb))\n\t\treturn false\n\t})\n\treturn out\n}\n\n// Tombstones is how many removed or released transitions still retain their\n// local clients for governance.\nfunc Tombstones() int { return tombstones.Size() }\n\nfunc setEdges(old string, edges []expEdge) {\n\tif len(edges) == 0 {\n\t\texpected.Remove(old)\n\t\treturn\n\t}\n\texpected.Set(old, edges)\n}\n\n// intend records the transition old → next over local as an intent owned by\n// the expectation decision owner (adding the owner to it when it is\n// recorded already), and returns the intent's generation, which the\n// decision keeps; its caller checked it (checkExpect).\nfunc intend(old, next, local string, owner int64) uint64 {\n\tsucc := append([]expEdge{}, expectedOf(old)...)\n\tfor i := range succ {\n\t\tif succ[i].to == next \u0026\u0026 succ[i].local == local {\n\t\t\tsucc[i].owners++\n\t\t\tsetEdges(old, succ)\n\t\t\towned.Set(ownerKey(old, next, local, succ[i].gen, owner), true)\n\t\t\treturn succ[i].gen\n\t\t}\n\t}\n\tintents++\n\tsetEdges(old, append(succ, expEdge{to: next, local: local, gen: intents, owners: 1}))\n\towned.Set(ownerKey(old, next, local, intents, owner), true)\n\tliveEdges++\n\ttombstones.Remove(tombKey(old, next, local)) // the graph retains it again\n\tchain.Emit(\"GovIntent\", \"from\", old, \"to\", next, \"client\", local, \"decision\", itoa(owner))\n\treturn intents\n}\n\n// expect makes the transition old → next over local active, recording it\n// first if need be; its caller checked it (checkExpect).\nfunc expect(old, next, local string) {\n\tsucc := append([]expEdge{}, expectedOf(old)...)\n\tfound := false\n\tfor i := range succ {\n\t\tif succ[i].to == next \u0026\u0026 succ[i].local == local {\n\t\t\tsucc[i].active, succ[i].owners, found = true, 0, true\n\t\t}\n\t}\n\tif !found {\n\t\tsucc = append(succ, expEdge{to: next, local: local, active: true})\n\t\tliveEdges++\n\t}\n\tsetEdges(old, succ)\n\ttombstones.Remove(tombKey(old, next, local))\n\tapprovedLocal.Set(local, true)\n\tchain.Emit(\"GovExpect\", \"from\", old, \"to\", next, \"client\", local)\n}\n\n// ownsIntent reports whether the intent old → next over local of\n// generation gen is still pending and owned by the decision owner: an epoch\n// switch that left it behind, or a release, dropped it otherwise. A keyed\n// lookup, among at most maxExpected successors.\nfunc ownsIntent(old, next, local string, gen uint64, owner int64) bool {\n\tfor _, x := range expectedOf(old) {\n\t\tif x.to == next \u0026\u0026 x.local == local \u0026\u0026 !x.active \u0026\u0026 x.gen == gen {\n\t\t\treturn owned.Has(ownerKey(old, next, local, gen, owner))\n\t\t}\n\t}\n\treturn false\n}\n\n// disown removes owner's entry, whatever became of the intent.\nfunc disown(old, next, local string, gen uint64, owner int64) {\n\towned.Remove(ownerKey(old, next, local, gen, owner))\n}\n\n// release drops owner from the intent old → next over local: the intent\n// goes with its last owner, unless it is active, leaving a tombstone in its\n// place that keeps its local client retained for governance.\nfunc release(old, next, local string, gen uint64, owner int64) {\n\tif !owned.Has(ownerKey(old, next, local, gen, owner)) {\n\t\treturn // not an owner (any more): nothing to release\n\t}\n\tdisown(old, next, local, gen, owner)\n\tsucc := append([]expEdge{}, expectedOf(old)...)\n\tfor i := range succ {\n\t\tx := succ[i]\n\t\tif x.to != next || x.local != local || x.active || x.gen != gen {\n\t\t\tcontinue\n\t\t}\n\t\tif x.owners \u003e 1 {\n\t\t\tsucc[i].owners--\n\t\t\tsetEdges(old, succ)\n\t\t\treturn\n\t\t}\n\t\tsetEdges(old, append(succ[:i:i], succ[i+1:]...))\n\t\tliveEdges--\n\t\tentomb(old, next, local)\n\t\treturn\n\t}\n}\n\n// retained is every router client the epoch client's router counterparty\n// reaches, itself included, and every local client retained for\n// governance, through live transitions (pending or active) and tombstones\n// alike: the graph an epoch switch keeps and retention protects. Each router\n// client is expanded once, so the walk reads every transition and tombstone\n// at most once: at most maxLive in all.\nfunc retained() (remotes, locals map[string]bool) {\n\tif epoch == 0 {\n\t\treturn nil, nil\n\t}\n\tstart := core.CounterpartyClientID(EpochClient())\n\tremotes, locals = map[string]bool{start: true}, map[string]bool{}\n\tfront := []string{start}\n\tfor len(front) \u003e 0 {\n\t\tvar next []string\n\t\tvisit := func(to, local string) {\n\t\t\tlocals[local] = true\n\t\t\tif !remotes[to] {\n\t\t\t\tremotes[to] = true\n\t\t\t\tnext = append(next, to)\n\t\t\t}\n\t\t}\n\t\tfor _, c := range front {\n\t\t\tfor _, x := range expectedOf(c) {\n\t\t\t\tvisit(x.to, x.local)\n\t\t\t}\n\t\t\tfor _, t := range tombsOf(c) {\n\t\t\t\tvisit(t.to, t.local)\n\t\t\t}\n\t\t}\n\t\tfront = next\n\t}\n\treturn remotes, locals\n}\n\n// reaches reports whether router client from reaches target through live\n// transitions and tombstones: each router client expanded once, at most\n// maxLive edges read.\nfunc reaches(from, target string) bool {\n\tseen := map[string]bool{from: true}\n\tfront := []string{from}\n\tfor len(front) \u003e 0 {\n\t\tvar next []string\n\t\tfor _, c := range front {\n\t\t\ttos := []string{}\n\t\t\tfor _, x := range expectedOf(c) {\n\t\t\t\ttos = append(tos, x.to)\n\t\t\t}\n\t\t\tfor _, t := range tombsOf(c) {\n\t\t\t\ttos = append(tos, t.to)\n\t\t\t}\n\t\t\tfor _, to := range tos {\n\t\t\t\tif to == target {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t\tif !seen[to] {\n\t\t\t\t\tseen[to] = true\n\t\t\t\t\tnext = append(next, to)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfront = next\n\t}\n\treturn false\n}\n\n// pruneUnreachable drops every transition and tombstone whose old router\n// client the epoch client's counterparty no longer reaches through either\n// (an epoch switch left it behind, superseded): bounded by maxLive.\nfunc pruneUnreachable() {\n\tseen, _ := retained()\n\tvar drop []string\n\texpected.Iterate(\"\", \"\", func(old string, v any) bool {\n\t\tif !seen[old] {\n\t\t\tdrop = append(drop, old)\n\t\t}\n\t\treturn false\n\t})\n\tfor _, old := range drop {\n\t\tliveEdges -= len(expectedOf(old))\n\t\texpected.Remove(old)\n\t}\n\tvar dropTombs []string\n\ttombstones.Iterate(\"\", \"\", func(k string, v any) bool {\n\t\tif !seen[v.(*tomb).old] {\n\t\t\tdropTombs = append(dropTombs, k)\n\t\t}\n\t\treturn false\n\t})\n\tfor _, k := range dropTombs {\n\t\ttombstones.Remove(k)\n\t}\n}\n\n// onExpectedPath reports whether the gno client c carries the governance\n// epoch or is the local client of a transition, pending, active or\n// tombstoned (removed or released, not yet superseded), reachable from it\n// at any distance through either (any path in the graph is within a\n// notice's maxNoticeSteps): such a client is never retired for the gov port, so a\n// notice over it can still arrive and the epoch's decisions still be\n// re-sent. Exact: the graph holds at most maxLive transitions.\nfunc onExpectedPath(c string) bool {\n\tif epoch == 0 {\n\t\treturn false\n\t}\n\tif c == EpochClient() {\n\t\treturn true\n\t}\n\t_, locals := retained()\n\treturn locals[c]\n}\n\n// awaitGov notes a gno client retired for transfers only, to retire for good\n// once the epoch has moved off every expected path through it.\nfunc awaitGov(c string) { awaitingGov.Set(c, true) }\n\nfunc epochKey(e uint32) string {\n\ts := strconv.FormatUint(uint64(e), 10)\n\treturn strings.Repeat(\"0\", 10-len(s)) + s\n}\n\nfunc dispatchKey(e uint32, seq uint64) string {\n\ts := u64s(seq)\n\treturn epochKey(e) + \"/\" + strings.Repeat(\"0\", 20-len(s)) + s\n}\n\nfunc epochRecOf(e uint32) *epochRec {\n\tv := epochs.Get(epochKey(e))\n\tif v == nil {\n\t\treturn nil\n\t}\n\treturn v.(*epochRec)\n}\n\nfunc dispatchOf(e uint32, seq uint64) *dispatch {\n\tv := dispatches.Get(dispatchKey(e, seq))\n\tif v == nil {\n\t\treturn nil\n\t}\n\treturn v.(*dispatch)\n}\n\n// Envelope is the canonical envelope of a governance packet.\nfunc Envelope(e uint32, seq uint64, kind uint8, params []byte) []byte {\n\tout := make([]byte, envelopeHeader, envelopeHeader+len(params))\n\tout[0] = EnvelopeVersion\n\tbinary.BigEndian.PutUint32(out[1:5], e)\n\tbinary.BigEndian.PutUint64(out[5:13], seq)\n\tout[13] = kind\n\treturn append(out, params...)\n}\n\n// ParseEnvelope reads an envelope's prefix; ok is false when it is too short\n// or of another version.\nfunc ParseEnvelope(b []byte) (e uint32, seq uint64, kind uint8, params []byte, ok bool) {\n\tif len(b) \u003c envelopeHeader || b[0] != EnvelopeVersion {\n\t\treturn 0, 0, 0, nil, false\n\t}\n\treturn binary.BigEndian.Uint32(b[1:5]), binary.BigEndian.Uint64(b[5:13]), b[13], b[envelopeHeader:], true\n}\n\n// Epoch is the current governance epoch (0 until bound), and EpochClient its\n// gno client.\nfunc Epoch() uint32 { return epoch }\n\nfunc EpochClient() string {\n\tif r := epochRecOf(epoch); r != nil {\n\t\treturn r.client\n\t}\n\treturn \"\"\n}\n\n// LastSeq is the last seq bound in epoch e.\nfunc LastSeq(e uint32) uint64 {\n\tif r := epochRecOf(e); r != nil {\n\t\treturn r.last\n\t}\n\treturn 0\n}\n\n// DispatchBody is the envelope bound to (e, seq), copied, or nil.\nfunc DispatchBody(e uint32, seq uint64) []byte {\n\tif d := dispatchOf(e, seq); d != nil {\n\t\treturn append([]byte{}, d.body...)\n\t}\n\treturn nil\n}\n\n// DispatchState says what the channel answered for (e, seq): the last app\n// acknowledgement status (0 if none), whether it may be re-dispatched, whether\n// a packet is in flight, how many times it was sent and the last event.\nfunc DispatchState(e uint32, seq uint64) (status uint8, redispatch, inFlight bool, sends int, event string) {\n\td := dispatchOf(e, seq)\n\tif d == nil {\n\t\treturn 0, false, false, 0, \"\"\n\t}\n\treturn d.status, d.redispatch, d.inFlight, d.sends, d.event\n}\n\n// DispatchTransport is the core sequence of the last packet sent for (e,\n// seq), 0 if none.\nfunc DispatchTransport(e uint32, seq uint64) uint64 {\n\tif d := dispatchOf(e, seq); d != nil {\n\t\treturn d.transport\n\t}\n\treturn 0\n}\n\n// govIdentity is the identity of the route the governance channel was bound\n// over (RouteIdentity): every client approved to carry a later epoch must\n// authenticate it.\nvar govIdentity string\n\n// bindChannel opens epoch 1 over client.\nfunc bindChannel(client string) {\n\tgovIdentity = RouteIdentity(client)\n\tsetEpoch(1, client)\n}\n\n// setEpoch opens epoch e over client (IBCAdmin's epoch notice): the earlier\n// epochs' unacknowledged decisions are never sent again.\nfunc setEpoch(e uint32, client string) {\n\tepoch = e\n\tepochs.Set(epochKey(epoch), \u0026epochRec{client: client})\n\tchain.Emit(\"GovEpoch\", \"epoch\", strconv.FormatUint(uint64(epoch), 10), \"client\", client)\n}\n\n// bind binds the next seq of the current epoch to kind and params and sends\n// it. It returns the seq.\nfunc bind(cur realm, decision int64, kind uint8, params []byte) (uint64, string) {\n\trec := epochRecOf(epoch)\n\tif rec == nil {\n\t\treturn 0, \"the governance channel is not bound\"\n\t}\n\t// Every failure the core's send would raise is refused here, so the\n\t// decision is spent rather than aborted and left pending.\n\tswitch {\n\tcase core.Paused():\n\t\treturn 0, \"the core is paused: no governance packet can be sent\"\n\tcase core.IsAppPaused(GovPort):\n\t\treturn 0, \"the gov app is paused on the core\"\n\tcase core.IsClientRetiredFor(rec.client, GovPort):\n\t\treturn 0, \"the epoch's client \" + rec.client + \" is retired\"\n\tcase core.ClientStatus(rec.client) != \"Active\":\n\t\treturn 0, \"the epoch's client \" + rec.client + \" is \" + core.ClientStatus(rec.client) + \", not Active\"\n\tcase core.CounterpartyClientID(rec.client) == \"\":\n\t\treturn 0, \"the epoch's client \" + rec.client + \" has no counterparty\"\n\t}\n\tif latest, ok := core.ClientLatestTimestamp(rec.client); !ok || latest \u003e= uint64(now()+GovPacketTimeout) {\n\t\treturn 0, \"the epoch's client \" + rec.client + \" is ahead of the packet's timeout\"\n\t}\n\tseq := rec.last + 1\n\tbody := Envelope(epoch, seq, kind, params)\n\tif len(body) \u003e MaxEnvelope {\n\t\treturn 0, \"the envelope is \" + strconv.Itoa(len(body)) + \" bytes, above \" + strconv.Itoa(MaxEnvelope)\n\t}\n\trec.last = seq\n\td := \u0026dispatch{epoch: epoch, seq: seq, kind: kind, body: body, decision: decision}\n\tdispatches.Set(dispatchKey(epoch, seq), d)\n\tsend(cur, d)\n\treturn seq, \"\"\n}\n\n// send sends d's bytes over its epoch's client, under a one-time dispatch\n// authorization OnSendPacket consumes.\nfunc send(cur realm, d *dispatch) {\n\trec := epochRecOf(d.epoch)\n\tpendingAuth = \u0026authorization{client: rec.client, epoch: d.epoch, seq: d.seq, hash: sha256.Sum256(d.body)}\n\ttransportSeq := core.SendPacket(cross(cur), types.MsgSendPacket{\n\t\tSourceClient:     rec.client,\n\t\tTimeoutTimestamp: uint64(now() + GovPacketTimeout),\n\t\tPayloads: []types.Payload{{\n\t\t\tSourcePort:      GovPort,\n\t\t\tDestinationPort: GovDestPort,\n\t\t\tVersion:         GovVersion,\n\t\t\tEncoding:        GovEncoding,\n\t\t\tValue:           append([]byte{}, d.body...),\n\t\t}},\n\t})\n\tif pendingAuth != nil {\n\t\tpanic(\"ibcgov: the dispatch authorization was not consumed\")\n\t}\n\td.inFlight, d.redispatch, d.transport = true, false, transportSeq\n\td.sends++\n\td.event = \"sent as packet \" + u64s(transportSeq)\n\tchain.Emit(\"GovPacketSent\", \"epoch\", strconv.FormatUint(uint64(d.epoch), 10), \"seq\", u64s(d.seq),\n\t\t\"kind\", strconv.Itoa(int(d.kind)), \"client\", rec.client, \"sequence\", u64s(transportSeq))\n}\n\n// Redispatch sends decision (e, seq) of the current epoch again, in a fresh\n// transport packet with identical bytes, whenever no packet of it is in\n// flight, whatever the channel answered before: a timeout, an error or\n// malformed acknowledgement, redispatch = 1, and a success too (recorded,\n// buffered, duplicate, spent), since a success acknowledgement may have\n// been forged by a compromised Ethereum-side verifier while the decision\n// never reached IBCAdmin. IBCAdmin deduplicates by seq and hash: the same\n// bytes at a seq it holds are a duplicate (status 3, redispatch 0), a no-op.\n// The one exception is a conflict acknowledged without redispatch = 1:\n// IBCAdmin re-flags itself on every conflict, so a free re-send would let\n// anyone keep it deferred; it is re-sent once per governed authorization\n// (NewRetransmitRequest), which this call uses up. Anyone may call it. A past epoch's decisions are\n// never sent again.\nfunc Redispatch(cur realm, e uint32, seq uint64) {\n\td := dispatchOf(e, seq)\n\tif d == nil {\n\t\tpanic(\"ibcgov: no dispatched decision \" + strconv.FormatUint(uint64(e), 10) + \"/\" + u64s(seq))\n\t}\n\tif e != epoch {\n\t\tpanic(\"ibcgov: epoch \" + strconv.FormatUint(uint64(e), 10) + \" is past: its decisions are never sent again\")\n\t}\n\tif d.inFlight || (d.status == StatusConflict \u0026\u0026 !d.redispatch \u0026\u0026 !d.retransmit) {\n\t\tpanic(\"ibcgov: decision \" + u64s(seq) + \" is not re-dispatchable (\" + d.event + \")\")\n\t}\n\tif d.status == StatusConflict \u0026\u0026 !d.redispatch {\n\t\td.retransmit = false // used once: another re-send needs another decision\n\t}\n\tsend(cur, d)\n}\n\n// ---------------------------------------------------------------- the app\n\n// App is the IBC app registered on port gov of the gno core.\ntype App struct{}\n\nvar _ app.IBCApp = \u0026App{}\n\n// assertCoreCaller panics unless the IBC core invokes the callback: the\n// methods are exported and any realm can hold an *App.\nfunc assertCoreCaller(cur realm) {\n\tif p := cur.Previous().PkgPath(); p != CorePath {\n\t\tpanic(\"ibcgov: the gov callbacks are the IBC core's, not \" + who(0, cur.Previous()))\n\t}\n}\n\n// OnSendPacket accepts a send on port gov only against the dispatch\n// authorization this realm created for it, and consumes the authorization.\nfunc (a *App) OnSendPacket(cur realm, sourceClient, destinationClient string, sequence uint64, payload types.Payload) error {\n\tassertCoreCaller(cur)\n\tauth := pendingAuth\n\tpendingAuth = nil\n\tif auth == nil {\n\t\treturn ufmt.Errorf(\"ibcgov: governance packets are sent by ibcgov's decisions only\")\n\t}\n\tif payload.SourcePort != GovPort || payload.DestinationPort != GovDestPort || payload.Version != GovVersion || payload.Encoding != GovEncoding {\n\t\treturn ufmt.Errorf(\"ibcgov: not a governance payload\")\n\t}\n\tif sourceClient != auth.client {\n\t\treturn ufmt.Errorf(\"ibcgov: authorized for client %s, not %s\", auth.client, sourceClient)\n\t}\n\tif sha256.Sum256(payload.Value) != auth.hash {\n\t\treturn ufmt.Errorf(\"ibcgov: the payload differs from the authorized one\")\n\t}\n\te, seq, _, _, ok := ParseEnvelope(payload.Value)\n\tif !ok || e != auth.epoch || seq != auth.seq {\n\t\treturn ufmt.Errorf(\"ibcgov: the envelope differs from the authorized one\")\n\t}\n\treturn nil\n}\n\n// OnRecvPacket takes IBCAdmin's epoch notice, the only packet this realm\n// receives (see above), and refuses every other: an error acknowledgement.\nfunc (a *App) OnRecvPacket(cur realm, sourceClient, destinationClient string, sequence uint64, payload types.Payload) types.RecvPacketResult {\n\tassertCoreCaller(cur)\n\te, status, refusal := takeNotice(cur, sourceClient, destinationClient, payload)\n\tif refusal != \"\" {\n\t\tchain.Emit(\"GovNotice\", \"client\", destinationClient, \"status\", \"refused\", \"reason\", refusal)\n\t\treturn types.RecvPacketResult{Status: types.PacketStatus_Failure}\n\t}\n\tack := make([]byte, ackLen)\n\tbinary.BigEndian.PutUint32(ack[0:4], e)\n\tack[12] = status\n\treturn types.RecvPacketResult{Status: types.PacketStatus_Success, Acknowledgement: ack}\n}\n\n// takeNotice applies an epoch notice received over client from the router's\n// client source, or says why it refuses it.\nfunc takeNotice(cur realm, source, client string, payload types.Payload) (uint32, uint8, string) {\n\tif payload.SourcePort != GovDestPort || payload.DestinationPort != GovPort || payload.Version != GovVersion || payload.Encoding != GovEncoding {\n\t\treturn 0, 0, \"not an epoch notice payload\"\n\t}\n\te, named, ok := ParseEpochNotice(payload.Value)\n\tswitch {\n\tcase !ok:\n\t\treturn 0, 0, \"a malformed epoch notice\"\n\tcase named != source:\n\t\treturn 0, 0, \"the notice names \" + named + \", not \" + source + \", the client it came from\"\n\tcase epoch != 0 \u0026\u0026 e == epoch \u0026\u0026 client == EpochClient():\n\t\treturn e, StatusDuplicate, \"\" // the switch happened already: acknowledged again\n\t}\n\tif epoch == 0 || e \u003c= epoch || e-epoch \u003e maxNoticeSteps {\n\t\treturn 0, 0, \"the notice is of epoch \" + strconv.FormatUint(uint64(e), 10) + \", not one of the next \" + strconv.Itoa(maxNoticeSteps) + \" after \" + strconv.FormatUint(uint64(epoch), 10)\n\t}\n\t// Epoch current + k goes over a client reached from the epoch's through\n\t// exactly k expected transitions: Ethereum raised its epoch once per\n\t// kind 10 or recovery it executed, and sent a notice over each step's\n\t// new client. A later notice moves the epoch past the steps in between,\n\t// whose notices may never come (a client that failed meanwhile). The\n\t// next step (k = 1) is checked here, among at most maxExpected edges; a\n\t// later one is recorded as proven, and applied by anyone who names its\n\t// path (ApplyEpochNotice), checked in O(k): no search of the expected\n\t// graph, however it branches, ever refuses an authentic notice.\n\tk := int(e - epoch)\n\tif k \u003e 1 {\n\t\tif !approvedLocal.Has(client) {\n\t\t\treturn 0, 0, client + \" is no client approved to carry a governance epoch\"\n\t\t}\n\t\tproven.Set(provenKey(e, client), \u0026provenNotice{source: source, gen: core.ClientTrustGeneration(client)})\n\t\tchain.Emit(\"GovNotice\", \"client\", client, \"epoch\", strconv.FormatUint(uint64(e), 10), \"status\", \"proven\")\n\t\treturn 0, 0, \"epoch \" + strconv.FormatUint(uint64(e), 10) + \" is \" + strconv.Itoa(k) + \" steps on: recorded as proven, applied by ApplyEpochNotice with its path\"\n\t}\n\tfound := false\n\tfor _, x := range expectedOf(core.CounterpartyClientID(EpochClient())) {\n\t\tif x.active \u0026\u0026 x.to == source \u0026\u0026 x.local == client {\n\t\t\tfound = true\n\t\t}\n\t}\n\tif !found {\n\t\treturn 0, 0, \"epoch \" + strconv.FormatUint(uint64(e), 10) + \" is expected over no transition to \" + source + \" carried by \" + client + \" from \" + core.CounterpartyClientID(EpochClient())\n\t}\n\tswitchEpoch(cur, e, client, nil)\n\treturn e, StatusRecorded, \"\"\n}\n\n// switchEpoch opens epoch e over client and retires for governance the old\n// epoch's client, for good when its route replacement executed here (it is\n// retired for transfers already), otherwise for the gov port, its\n// replacement retiring the rest. That is the only client a switch retires:\n// the path's intermediate clients (supplied by whoever applied the notice,\n// not proven to be Ethereum's) and every other client its pruning\n// disconnects (on a branch it left behind, a cancelled recovery's say) is\n// taken out of awaitingGov and kept off the graph, its gov port open and\n// inert, as a purge keeps the clients it disconnects, since Ethereum may\n// still reach it another way (expected again, its notice applies). Its work\n// is bounded: two walks of the graph.\nfunc switchEpoch(cur realm, e uint32, client string, passed []string) {\n\told := EpochClient()\n\t_, before := retained()\n\tsetEpoch(e, client)\n\tpruneUnreachable()\n\t_, after := retained()\n\tfor c := range before {\n\t\tif !after[c] \u0026\u0026 awaitingGov.Has(c) {\n\t\t\tawaitingGov.Remove(c)\n\t\t}\n\t}\n\t// Only the old epoch client: the notice of a later epoch proves\n\t// Ethereum has moved past it. The path's intermediate clients are named\n\t// by whoever applied the notice, not proven passed (another path to the\n\t// same client may be the one Ethereum took), so they are kept off the\n\t// graph like every client the pruning disconnects. Never the client\n\t// just adopted (the path is simple; this guards it).\n\tif old != client {\n\t\tretireForGov(cur, old)\n\t\tawaitingGov.Remove(old)\n\t}\n\tfor _, c := range passed {\n\t\tawaitingGov.Remove(c)\n\t}\n}\n\n// PruneGovRetirements retires for governance up to n (1 to 64) of the gno\n// clients a route replacement retired for transfers while they were on an\n// expected path of the governance epoch, once they no longer are: anyone may\n// call it. A safety net only: nothing leaves a client here once no path\n// reaches it (a removal or a release leaves a tombstone that keeps it\n// reachable; a switch and a purge take the clients they disconnect out of\n// awaitingGov), and a switch retires the old epoch client itself, so a\n// prune retires nothing a switch did not. A client on a path, or one whose paths cannot be told within the\n// search's bounds, is kept for a later call. Each call visits n clients from\n// where the last one stopped, wrapping round at the end, so a client kept\n// never holds back those after it. It returns how many it retired.\nfunc PruneGovRetirements(cur realm, n int) int {\n\tif n \u003c 1 || n \u003e 64 {\n\t\tpanic(\"ibcgov: prune 1 to 64 clients a call\")\n\t}\n\t// onExpectedPath, with the epoch client and the retained graph read\n\t// once: the walk changes neither.\n\tepochClient := EpochClient()\n\t_, locals := retained()\n\tvar done []string\n\tseen := 0\n\tlast := \"\"\n\tawaitingGov.Iterate(after(govPruneAfter), \"\", func(c string, _ any) bool {\n\t\tif seen == n {\n\t\t\treturn true\n\t\t}\n\t\tseen++\n\t\tlast = c\n\t\tif epoch == 0 || c != epochClient \u0026\u0026 !locals[c] {\n\t\t\tdone = append(done, c)\n\t\t}\n\t\treturn false\n\t})\n\tif seen \u003c n {\n\t\tlast = \"\" // the walk reached the end: the next starts over\n\t}\n\tgovPruneAfter = last\n\tfor _, c := range done {\n\t\tawaitingGov.Remove(c)\n\t\tretireForGov(cur, c)\n\t}\n\treturn len(done)\n}\n\n// AwaitingGovRetirement is how many clients wait for PruneGovRetirements.\nfunc AwaitingGovRetirement() int { return awaitingGov.Size() }\n\n// provenNotice is an epoch notice the core proved over a local client: the\n// router client it came from, and the local client's trust generation then.\ntype provenNotice struct {\n\tsource string\n\tgen    uint64\n}\n\nfunc provenKey(e uint32, client string) string { return epochKey(e) + \"|\" + client }\n\n// applicable reports whether the notice of epoch e recorded as proven\n// over client from the router client source may still be applied: its\n// epoch past the current and within maxNoticeSteps, and the client still\n// vouching for it (Active, not retired for the gov port, at the trust\n// generation it was proven under), as ApplyEpochNotice requires; the path\n// it needs is checked by the caller.\nfunc applicable(e uint32, client, source string) bool {\n\tif e \u003c= epoch || e-epoch \u003e maxNoticeSteps {\n\t\treturn false\n\t}\n\tv := proven.Get(provenKey(e, client))\n\tif v == nil {\n\t\treturn false\n\t}\n\tpn := v.(*provenNotice)\n\treturn pn.source == source \u0026\u0026 core.ClientStatus(client) == \"Active\" \u0026\u0026 !core.IsClientRetiredFor(client, GovPort) \u0026\u0026 core.ClientTrustGeneration(client) == pn.gen\n}\n\n// pinnedBy is a client over which a notice recorded as proven may still be\n// applied along a path that uses the transition from → to over local, or\n// \"\": a path of active transitions, or (all, for a purge) one that may also\n// take live and tombstoned transitions an approval could make active. ApplyEpochNotice needs a path of exactly e - current active\n// transitions from the epoch client's router counterparty, ending at the\n// notice's source over its client, so only a transition on such a path is\n// pinned: one at step i of it leaves from a router client reached in exactly\n// i - 1 steps, and either is the last step (to the source, over the notice's\n// client) or leads on, in exactly the steps left, to that last step. The\n// graph is acyclic (checkExpect), so every such walk of exact length is a\n// simple path, the only kind ApplyEpochNotice accepts. Every\n// recorded notice is read, not only the highest per client. Bounded: the\n// graph holds at most maxLive edges and a path at most maxNoticeSteps\n// steps, so each walk is at most 64 layers of 64 edges, and each notice\n// looked up is a distinct (epoch, client) pair, deduplicated first.\nfunc pinnedBy(from, to, local string, all bool) string {\n\t// The transitions a path may take: active ones only, or (all) every\n\t// live one and every tombstone, which an approval could make active.\n\t// Read once a router client: the graph does not change during the call.\n\tmemo := map[string][]expEdge{}\n\touts := func(c string) []expEdge {\n\t\tif out, ok := memo[c]; ok {\n\t\t\treturn out\n\t\t}\n\t\tvar out []expEdge\n\t\tfor _, x := range expectedOf(c) {\n\t\t\tif x.active || all {\n\t\t\t\tout = append(out, x)\n\t\t\t}\n\t\t}\n\t\tif all {\n\t\t\tfor _, t := range tombsOf(c) {\n\t\t\t\tout = append(out, expEdge{to: t.to, local: t.local})\n\t\t\t}\n\t\t}\n\t\tmemo[c] = out\n\t\treturn out\n\t}\n\t// The steps i - 1 (0 to 63) in which from is reached exactly.\n\tvar at []int\n\tfront := map[string]bool{core.CounterpartyClientID(EpochClient()): true}\n\tfor d := 0; d \u003c maxNoticeSteps \u0026\u0026 len(front) \u003e 0; d++ {\n\t\tif front[from] {\n\t\t\tat = append(at, d)\n\t\t}\n\t\tnext := map[string]bool{}\n\t\tfor c := range front {\n\t\t\tfor _, x := range outs(c) {\n\t\t\t\tnext[x.to] = true\n\t\t\t}\n\t\t}\n\t\tfront = next\n\t}\n\tif len(at) == 0 {\n\t\treturn \"\"\n\t}\n\ttype want struct {\n\t\te              uint32\n\t\tclient, source string\n\t}\n\tseen := map[want]bool{}\n\tvar wants []want\n\tadd := func(steps int, client, source string) {\n\t\tif steps \u003e maxNoticeSteps {\n\t\t\treturn\n\t\t}\n\t\tw := want{epoch + uint32(steps), client, source}\n\t\tif !seen[w] {\n\t\t\tseen[w] = true\n\t\t\twants = append(wants, w)\n\t\t}\n\t}\n\t// The transition itself as the last step: k = d + 1.\n\tfor _, d := range at {\n\t\tadd(d+1, local, to)\n\t}\n\t// The last steps reached from to, j steps after the transition: k = d +\n\t// 1 + j + 1.\n\tfront = map[string]bool{to: true}\n\tfor j := 0; at[0]+j+2 \u003c= maxNoticeSteps \u0026\u0026 len(front) \u003e 0; j++ {\n\t\tnext := map[string]bool{}\n\t\tfor c := range front {\n\t\t\tfor _, x := range outs(c) {\n\t\t\t\tnext[x.to] = true\n\t\t\t\tfor _, d := range at {\n\t\t\t\t\tadd(d+j+2, x.local, x.to)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfront = next\n\t}\n\tfor _, w := range wants {\n\t\tif applicable(w.e, w.client, w.source) {\n\t\t\treturn w.client\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// ApplyEpochNotice applies an epoch notice of epoch e that the core proved\n// over client (OnRecvPacket recorded it, being more than one step on), along\n// path, the k = e - current expected transitions from the epoch client's\n// router counterparty, as comma-separated \"routerClient/localClient\" pairs\n// ending with the notice's source and client: each must be a recorded\n// expected transition with its local binding. Anyone may call it; it is\n// checked in O(k), k at most maxNoticeSteps, 64 (ADR pr6375 §7.12).\nfunc ApplyEpochNotice(cur realm, e uint32, client, path string) {\n\tv := proven.Get(provenKey(e, client))\n\tif v == nil {\n\t\tpanic(\"ibcgov: no epoch notice of epoch \" + strconv.FormatUint(uint64(e), 10) + \" was proven over \" + client)\n\t}\n\tpn := v.(*provenNotice)\n\tsource := pn.source\n\t// The proof holds while the client that verified it does: Active, not\n\t// retired, and at the trust generation it was proven under (no freeze,\n\t// recovery, upgrade or migration since). Otherwise a freshly proven\n\t// notice is needed.\n\tswitch {\n\tcase core.ClientStatus(client) != \"Active\":\n\t\tpanic(\"ibcgov: \" + client + \" is \" + core.ClientStatus(client) + \": relay the notice again once it is Active\")\n\tcase core.IsClientRetiredFor(client, GovPort):\n\t\tpanic(\"ibcgov: \" + client + \" is retired\")\n\tcase core.ClientTrustGeneration(client) != pn.gen:\n\t\tpanic(\"ibcgov: \" + client + \"'s trust generation changed since the notice was proven: relay it again\")\n\t}\n\tif epoch == 0 || e \u003c= epoch || e-epoch \u003e maxNoticeSteps {\n\t\tpanic(\"ibcgov: epoch \" + strconv.FormatUint(uint64(e), 10) + \" is not one of the next \" + strconv.Itoa(maxNoticeSteps) + \" after \" + strconv.FormatUint(uint64(epoch), 10))\n\t}\n\tsteps := splitList(path)\n\tif len(steps) != int(e-epoch) {\n\t\tpanic(\"ibcgov: the path names \" + strconv.Itoa(len(steps)) + \" transitions, not \" + strconv.FormatUint(uint64(e-epoch), 10))\n\t}\n\tprev := core.CounterpartyClientID(EpochClient())\n\tvar to, local string\n\tvar passed []string\n\t// A simple path: no router client or local client twice, the epoch's\n\t// own included (the graph admits no cycle, so this only guards it).\n\tseenRouter := map[string]bool{prev: true}\n\tseenLocal := map[string]bool{EpochClient(): true}\n\tfor i, st := range steps {\n\t\tj := strings.Index(st, \"/\")\n\t\tif j \u003c 0 {\n\t\t\tpanic(\"ibcgov: a step is routerClient/localClient, not \" + st)\n\t\t}\n\t\tto, local = st[:j], st[j+1:]\n\t\tif seenRouter[to] || seenLocal[local] {\n\t\t\tpanic(\"ibcgov: step \" + strconv.Itoa(i+1) + \" repeats a client of the path: \" + st)\n\t\t}\n\t\tseenRouter[to], seenLocal[local] = true, true\n\t\tfound := false\n\t\tfor _, x := range expectedOf(prev) {\n\t\t\tif x.active \u0026\u0026 x.to == to \u0026\u0026 x.local == local {\n\t\t\t\tfound = true\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\tpanic(\"ibcgov: step \" + strconv.Itoa(i+1) + \", \" + prev + \" -\u003e \" + to + \" over \" + local + \", is no expected transition\")\n\t\t}\n\t\tif i \u003c len(steps)-1 {\n\t\t\tpassed = append(passed, local)\n\t\t}\n\t\tprev = to\n\t}\n\tif to != source || local != client {\n\t\tpanic(\"ibcgov: the path ends at \" + to + \" over \" + local + \", not the notice's \" + source + \" over \" + client)\n\t}\n\tproven.Remove(provenKey(e, client))\n\tswitchEpoch(cur, e, client, passed)\n\tchain.Emit(\"GovNotice\", \"client\", client, \"epoch\", strconv.FormatUint(uint64(e), 10), \"status\", \"applied\")\n}\n\n// retireForGov retires the gno client c for governance: for good if a route\n// replacement retired it for transfers, otherwise for the gov port.\nfunc retireForGov(cur realm, c string) {\n\trd, _ := transfer.ExtView(\"controls\").(controls.Reader)\n\tswitch {\n\tcase core.IsClientRetired(c):\n\tcase rd.Valid() \u0026\u0026 rd.RetiredRouteOf(c) != \"\":\n\t\tcore.RetireClient(cross(cur), c)\n\tcase !core.IsClientRetiredFor(c, GovPort):\n\t\tcore.RetireClientPort(cross(cur), c, GovPort)\n\t}\n}\n\n// ownDispatch is the dispatch a packet's payload carries, if it is ours.\nfunc ownDispatch(payload types.Payload) *dispatch {\n\te, seq, _, _, ok := ParseEnvelope(payload.Value)\n\tif !ok {\n\t\treturn nil\n\t}\n\td := dispatchOf(e, seq)\n\tif d == nil || !bytes.Equal(d.body, payload.Value) {\n\t\treturn nil\n\t}\n\treturn d\n}\n\n// OnAcknowledgementPacket reads IBCAdmin's answer. It never fails: an\n// acknowledgement this realm cannot use leaves the decision re-dispatchable,\n// and re-sending identical bytes is always safe.\nfunc (a *App) OnAcknowledgementPacket(cur realm, sourceClient, destinationClient string, sequence uint64, acknowledgement []byte, payload types.Payload) error {\n\tassertCoreCaller(cur)\n\td := ownDispatch(payload)\n\tif d == nil {\n\t\treturn nil\n\t}\n\tif d.transport == sequence {\n\t\td.inFlight = false\n\t}\n\tcurrent := d.epoch == epoch\n\tif bytes.Equal(acknowledgement, types.UniversalErrorAcknowledgement()) {\n\t\td.redispatch = current\n\t\td.event = \"error acknowledgement\"\n\t\tif !current {\n\t\t\td.event += \" of a past epoch\"\n\t\t}\n\t\tchain.Emit(\"GovAck\", \"epoch\", strconv.FormatUint(uint64(d.epoch), 10), \"seq\", u64s(d.seq), \"status\", \"error\")\n\t\treturn nil\n\t}\n\te, seq, status, redispatch, ok := ParseAck(acknowledgement)\n\tif !ok || e != d.epoch || seq != d.seq {\n\t\td.redispatch = current\n\t\td.event = \"malformed acknowledgement\"\n\t\tchain.Emit(\"GovAck\", \"epoch\", strconv.FormatUint(uint64(d.epoch), 10), \"seq\", u64s(d.seq), \"status\", \"malformed\")\n\t\treturn nil\n\t}\n\t// A duplicate answering a re-send of a decision already acknowledged\n\t// otherwise changes nothing: the earlier status stands.\n\tif status == StatusDuplicate \u0026\u0026 d.status != 0 \u0026\u0026 d.status != StatusDuplicate {\n\t\td.redispatch = current \u0026\u0026 redispatch\n\t\td.event = \"acknowledged: duplicate of a decision \" + statusWord(d.status)\n\t\tchain.Emit(\"GovAck\", \"epoch\", strconv.FormatUint(uint64(d.epoch), 10), \"seq\", u64s(d.seq), \"status\", statusWord(status))\n\t\treturn nil\n\t}\n\td.status = status\n\t// redispatch = 1 means: send the same bytes again, whatever the status\n\t// (a cancel beyond IBCAdmin's window is acknowledged buffered with it).\n\td.redispatch = current \u0026\u0026 redispatch\n\td.event = \"acknowledged: \" + statusWord(d.status)\n\tif redispatch {\n\t\td.event += \", redispatch\"\n\t}\n\tchain.Emit(\"GovAck\", \"epoch\", strconv.FormatUint(uint64(d.epoch), 10), \"seq\", u64s(d.seq), \"status\", statusWord(d.status))\n\treturn nil\n}\n\n// OnTimeoutPacket makes a decision of the current epoch re-dispatchable.\nfunc (a *App) OnTimeoutPacket(cur realm, sourceClient, destinationClient string, sequence uint64, payload types.Payload) error {\n\tassertCoreCaller(cur)\n\td := ownDispatch(payload)\n\tif d == nil {\n\t\treturn nil\n\t}\n\tif d.transport == sequence {\n\t\td.inFlight = false\n\t}\n\td.redispatch = d.epoch == epoch\n\td.event = \"timed out\"\n\tchain.Emit(\"GovTimeout\", \"epoch\", strconv.FormatUint(uint64(d.epoch), 10), \"seq\", u64s(d.seq))\n\treturn nil\n}\n\n// ParseAck reads IBCAdmin's app acknowledgement\n// epoch u32 ‖ seq u64 ‖ status u8 ‖ redispatch u8 (big-endian, 14 bytes);\n// ok is false for any other length, a status outside 1–5 or a redispatch byte\n// other than 0 or 1.\nfunc ParseAck(b []byte) (e uint32, seq uint64, status uint8, redispatch bool, ok bool) {\n\tif len(b) != ackLen || b[12] \u003c StatusRecorded || b[12] \u003e StatusSpent || b[13] \u003e 1 {\n\t\treturn 0, 0, 0, false, false\n\t}\n\treturn binary.BigEndian.Uint32(b[0:4]), binary.BigEndian.Uint64(b[4:12]), b[12], b[13] == 1, true\n}\n\nfunc statusWord(s uint8) string {\n\tswitch s {\n\tcase StatusRecorded:\n\t\treturn \"recorded\"\n\tcase StatusBuffered:\n\t\treturn \"buffered\"\n\tcase StatusDuplicate:\n\t\treturn \"duplicate\"\n\tcase StatusConflict:\n\t\treturn \"conflict\"\n\tcase StatusSpent:\n\t\treturn \"spent\"\n\t}\n\treturn \"none\"\n}\n\n// --------------------------------------------------- Ethereum-bound kinds\n\n// ethParams is an Ethereum-bound decision: dispatched at once, its delay\n// runs in IBCAdmin (ADR pr6375 §6.6).\ntype ethParams struct {\n\tkind   uint8\n\tparams []byte\n\tepoch  uint32 // a cancel: the epoch whose numbers it names (0: any)\n\t// a kind 10: the transition it makes Ethereum expect, recorded here\n\t// when it is dispatched (router client ids), and the local gno client\n\t// approved to carry the epoch over the new one\n\tfrom, to, local string\n}\n\nfunc (p *ethParams) apply(cur realm, t int64) (string, string) {\n\tif p.epoch != 0 \u0026\u0026 p.epoch != epoch {\n\t\treturn \"\", \"the cancel names numbers of epoch \" + strconv.FormatUint(uint64(p.epoch), 10) + \", which is past\"\n\t}\n\tif p.to != \"\" {\n\t\tif refusal := checkExpect(p.from, p.to, p.local); refusal != \"\" {\n\t\t\treturn \"\", refusal\n\t\t}\n\t}\n\tseq, refusal := bind(cur, ledger.Last(), p.kind, p.params)\n\tif refusal != \"\" {\n\t\treturn \"\", refusal\n\t}\n\tnote := \"dispatched as epoch \" + strconv.FormatUint(uint64(epoch), 10) + \" seq \" + u64s(seq)\n\tif p.to != \"\" {\n\t\t// The packet goes at once; approving the local client to carry the\n\t\t// epoch loosens, so the expectation is a decision of its own that\n\t\t// waits 7 days and that a hold defers, as for an emergency recovery\n\t\t// (NewExpectRecoveryRequest). A notice over it before then is\n\t\t// refused, and IBCAdmin sends it again.\n\t\ttext := \"expect the governance epoch to move from \" + p.from + \" to \" + p.to + \", carried here by \" + p.local + \" (kind 10)\"\n\t\tep := \u0026expectParams{from: p.from, to: p.to, local: p.local}\n\t\td := decide(cur, \"expect transition\", text, ep, t, t, true)\n\t\tep.owner = d.ID\n\t\tep.gen = intend(p.from, p.to, p.local, d.ID)\n\t\tnote += \"; its transition is expected from decision \" + itoa(d.ID)\n\t}\n\treturn note, \"\"\n}\n\nfunc ethRequest(cur realm, name string, kind uint8, text string, params []byte) Request {\n\treturn ethRequestIn(cur, name, kind, text, params, 0)\n}\n\nfunc ethRequestIn(cur realm, name string, kind uint8, text string, params []byte, bound uint32) Request {\n\treturn ethRequestP(cur, name, text, \u0026ethParams{kind: kind, params: params, epoch: bound})\n}\n\nfunc ethRequestP(cur realm, name, text string, p *ethParams) Request {\n\tkind, params := p.kind, p.params\n\tif epochRecOf(epoch) == nil {\n\t\tpanic(\"ibcgov: the governance channel is not bound\")\n\t}\n\tif envelopeHeader+len(params) \u003e MaxEnvelope {\n\t\tpanic(\"ibcgov: the envelope would be \" + strconv.Itoa(envelopeHeader+len(params)) + \" bytes, above \" + strconv.Itoa(MaxEnvelope))\n\t}\n\ttext = \"ethereum kind \" + strconv.Itoa(int(kind)) + \" (\" + name + \"): \" + text\n\treturn propose(cur, \"eth \"+name, text, func(cur realm, made, t int64) {\n\t\tdecide(cur, \"eth \"+name, text, p, made, t, false)\n\t})\n}\n\nfunc mustEthAddr(s string) [20]byte {\n\ta, err := ethaddr.Parse(s)\n\tif err != nil {\n\t\tpanic(\"ibcgov: \" + err.Error())\n\t}\n\treturn [20]byte(a)\n}\n\n// mustEthClientID is a client id of the Ethereum router: one it generated\n// (\"client-N\") or a custom id (types.IsValidRouterClientID), not an SDK id.\nfunc mustEthClientID(s string) string {\n\tif !types.IsValidRouterClientID(s) {\n\t\tpanic(\"ibcgov: not an Ethereum router client id: \" + s)\n\t}\n\treturn s\n}\n\n// mustCustomEthClientID is a router client id that governance adds (kind 6\n// op 1, and kind 10's new client): eureka's router takes only a valid custom\n// id there (IBCIdentifiers.validateCustomIBCIdentifier: 4 to 128 characters\n// of its alphabet, not starting with \"client-\" or \"channel-\"), so any other\n// decision would only be spent on Ethereum.\nfunc mustCustomEthClientID(s string) string {\n\tif !types.IsValidCustomClientID(s) {\n\t\tpanic(\"ibcgov: a router client id added by governance is a custom id: 4 to 128 characters of a-z A-Z 0-9 . _ + - # [ ] \u003c \u003e, not starting with client- or channel-: \" + s)\n\t}\n\treturn s\n}\n\nfunc mustHash(s string) [32]byte {\n\tb, err := hex.DecodeString(strings.TrimPrefix(s, \"0x\"))\n\tif err != nil || len(b) != 32 {\n\t\tpanic(\"ibcgov: not a 32-byte hash in hex: \" + s)\n\t}\n\tvar h [32]byte\n\tcopy(h[:], b)\n\treturn h\n}\n\nfunc mustHexBytes(s string) []byte {\n\tb, err := hex.DecodeString(strings.TrimPrefix(s, \"0x\"))\n\tif err != nil {\n\t\tpanic(\"ibcgov: not hex: \" + s)\n\t}\n\treturn b\n}\n\nfunc ethAddrText(a [20]byte) string { return ethaddr.Address(a).String() }\n\n// NewEthCancelRequest (kind 1) cancels Ethereum-bound decisions of the\n// current epoch: ranges is a comma-separated list of \"from-to\" (inclusive,\n// at most 14, what the 1024-byte envelope holds), each within the numbers\n// already dispatched. IBCAdmin applies\n// it at recording, whatever its own seq. It is spent if a route replacement\n// opens another epoch before it is recorded.\nfunc NewEthCancelRequest(cur realm, ranges string) Request {\n\titems := splitList(ranges)\n\tif len(items) == 0 || len(items) \u003e maxCancelRanges {\n\t\tpanic(\"ibcgov: name 1 to \" + strconv.Itoa(maxCancelRanges) + \" ranges: no more fit the 1024-byte envelope\")\n\t}\n\tlast := LastSeq(epoch)\n\tvar vals []abi.Value\n\tfor _, it := range items {\n\t\ti := strings.Index(it, \"-\")\n\t\tfrom, to := it, it\n\t\tif i \u003e= 0 {\n\t\t\tfrom, to = it[:i], it[i+1:]\n\t\t}\n\t\tf, err1 := strconv.ParseUint(from, 10, 64)\n\t\tg, err2 := strconv.ParseUint(to, 10, 64)\n\t\tif err1 != nil || err2 != nil || f \u003c 1 || g \u003c f || g \u003e last {\n\t\t\tpanic(\"ibcgov: not a range of dispatched numbers (1-\" + u64s(last) + \"): \" + it)\n\t\t}\n\t\tvals = append(vals, abi.Tuple(abi.Uint64(f), abi.Uint64(g)))\n\t}\n\treturn ethRequestIn(cur, \"cancel\", KindCancel, \"cancel \"+strings.Join(items, \",\")+\" of epoch \"+strconv.FormatUint(uint64(epoch), 10),\n\t\tabi.Encode(abi.Array(vals...)), epoch)\n}\n\n// NewEthUnpauseRequest (kind 2) unpauses an object on Ethereum after 24 hours\n// there: object 1 is the transfer route of the router client clientID, object\n// 2 IBCAdmin itself (clientID empty).\nfunc NewEthUnpauseRequest(cur realm, object int, clientID string) Request {\n\tswitch object {\n\tcase 1:\n\t\tmustEthClientID(clientID)\n\tcase 2:\n\t\tif clientID != \"\" {\n\t\t\tpanic(\"ibcgov: object 2 (IBCAdmin) names no client\")\n\t\t}\n\tdefault:\n\t\tpanic(\"ibcgov: object 1 (a transfer route) or 2 (IBCAdmin)\")\n\t}\n\treturn ethRequest(cur, \"unpause\", KindUnpause, \"unpause object \"+strconv.Itoa(object)+\" \"+clientID,\n\t\tabi.Encode(abi.Uint64(uint64(object)), abi.String(clientID)))\n}\n\n// NewEthSeatsRequest (kind 3) sets IBCAdmin's seats: changes is a\n// comma-separated list of \"seat:0xaddress\" (seats 0–3 guardians, 4–5 watch\n// keys; the zero address vacates), at most two guardian seats.\nfunc NewEthSeatsRequest(cur realm, changes string) Request {\n\titems := splitList(changes)\n\tif len(items) == 0 || len(items) \u003e maxEthSeatsPerCall {\n\t\tpanic(\"ibcgov: name 1 to 6 seat changes\")\n\t}\n\tseen := map[int]bool{}\n\tguardians := 0\n\tvar vals []abi.Value\n\tvar parts []string\n\tfor _, it := range items {\n\t\ti := strings.Index(it, \":\")\n\t\tif i \u003c 0 {\n\t\t\tpanic(\"ibcgov: a seat change is seat:address\")\n\t\t}\n\t\tn, err := strconv.Atoi(it[:i])\n\t\tif err != nil || n \u003c 0 || n \u003e= SeatCount || seen[n] {\n\t\t\tpanic(\"ibcgov: not a seat, or named twice: \" + it[:i])\n\t\t}\n\t\tseen[n] = true\n\t\tif n \u003c GuardianSeats {\n\t\t\tguardians++\n\t\t}\n\t\ta := mustEthAddr(strings.TrimSpace(it[i+1:]))\n\t\tvals = append(vals, abi.Tuple(abi.Uint64(uint64(n)), abi.Address(a)))\n\t\tparts = append(parts, strconv.Itoa(n)+\"=\"+ethAddrText(a))\n\t}\n\tif guardians \u003e maxEthGuardianSeats {\n\t\tpanic(\"ibcgov: a decision changes at most two guardian seats\")\n\t}\n\treturn ethRequest(cur, \"seats\", KindSeats, \"set seats \"+strings.Join(parts, \",\"), abi.Encode(abi.Array(vals...)))\n}\n\n// NewEthLimitsRequest (kind 4) sets a token's limits on the route of the\n// router client clientID: per-message maximum, day cap, refund cap, and for\n// Ethereum-native tokens the send maximum and minimum (0 unset), in base\n// units (the uint256 of the kind's tuple). IBCAdmin applies a pure lowering\n// at recording, anything else after 7 days.\nfunc NewEthLimitsRequest(cur realm, clientID, token string, maxPerMessage, dayCap, refundCap, sendMax, sendMin uint256) Request {\n\tmustEthClientID(clientID)\n\ttok := mustEthAddr(token)\n\tvals := []abi.Value{abi.String(clientID), abi.Address(tok),\n\t\tabi.Uint256Of(maxPerMessage), abi.Uint256Of(dayCap), abi.Uint256Of(refundCap), abi.Uint256Of(sendMax), abi.Uint256Of(sendMin)}\n\ttext := \"limits of \" + ethAddrText(tok) + \" on \" + clientID + \": maxPerMessage=\" + u256s(maxPerMessage) + \" dayCap=\" + u256s(dayCap) +\n\t\t\" refundCap=\" + u256s(refundCap) + \" sendMax=\" + u256s(sendMax) + \" sendMin=\" + u256s(sendMin)\n\treturn ethRequest(cur, \"limits\", KindLimits, text, abi.Encode(vals...))\n}\n\n// NewEthVoucherRequest (kind 5) registers the GnoVoucher of a gno denom on\n// the route of clientID (setCustomERC20), pinned to its code hash. baseDenom\n// is the denom as the transfer app puts it on the wire (isGnoWireDenom): a\n// native denom, or a GRC20 alias; a realm-issued coin (\"/gno.land/r/...:x\")\n// never travels and is refused.\nfunc NewEthVoucherRequest(cur realm, clientID, baseDenom, voucher, codeHash string) Request {\n\tmustEthClientID(clientID)\n\tif !isGnoWireDenom(baseDenom) {\n\t\tpanic(\"ibcgov: not a gno.land denom the transfer app sends: \" + baseDenom)\n\t}\n\tv, h := mustEthAddr(voucher), mustHash(codeHash)\n\ttext := \"voucher of \" + baseDenom + \" on \" + clientID + \": \" + ethAddrText(v) + \" code hash \" + hex.EncodeToString(h[:])\n\treturn ethRequest(cur, \"voucher\", KindVoucher, text, abi.Encode(abi.String(clientID), abi.String(baseDenom), abi.Address(v), abi.Bytes32(h)))\n}\n\n// NewEthClientRequest (kind 6) adds (op 1) or migrates (op 2) a router light\n// client: clientID is the router's client id, counterpartyClientID the gno\n// client it tracks (op 1), merklePrefix a comma-separated list of hex items,\n// lightClient the contract and codeHash its code hash.\nfunc NewEthClientRequest(cur realm, op int, clientID, counterpartyClientID, merklePrefix, lightClient, codeHash string) Request {\n\tif op != 1 \u0026\u0026 op != 2 {\n\t\tpanic(\"ibcgov: op 1 (add) or 2 (migrate)\")\n\t}\n\tif op == 1 {\n\t\tmustCustomEthClientID(clientID)\n\t\tif core.ClientType(counterpartyClientID) == \"\" {\n\t\t\tpanic(\"ibcgov: the counterparty \" + counterpartyClientID + \" is not a gno client\")\n\t\t}\n\t} else {\n\t\tmustEthClientID(clientID)\n\t\tif counterpartyClientID != \"\" {\n\t\t\tmustEthClientID(counterpartyClientID)\n\t\t}\n\t}\n\tvar prefix []abi.Value\n\tvar prefixText []string\n\tfor _, item := range strings.Split(merklePrefix, \",\") {\n\t\tb := mustHexBytes(strings.TrimSpace(item))\n\t\tprefix = append(prefix, abi.Bytes(b))\n\t\tprefixText = append(prefixText, hex.EncodeToString(b))\n\t}\n\tlc, h := mustEthAddr(lightClient), mustHash(codeHash)\n\ttext := \"client op \" + strconv.Itoa(op) + \" \" + clientID + \" counterparty \" + counterpartyClientID + \" prefix [\" +\n\t\tstrings.Join(prefixText, \",\") + \"] light client \" + ethAddrText(lc) + \" code hash \" + hex.EncodeToString(h[:])\n\treturn ethRequest(cur, \"client\", KindClient, text, abi.Encode(abi.Uint64(uint64(op)), abi.String(clientID),\n\t\tabi.String(counterpartyClientID), abi.Array(prefix...), abi.Address(lc), abi.Bytes32(h)))\n}\n\n// NewEthUpgradeRequest (kind 7) upgrades a proxy to an implementation pinned\n// to its code hash, with an optional call (hex, may be empty).\nfunc NewEthUpgradeRequest(cur realm, proxy, implementation, codeHash, call string) Request {\n\tp, im, h := mustEthAddr(proxy), mustEthAddr(implementation), mustHash(codeHash)\n\tc := []byte{}\n\tif call != \"\" {\n\t\tc = mustHexBytes(call)\n\t}\n\ttext := \"upgrade \" + ethAddrText(p) + \" to \" + ethAddrText(im) + \" code hash \" + hex.EncodeToString(h[:]) + \" call \" + hex.EncodeToString(c)\n\treturn ethRequest(cur, \"upgrade\", KindUpgrade, text, abi.Encode(abi.Address(p), abi.Address(im), abi.Bytes32(h), abi.Bytes(c)))\n}\n\n// NewEthRoleRequest (kind 8) grants or revokes a non-admin AccessManager role\n// (the relayer allow-list). ADMIN_ROLE (0) and PUBLIC_ROLE are refused.\nfunc NewEthRoleRequest(cur realm, grant bool, roleID uint64, account string) Request {\n\tif roleID == adminRoleID || roleID == publicRoleID {\n\t\tpanic(\"ibcgov: kind 8 never names ADMIN_ROLE or PUBLIC_ROLE\")\n\t}\n\ta := mustEthAddr(account)\n\tverb := \"revoke\"\n\tif grant {\n\t\tverb = \"grant\"\n\t}\n\treturn ethRequest(cur, \"role\", KindRole, verb+\" role \"+u64s(roleID)+\" to \"+ethAddrText(a),\n\t\tabi.Encode(abi.Bool(grant), abi.Uint64(roleID), abi.Address(a)))\n}\n\n// NewEthHandOverRequest (kind 9) swaps IBCAdmin's executor to exec, pinned to\n// its code hash. It only moves the executor pin: any migration is the new\n// executor's own, done afterwards through its own decisions. The migration\n// call field stays on the wire, always empty: Ethereum spends a kind 9 that\n// carries one at recording.\nfunc NewEthHandOverRequest(cur realm, exec, codeHash string) Request {\n\te, h := mustEthAddr(exec), mustHash(codeHash)\n\ttext := \"hand over to \" + ethAddrText(e) + \" code hash \" + hex.EncodeToString(h[:])\n\treturn ethRequest(cur, \"hand-over\", KindHandOver, text, abi.Encode(abi.Address(e), abi.Bytes32(h), abi.Bytes([]byte{})))\n}\n\n// NewEthRouteReplacementRequest (kind 10) retires and freezes the router\n// client oldClientID, migrates its route to newClientID (a custom id added by\n// kind 6 first) and opens IBCAdmin's next epoch over it. suspectFrom is the\n// Ethereum block from which acknowledgements and timeouts processed through\n// the old client are suspect (ADR pr6375 §5.4, §7.12): every send record\n// processed at or after it must be settled by kind 12 before kind 13 reopens\n// the route. localClientID is the gno.land Ethereum client approved to carry\n// the governance epoch over newClientID once IBCAdmin's epoch notice\n// arrives (Active, not retired, its counterparty newClientID, authenticating\n// the governance route's identity, checked again when it is dispatched):\n// the notice is accepted over that client only.\nfunc NewEthRouteReplacementRequest(cur realm, oldClientID, newClientID string, suspectFrom uint64, localClientID string) Request {\n\tmustEthClientID(oldClientID)\n\tmustCustomEthClientID(newClientID)\n\tif oldClientID == newClientID {\n\t\tpanic(\"ibcgov: the clients differ\")\n\t}\n\tif refusal := checkExpect(oldClientID, newClientID, localClientID); refusal != \"\" {\n\t\tpanic(\"ibcgov: \" + refusal)\n\t}\n\treturn ethRequestP(cur, \"route replacement\", \"replace route \"+oldClientID+\" by \"+newClientID+\" (carried here by \"+localClientID+\"), suspect from Ethereum block \"+u64s(suspectFrom),\n\t\t\u0026ethParams{kind: KindRouteReplace, params: abi.Encode(abi.String(oldClientID), abi.String(newClientID), abi.Uint64(suspectFrom)), from: oldClientID, to: newClientID, local: localClientID})\n}\n\n// expectParams records an expected governance transition.\ntype expectParams struct {\n\tfrom, to, local string\n\towner           int64  // this expectation's decision, an owner of its intent\n\tgen             uint64 // the intent's generation it owns\n}\n\nfunc (p *expectParams) apply(cur realm, t int64) (string, string) {\n\t// It applies only to its own intent, still pending: an epoch switch\n\t// that left the transition behind dropped it, and an approval of it is\n\t// then stale.\n\tif !ownsIntent(p.from, p.to, p.local, p.gen, p.owner) {\n\t\tdisown(p.from, p.to, p.local, p.gen, p.owner)\n\t\treturn \"\", \"its intent is gone (an epoch switch left the transition behind, or it was released)\"\n\t}\n\tif refusal := checkExpect(p.from, p.to, p.local); refusal != \"\" {\n\t\trelease(p.from, p.to, p.local, p.gen, p.owner)\n\t\treturn \"\", refusal\n\t}\n\tdisown(p.from, p.to, p.local, p.gen, p.owner)\n\texpect(p.from, p.to, p.local)\n\treturn \"expect the governance epoch to move from \" + p.from + \" to \" + p.to + \" over \" + p.local, \"\"\n}\n\n// removeParams removes an active expected transition.\ntype removeParams struct{ from, to, local string }\n\nfunc (p *removeParams) apply(cur realm, t int64) (string, string) {\n\tif refusal := checkRemove(p.from, p.to, p.local); refusal != \"\" {\n\t\treturn \"\", refusal\n\t}\n\tentomb(p.from, p.to, p.local) // in its place: its client stays retained\n\tvar kept []expEdge\n\tfor _, x := range expectedOf(p.from) {\n\t\tif !(x.active \u0026\u0026 x.to == p.to \u0026\u0026 x.local == p.local) {\n\t\t\tkept = append(kept, x)\n\t\t}\n\t}\n\tsetEdges(p.from, kept)\n\tliveEdges--\n\tchain.Emit(\"GovUnexpect\", \"from\", p.from, \"to\", p.to, \"client\", p.local)\n\treturn \"no longer expect the governance epoch to move from \" + p.from + \" to \" + p.to + \" over \" + p.local, \"\"\n}\n\n// checkRemove is why the active transition from → to over local cannot be\n// removed, or \"\": it must be active (a pending one goes with its\n// expectations, cancelled), not lead to the epoch's own client, and lie on\n// no path a notice recorded as proven may still be applied along (see\n// pinnedBy: a notice its client no longer vouches for, or with no path of\n// the length it needs through the transition, pins nothing). It needs no\n// room: its tombstone takes its place in the joint budget.\nfunc checkRemove(from, to, local string) string {\n\tfound := false\n\tfor _, x := range expectedOf(from) {\n\t\tif x.active \u0026\u0026 x.to == to \u0026\u0026 x.local == local {\n\t\t\tfound = true\n\t\t}\n\t}\n\tif !found {\n\t\treturn \"no active expected transition from \" + from + \" to \" + to + \" over \" + local\n\t}\n\tif local == EpochClient() {\n\t\treturn local + \" carries the governance epoch\"\n\t}\n\tif c := pinnedBy(from, to, local, false); c != \"\" {\n\t\treturn \"a notice proven over \" + c + \" may need the transition: apply it or let it lapse first\"\n\t}\n\treturn \"\"\n}\n\n// retransmitParams authorizes one re-send of a conflicted dispatch.\ntype retransmitParams struct {\n\te   uint32\n\tseq uint64\n}\n\n// checkRetransmit is why dispatch (e, seq) cannot be authorized for a\n// re-send after a conflict, or \"\".\nfunc checkRetransmit(e uint32, seq uint64) string {\n\td := dispatchOf(e, seq)\n\tswitch {\n\tcase d == nil:\n\t\treturn \"no dispatched decision \" + strconv.FormatUint(uint64(e), 10) + \"/\" + u64s(seq)\n\tcase e != epoch:\n\t\treturn \"epoch \" + strconv.FormatUint(uint64(e), 10) + \" is past: its decisions are never sent again\"\n\tcase d.status != StatusConflict || d.redispatch:\n\t\treturn \"decision \" + u64s(seq) + \" was not answered with a conflict that bars a re-send (\" + d.event + \")\"\n\tcase d.retransmit:\n\t\treturn \"decision \" + u64s(seq) + \" is authorized for a re-send already\"\n\t}\n\treturn \"\"\n}\n\nfunc (p *retransmitParams) apply(cur realm, t int64) (string, string) {\n\tif refusal := checkRetransmit(p.e, p.seq); refusal != \"\" {\n\t\treturn \"\", refusal\n\t}\n\td := dispatchOf(p.e, p.seq)\n\td.retransmit = true\n\td.event += \"; one re-send authorized\"\n\treturn \"authorize one re-send of epoch \" + strconv.FormatUint(uint64(p.e), 10) + \" seq \" + u64s(p.seq) + \": anyone calls Redispatch\", \"\"\n}\n\n// NewRetransmitRequest authorizes, at once, one re-send of dispatch (e,\n// seq) of the current epoch that IBCAdmin answered with a conflict and no\n// redispatch, whose re-send Redispatch otherwise refuses (ADR 0004: a\n// deliberate addition to ADR pr6375 §5.4). Governance decides it after\n// checking on Ethereum that the conflict was fabricated: IBCAdmin holds no\n// decision at that seq, or holds these very bytes, and emitted no Conflict\n// for it. It is no loosening: the bytes are immutable and IBCAdmin\n// deduplicates them, so the re-send records the decision if it was never\n// delivered and is answered duplicate if it was. It is used up by one\n// Redispatch; another needs another decision.\nfunc NewRetransmitRequest(cur realm, e uint32, seq uint64) Request {\n\tif refusal := checkRetransmit(e, seq); refusal != \"\" {\n\t\tpanic(\"ibcgov: \" + refusal)\n\t}\n\ttext := \"authorize one re-send of the conflicted governance dispatch, epoch \" + strconv.FormatUint(uint64(e), 10) + \" seq \" + u64s(seq)\n\treturn propose(cur, \"retransmit\", text, func(cur realm, made, t int64) {\n\t\tdecide(cur, \"retransmit\", text, \u0026retransmitParams{e: e, seq: seq}, made, t, false)\n\t})\n}\n\n// purgeParams drops a tombstone.\ntype purgeParams struct{ from, to, local string }\n\n// checkPurge is why the tombstone of the transition from → to over local\n// cannot be purged, or \"\": it must exist, and lie on no path a notice\n// recorded as proven may still be applied along once the transitions on it\n// are approved again (pinnedBy through live and tombstoned transitions).\nfunc checkPurge(from, to, local string) string {\n\tif !tombstones.Has(tombKey(from, to, local)) {\n\t\treturn \"no tombstone of the transition from \" + from + \" to \" + to + \" over \" + local\n\t}\n\tif c := pinnedBy(from, to, local, true); c != \"\" {\n\t\treturn \"a notice proven over \" + c + \" may need the transition: approve it again and apply the notice, or let it lapse first\"\n\t}\n\treturn \"\"\n}\n\nfunc (p *purgeParams) apply(cur realm, t int64) (string, string) {\n\tif refusal := checkPurge(p.from, p.to, p.local); refusal != \"\" {\n\t\treturn \"\", refusal\n\t}\n\t// A purge only frees capacity: it never makes a client retirable. The\n\t// clients it disconnects (no longer reachable, though no switch passed\n\t// them) leave awaitingGov, so no prune retires them: they stay as\n\t// clients kept off the graph, their gov port open and inert, retired for\n\t// governance only by a switch leaving them as its old epoch client. Bounded: two walks of at\n\t// most maxLive edges.\n\t_, before := retained()\n\ttombstones.Remove(tombKey(p.from, p.to, p.local))\n\t_, after := retained()\n\tfor c := range before {\n\t\tif !after[c] \u0026\u0026 awaitingGov.Has(c) {\n\t\t\tawaitingGov.Remove(c)\n\t\t}\n\t}\n\tchain.Emit(\"GovPurge\", \"from\", p.from, \"to\", p.to, \"client\", p.local)\n\treturn \"purged the tombstone of the transition from \" + p.from + \" to \" + p.to + \" over \" + p.local, \"\"\n}\n\n// NewPurgeTombstoneRequest drops, after 7 days (deferred by a hold), the\n// tombstone a removed or released transition from the router client\n// oldClientID to newClientID over the gno client localClientID left (ADR\n// 0004: a deliberate addition), freeing its place in the joint budget of\n// maxLive without an epoch switch. It frees capacity only: the local\n// client, and those of the transitions beyond it, are no longer held by the\n// graph, but none becomes retirable by it: they are kept off the graph,\n// their gov port open and inert, until a switch passes them; if Ethereum did\n// execute the transition, it must be expected again for its notice to apply.\n// Refused, when built and when it applies, while a notice recorded as\n// proven may still be applied along a path through it of the length it\n// needs, its transitions approved again.\nfunc NewPurgeTombstoneRequest(cur realm, oldClientID, newClientID, localClientID string) Request {\n\tif refusal := checkPurge(oldClientID, newClientID, localClientID); refusal != \"\" {\n\t\tpanic(\"ibcgov: \" + refusal)\n\t}\n\ttext := \"purge the tombstone of the transition from \" + oldClientID + \" to \" + newClientID + \" over \" + localClientID\n\treturn propose(cur, \"purge tombstone\", text, func(cur realm, made, t int64) {\n\t\tdecide(cur, \"purge tombstone\", text, \u0026purgeParams{from: oldClientID, to: newClientID, local: localClientID}, made, t, true)\n\t})\n}\n\n// NewRemoveExpectationRequest removes, at once, the active expected\n// transition from the router client oldClientID to newClientID over the gno\n// client localClientID (ADR 0004: a deliberate addition to ADR pr6375\n// §7.12). It tightens: it narrows what a notice may authorize, and frees its\n// place among the old client's successors and the graph's 64, so\n// governance can drop a transition Ethereum cancelled or never executed. A\n// transition Ethereum did execute costs only liveness if removed: it is\n// expected again through the 7-day expectation. Refused for the transition\n// to the epoch's own client, and while a notice recorded as proven may still\n// be applied along a path through it of exactly the steps the notice needs:\n// a notice whose client is frozen, expired, recovered, converted or retired,\n// whose epoch the current one reached, or with no such path through the\n// transition, pins nothing.\nfunc NewRemoveExpectationRequest(cur realm, oldClientID, newClientID, localClientID string) Request {\n\tif refusal := checkRemove(oldClientID, newClientID, localClientID); refusal != \"\" {\n\t\tpanic(\"ibcgov: \" + refusal)\n\t}\n\ttext := \"remove the expected transition from \" + oldClientID + \" to \" + newClientID + \" over \" + localClientID\n\treturn propose(cur, \"remove expectation\", text, func(cur realm, made, t int64) {\n\t\tdecide(cur, \"remove expectation\", text, \u0026removeParams{from: oldClientID, to: newClientID, local: localClientID}, made, t, false)\n\t})\n}\n\n// NewExpectRecoveryRequest records, after 7 days, that Ethereum's emergency\n// recovery is to move the governance epoch from the router client\n// oldClientID to newClientID (ADR pr6375 §7.12): ibcgov dispatches no\n// emergency recovery, so this decision is what lets the recovery's epoch\n// notice be accepted, over localClientID only (checked as for a kind 10,\n// and again when it executes). Approving a local verifier loosens, so it\n// waits 7 days and a guardian hold defers it, within Ethereum's own 30-day\n// recovery delay.\n// An expectation never confirmed by a notice costs nothing; a router client\n// has at most 4 expected successors.\nfunc NewExpectRecoveryRequest(cur realm, oldClientID, newClientID, localClientID string) Request {\n\tmustEthClientID(oldClientID)\n\tmustCustomEthClientID(newClientID)\n\tif oldClientID == newClientID {\n\t\tpanic(\"ibcgov: the clients differ\")\n\t}\n\tif refusal := checkExpect(oldClientID, newClientID, localClientID); refusal != \"\" {\n\t\tpanic(\"ibcgov: \" + refusal)\n\t}\n\ttext := \"expect the emergency recovery of \" + oldClientID + \" by \" + newClientID + \", carried here by \" + localClientID\n\treturn propose(cur, \"expect recovery\", text, func(cur realm, made, t int64) {\n\t\t// Fresh parameters for every recording: a Request governed twice\n\t\t// makes two decisions, each owning its own place in the intent.\n\t\tp := \u0026expectParams{from: oldClientID, to: newClientID, local: localClientID}\n\t\t// An intent from the recording on: it holds the successor's place\n\t\t// and keeps its clients open for governance.\n\t\t// Refused when recorded (the graph changed since the request was\n\t\t// built): a spent decision, no intent, so an approved proposal is\n\t\t// used up rather than left to execute again once room frees.\n\t\tif refusal := checkExpect(oldClientID, newClientID, localClientID); refusal != \"\" {\n\t\t\tspendAtRecording(\"expect recovery\", text, p, made, t, refusal)\n\t\t\treturn\n\t\t}\n\t\td := decide(cur, \"expect recovery\", text, p, made, t, true)\n\t\tp.owner = d.ID\n\t\tp.gen = intend(oldClientID, newClientID, localClientID, d.ID)\n\t})\n}\n\n// NewEthRouteSettingsRequest (kind 11) sets the quarantine of the route of\n// clientID after a validator-set transition, in seconds (0 disables it).\nfunc NewEthRouteSettingsRequest(cur realm, clientID string, quarantineSeconds uint64) Request {\n\tmustEthClientID(clientID)\n\treturn ethRequest(cur, \"route settings\", KindRouteSettings, \"quarantine of \"+clientID+\": \"+u64s(quarantineSeconds)+\" s\",\n\t\tabi.Encode(abi.String(clientID), abi.Uint64(quarantineSeconds)))\n}\n\n// NewEthSettleRequest (kind 12) applies a reconciliation's outcomes to the\n// send records of clientId, the router client a route replacement retired\n// (ADR pr6375 §5.4, §7.12): items is a comma-separated list of\n// \"sequence:action:amount\" (amount in base units as decimal digits, below\n// 2^256), at most 8, each sequence once:\n//\n//   - 1 a genuine refund: pay its remainder, amount exactly the entitlement\n//     minus what was already refunded (a refund is never cut short), 0 for one\n//     paid in full already, which only marks the record settled;\n//   - 2 reject a queued refund none of which was paid, amount 0;\n//   - 3 settled as delivered, no refund paid, amount 0;\n//   - 4 a forged refund: the unpaid remainder is rejected and what was\n//     already paid is recorded as a loss, atomically; amount is exactly what\n//     was paid;\n//   - 5 a forged receive: sequence is an inbound sequence on the retired\n//     client, amount what its receive released or minted, booked as a loss\n//     once.\n//\n// IBCAdmin applies it after 7 days; each send record is settled at most once.\nfunc NewEthSettleRequest(cur realm, clientID, items string) Request {\n\tmustEthClientID(clientID)\n\tlist := splitList(items)\n\tif len(list) == 0 || len(list) \u003e maxSettleItems {\n\t\tpanic(\"ibcgov: name 1 to \" + strconv.Itoa(maxSettleItems) + \" settlement items\")\n\t}\n\t// A sequence is named once per direction: action 5 names an inbound\n\t// sequence, the others an outbound one, separate spaces.\n\tseen := map[string]bool{}\n\tvar vals []abi.Value\n\tfor _, it := range list {\n\t\tparts := strings.Split(it, \":\")\n\t\tif len(parts) != 3 {\n\t\t\tpanic(\"ibcgov: a settlement item is sequence:action:amount, not \" + it)\n\t\t}\n\t\tseq, err1 := strconv.ParseUint(parts[0], 10, 64)\n\t\taction, err2 := strconv.ParseUint(parts[1], 10, 8)\n\t\tif err1 != nil || err2 != nil || seq == 0 || seen[settleItemKey(seq, action)] || action \u003c 1 || action \u003e 5 {\n\t\t\tpanic(\"ibcgov: not a settlement item, or a sequence named twice in its direction: \" + it)\n\t\t}\n\t\tamount, err := strconv.ParseUint256(parts[2], 10)\n\t\tif err != nil {\n\t\t\tpanic(\"ibcgov: amount: \" + err.Error())\n\t\t}\n\t\tif ((action == 4 || action == 5) \u0026\u0026 amount == 0) || ((action == 2 || action == 3) \u0026\u0026 amount != 0) {\n\t\t\tpanic(\"ibcgov: a forged refund or receive names its positive amount, a rejection or a delivery 0: \" + it)\n\t\t}\n\t\tseen[settleItemKey(seq, action)] = true\n\t\tvals = append(vals, abi.Tuple(abi.Uint64(seq), abi.Uint64(action), abi.Uint256Of(amount)))\n\t}\n\treturn ethRequest(cur, \"settle\", KindSettle, \"settle sends of \"+clientID+\": \"+strings.Join(list, \",\"),\n\t\tabi.Encode(abi.String(clientID), abi.Array(vals...)))\n}\n\n// NewEthReopenRequest (kind 13) reopens the route of clientID that a route\n// replacement or the emergency recovery closed (ADR pr6375 §5.4). IBCAdmin\n// applies it after 7 days; Ethereum refuses it while any send record of the\n// route's retired client ids is open, and records every loss not\n// recapitalized as an accepted deficit.\nfunc NewEthReopenRequest(cur realm, clientID string) Request {\n\tmustEthClientID(clientID)\n\treturn ethRequest(cur, \"reopen\", KindReopen, \"reopen the route of \"+clientID, abi.Encode(abi.String(clientID)))\n}\n\n// isGnoWireDenom is the grammar of a gno.land denom as the transfer app puts\n// it on the wire, which IBCAdmin's kind 5 must accept exactly\n// (canonicalGnoDenom), at most 128 bytes:\n//   - a native denom: [a-z][a-z0-9]{2,15} (e.g. ugnot);\n//   - a GRC20 alias, the grc20reg key \"\u003crealm path\u003e[.\u003cslug\u003e]\" with \"/\" as\n//     \":\": \"gno.land:r\" then at least two \":\"-separated path segments, each a\n//     gno package name ([a-z][a-z0-9]*, with \"_\" or \"-\" only between\n//     alphanumerics: \"my-org\", not \"my--org\" or \"org-\"), then optionally \".\"\n//     and a slug of 1 to 32 characters of [A-Za-z0-9_-].\n//\n// A realm-issued coin (\"/gno.land/r/...:denom\") is no wire denom: the\n// transfer app looks it up as a grc20reg key and sends it only as a GRC20.\nfunc isGnoWireDenom(s string) bool {\n\tif len(s) == 0 || len(s) \u003e 128 {\n\t\treturn false\n\t}\n\tconst head = \"gno.land:r\"\n\tif !strings.HasPrefix(s, head+\":\") {\n\t\tif len(s) \u003c 3 || len(s) \u003e 16 || !lowerByte(s[0]) {\n\t\t\treturn false\n\t\t}\n\t\tfor i := 1; i \u003c len(s); i++ {\n\t\t\tif !lowerByte(s[i]) \u0026\u0026 !digitByte(s[i]) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\treturn true\n\t}\n\trest := s[len(head):]\n\tslug := \"\"\n\tif i := strings.IndexByte(rest, '.'); i \u003e= 0 {\n\t\trest, slug = rest[:i], rest[i+1:]\n\t\tif len(slug) == 0 || len(slug) \u003e 32 {\n\t\t\treturn false\n\t\t}\n\t\tfor i := 0; i \u003c len(slug); i++ {\n\t\t\tc := slug[i]\n\t\t\tif !lowerByte(c) \u0026\u0026 !digitByte(c) \u0026\u0026 !(c \u003e= 'A' \u0026\u0026 c \u003c= 'Z') \u0026\u0026 c != '_' \u0026\u0026 c != '-' {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\tsegs := strings.Split(rest[1:], \":\")\n\tif len(segs) \u003c 2 {\n\t\treturn false\n\t}\n\tfor _, seg := range segs {\n\t\tif !isPkgName(seg) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// isPkgName is gno's package path segment grammar (gnolang Re_name):\n// [a-z][a-z0-9]*([_-][a-z0-9]+)*.\nfunc isPkgName(seg string) bool {\n\tif len(seg) == 0 || !lowerByte(seg[0]) {\n\t\treturn false\n\t}\n\tfor i := 1; i \u003c len(seg); i++ {\n\t\tc := seg[i]\n\t\tswitch {\n\t\tcase lowerByte(c) || digitByte(c):\n\t\tcase c == '_' || c == '-':\n\t\t\tif i+1 \u003e= len(seg) || !(lowerByte(seg[i+1]) || digitByte(seg[i+1])) {\n\t\t\t\treturn false\n\t\t\t}\n\t\tdefault:\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc lowerByte(c byte) bool { return c \u003e= 'a' \u0026\u0026 c \u003c= 'z' }\nfunc digitByte(c byte) bool { return c \u003e= '0' \u0026\u0026 c \u003c= '9' }\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/aib/ibc/ibcgov\"\ngno = \"0.9\"\n"},{"name":"governor.gno","body":"package ibcgov\n\nimport (\n\t\"chain\"\n\n\t\"gno.land/p/aib/decisions/v0\"\n\t\"gno.land/r/gov/dao\"\n)\n\n// Request is what a builder returns: the proposal GovDAO's proxy executes\n// after the yield (Proposal), and the same decision the governor runs\n// directly before it (Govern). The closure is one; only who may run it\n// changes with the yield. A Request comes from a builder alone: its fields are\n// unexported, so no other code can build one or change what it decides, and a\n// stranger holding one can only hand it back to Govern, which refuses\n// everyone but the governor.\ntype Request struct {\n\tproposal dao.ProposalRequest\n\ttext     string\n\tdecide   func(cur realm) error\n}\n\n// Proposal is the request as a GovDAO proposal:\n// dao.MustCreateProposal(cross(cur), req.Proposal()).\nfunc (r Request) Proposal() dao.ProposalRequest { return r.proposal }\n\n// Description is the proposal's description: the parameters as the builder\n// wrote them.\nfunc (r Request) Description() string { return r.text }\n\n// mustBeGovernor is a decision's identity check, read from the closure's own\n// live cur: until the yield the governor's own transaction (a user realm with\n// the governor's address: a MsgRun's ephemeral realm, never another realm),\n// which is Govern's non-crossing call of the closure, with GovDAO's proposals\n// refused; after it, or when no governor was compiled, the GovDAO proxy alone.\n// A refusal panics, so a proposal made before the yield survives for a retry\n// after it, within its 14 days.\nfunc mustBeGovernor(cur realm) {\n\tprev := cur.Previous()\n\tif decisions.DAOGoverns(LaunchGovernor, yielded) {\n\t\tif prev.PkgPath() != decisions.DAOPath {\n\t\t\tpanic(\"ibcgov: only \" + decisions.DAOPath + \" decides, not \" + who(0, prev))\n\t\t}\n\t\treturn\n\t}\n\tif !prev.IsUser() || prev.Address() != LaunchGovernor {\n\t\tpanic(\"ibcgov: until the governor yields, \" + string(LaunchGovernor) + \" alone decides, by Govern from its own key: not \" + who(0, prev) + \" (a GovDAO proposal is refused until the yield)\")\n\t}\n}\n\n// propose wraps a builder's effect in the closure every builder shares: the\n// identity check, the 14-day deadline (an expired proposal returns an error,\n// which GovDAO takes as a denial for good) and the effect, which records the\n// decision. made is when the builder ran.\nfunc propose(cur realm, kind, text string, effect func(cur realm, made, t int64)) Request {\n\tmade := now()\n\tcb := func(cur realm) error {\n\t\tmustBeGovernor(cur)\n\t\tt := now()\n\t\tif err := decisions.Expired(made, t); err != nil {\n\t\t\treturn err\n\t\t}\n\t\teffect(cur, made, t)\n\t\treturn nil\n\t}\n\treturn Request{\n\t\tproposal: dao.NewProposalRequest(titlePrefix+kind, text, dao.NewSimpleExecutor(0, cur, cb, text)),\n\t\ttext:     text,\n\t\tdecide:   cb,\n\t}\n}\n\n// Govern is the governor's way in before the yield: the governor, by its own\n// transaction (the MsgRun that builds the request), makes a builder's decision\n// directly, through the same closure GovDAO's proxy runs after the yield: the\n// same checks, at-once test, 7-day queue and record. Refused for anyone else,\n// and for everyone once the governor yielded or when none was compiled.\nfunc Govern(cur realm, r Request) {\n\tif r.decide == nil {\n\t\tpanic(\"ibcgov: not a builder's request\")\n\t}\n\tif decisions.DAOGoverns(LaunchGovernor, yielded) {\n\t\tpanic(\"ibcgov: \" + governedBy() + \": a decision is a GovDAO proposal, never Govern\")\n\t}\n\tmustBeGovernor(cur)\n\tif err := r.decide(cur); err != nil {\n\t\tpanic(\"ibcgov: \" + err.Error())\n\t}\n}\n\nfunc governedBy() string {\n\tif LaunchGovernor == \"\" {\n\t\treturn \"no governor was compiled, \" + decisions.DAOPath + \" governs from the first block\"\n\t}\n\treturn \"the governor yielded at height \" + itoa(yielded) + \", \" + decisions.DAOPath + \" governs since\"\n}\n\n// Yield is the governor's one-way hand-over to GovDAO (ADR pr6375 §7.7): from\n// this block GovDAO's proposals are the only way a decision is made, and\n// Govern is closed for good. The governor alone, by its own transaction. A\n// decision the governor queued still applies by Execute after its delay, or a\n// cancel decision withdraws it.\nfunc Yield(cur realm) {\n\tif LaunchGovernor == \"\" {\n\t\tpanic(\"ibcgov: no governor was compiled: \" + decisions.DAOPath + \" governs from the first block\")\n\t}\n\tif yielded != 0 {\n\t\tpanic(\"ibcgov: the governor yielded at height \" + itoa(yielded) + \" already\")\n\t}\n\tprev := cur.Previous()\n\tif !prev.IsUser() || prev.Address() != LaunchGovernor {\n\t\tpanic(\"ibcgov: only the governor \" + string(LaunchGovernor) + \" yields, not \" + who(0, prev))\n\t}\n\tyielded = chainHeight()\n\tchain.Emit(\"Yielded\", \"governor\", string(LaunchGovernor), \"height\", itoa(yielded))\n}\n\n// Governor is the compiled governor key, and Yielded the height of its yield\n// (0 before).\nfunc Governor() address { return LaunchGovernor }\nfunc Yielded() int64    { return yielded }\n\n// GovDAOGoverns reports whether GovDAO's proposals decide now.\nfunc GovDAOGoverns() bool { return decisions.DAOGoverns(LaunchGovernor, yielded) }\n"},{"name":"guardians.gno","body":"package ibcgov\n\nimport (\n\t\"chain\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/aib/decisions/v0\"\n\t\"gno.land/p/aib/ibc/ics20/controls/v0\"\n\t\"gno.land/p/aib/ibc/lightclient/ethereum/v0\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/r/aib/ibc/apps/transfer\"\n\t\"gno.land/r/aib/ibc/core\"\n)\n\n// The guardians on gno.land (ADR pr6375 §7.4, R10): six seats, 0–3 the\n// guardians (2 and 3 the outsiders') and 4–5 the operator's watch keys, each\n// holding one gno address. A seat flags a function of a route, keyed by the\n// route key: sends (gno-native sends), credits (receives from Ethereum),\n// payouts (claim payouts) or refunds (the refunds hold of §7.13, which\n// delays error-ack and timeout refunds and the payout of queued refunds). One\n// flag pauses the function at once. Flags never stop voucher returns (burns),\n// acknowledgements or timeouts; only the refunds flag delays refunds, never\n// cancels them. A seat clears its own flag; an unpause decision clears every\n// seat's flags of a function. Clearing a payouts flag starts the route's\n// meters empty (a new payouts epoch).\n//\n// Two guardian seats flagging (anything) make a hold: every queued decision is\n// deferred and every unpause decision is spent. A hold ends 24 hours after\n// fewer than two guardian seats flag. A seat change, a vacancy included,\n// waits 7 days and lands through a hold no sooner than day 21 of it. A guardian's payouts flag may name the\n// packets it suspects, which makes their void immediate. Guardians delay;\n// they never veto.\n\nconst (\n\tSeatCount     = 6\n\tGuardianSeats = 4 // seats 0–3\n\n\tFnSends   = \"sends\"\n\tFnCredits = \"credits\"\n\tFnPayouts = \"payouts\"\n\tFnRefunds = \"refunds\"\n\n\t// holdLinger is how long a hold outlives the second flag's clearing.\n\tholdLinger = decisions.Day / decisions.Factor\n\t// holdReplacementWait is how long into a hold a guardian replacement\n\t// waits before it can land (day 21).\n\tholdReplacementWait = 21 * decisions.Day / decisions.Factor\n)\n\nvar (\n\tseats [SeatCount]address\n\t// flags holds the standing flags, keyed route|fn|seat.\n\tflags avl.Tree // flagKey -\u003e *flag\n\t// routes are the routes ibcgov enrolled, keyed by route key.\n\troutes avl.Tree // route key -\u003e *routeRec\n\n\tholdSince  int64 // when the current hold began, 0 if none\n\tbelowSince int64 // when it fell below two guardian seats, 0 while it has two\n\n\t// A seat's generation grows when its key leaves (removal or\n\t// replacement): a flag counts only while its generation is the seat's,\n\t// so a departing key's flags stop counting in constant work, however\n\t// many there are. seatFlags counts each seat's live flags, kept as flags\n\t// are set and cleared, which the hold reads in constant work. The stale\n\t// entries are removed lazily: by a later flag at the same key, an\n\t// unpause, a fold, or PruneFlags, which anyone may call.\n\tseatGen    [SeatCount]uint64\n\tseatFlags  [SeatCount]int64\n\tpruneAfter string // where PruneFlags resumes\n\t// genEnds records when each ended seat generation ended, kept while\n\t// entries of its flags remain: a flag reads its own generation's end, so\n\t// a later generation of the seat ending does not move an earlier one's.\n\t// A record goes with the last entry of its generation, so there is at\n\t// most one per stale entry.\n\tgenEnds avl.Tree // seat|gen -\u003e *genEnd\n)\n\n// genEnd is when a seat generation ended and how many of its flags'\n// entries remain.\ntype genEnd struct {\n\tat   int64\n\tleft int64\n}\n\nfunc genKey(seat int, gen uint64) string {\n\treturn strconv.Itoa(seat) + \"|\" + strconv.FormatUint(gen, 10)\n}\n\n// endedAt is when the generation of a flag that no longer counts ended.\nfunc (f *flag) endedAt() int64 {\n\treturn genEnds.Get(genKey(f.seat, f.gen)).(*genEnd).at\n}\n\n// dropStale removes the entry at k of a flag that no longer counts, and its\n// generation's record with the last of its entries.\nfunc dropStale(k string, f *flag) {\n\tflags.Remove(k)\n\tgk := genKey(f.seat, f.gen)\n\te := genEnds.Get(gk).(*genEnd)\n\tif e.left--; e.left == 0 {\n\t\tgenEnds.Remove(gk)\n\t}\n}\n\ntype flag struct {\n\tseat int\n\tgen  uint64 // the seat's generation when it was set\n\tat   int64\n\tkeys []string // a guardian's payouts flag: the packets it suspects\n}\n\n// live reports whether f still counts: its seat's key has not left since.\nfunc (f *flag) live() bool { return f.gen == seatGen[f.seat] }\n\n// routeRec is a route's record here: its payouts epoch, which the transfer\n// implementation reads to empty its meters when a payouts flag is cleared,\n// and its identity, recorded at enrolment and never changed: the history\n// every client of the route must authenticate (core.ClientRouteIdentity).\ntype routeRec struct {\n\tepoch    int64\n\tepochAt  int64\n\tidentity string\n}\n\nfunc flagKey(route, fn string, seat int) string { return route + \"|\" + fn + \"|\" + strconv.Itoa(seat) }\nfunc flagPrefix(route, fn string) string        { return route + \"|\" + fn + \"|\" }\n\nfunc isFn(fn string) bool {\n\treturn fn == FnSends || fn == FnCredits || fn == FnPayouts || fn == FnRefunds\n}\n\n// Seat is the address in a seat, \"\" if empty or out of range.\nfunc Seat(n int) address {\n\tif n \u003c 0 || n \u003e= SeatCount {\n\t\treturn \"\"\n\t}\n\treturn seats[n]\n}\n\n// SeatOf is the seat an address holds, -1 if none.\nfunc SeatOf(a address) int {\n\tif a == \"\" {\n\t\treturn -1\n\t}\n\tfor i := 0; i \u003c SeatCount; i++ {\n\t\tif seats[i] == a {\n\t\t\treturn i\n\t\t}\n\t}\n\treturn -1\n}\n\n// IsGuardianSeat reports whether seat n is a guardian's (0–3).\nfunc IsGuardianSeat(n int) bool { return n \u003e= 0 \u0026\u0026 n \u003c GuardianSeats }\n\n// IsRoute reports whether ibcgov enrolled route.\nfunc IsRoute(route string) bool { return routes.Has(route) }\n\n// routeKeyOf is the stable key flags and route records are kept under for\n// id: the route's key when id is a route key or one of a route's clients,\n// current or retired (a replacement keeps the first client's id as the key),\n// and id itself otherwise (an Ethereum client no route uses yet). Every flag operation and check\n// resolves its route through it, so a flag on a client and the transfer app's\n// check on its route meet.\nfunc routeKeyOf(id string) string {\n\tif routes.Has(id) {\n\t\treturn id\n\t}\n\tif rd, ok := transfer.ExtView(\"controls\").(controls.Reader); ok {\n\t\tif k := rd.RouteOf(id); k != \"\" {\n\t\t\treturn k\n\t\t}\n\t\tif k := rd.RetiredRouteOf(id); k != \"\" {\n\t\t\treturn k\n\t\t}\n\t}\n\treturn id\n}\n\n// foldFlags moves the flags standing under from to the route key to, when\n// from becomes one of the route's clients (a replacement), and returns how\n// many it moved. A seat's flag of a function standing under both is kept\n// once, with the earlier time and the packets both name.\nfunc foldFlags(from, to string, t int64) int {\n\tif from == to {\n\t\treturn 0\n\t}\n\tvar keys []string\n\tflags.Iterate(from+\"|\", from+\"|~\", func(k string, _ any) bool {\n\t\tkeys = append(keys, k)\n\t\treturn false\n\t})\n\tfor _, k := range keys {\n\t\tf := flags.Get(k).(*flag)\n\t\tif !f.live() {\n\t\t\tpruneFlag(k, f) // its seat's key left: it stopped counting then\n\t\t\tcontinue\n\t\t}\n\t\tflags.Remove(k)\n\t\tfn := strings.Split(k, \"|\")[1]\n\t\tif w := flags.Get(flagKey(to, fn, f.seat)); w != nil \u0026\u0026 !w.(*flag).live() {\n\t\t\tpruneFlag(flagKey(to, fn, f.seat), w.(*flag))\n\t\t} else if w != nil {\n\t\t\tseatFlags[f.seat]-- // merged into the route's own flag\n\t\t\tg := w.(*flag)\n\t\t\tif f.at \u003c g.at {\n\t\t\t\tg.at = f.at\n\t\t\t}\n\t\t\tfor _, key := range f.keys {\n\t\t\t\tif !contains(g.keys, key) \u0026\u0026 len(g.keys) \u003c controls.MaxKeys {\n\t\t\t\t\tg.keys = append(g.keys, key)\n\t\t\t\t}\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tflags.Set(flagKey(to, fn, f.seat), f)\n\t}\n\tif len(keys) \u003e 0 {\n\t\tchain.Emit(\"FlagsMoved\", \"from\", from, \"to\", to, \"flags\", strconv.Itoa(len(keys)))\n\t\tupdateHold(t)\n\t}\n\treturn len(keys)\n}\n\n// flaggable reports whether a flag may stand on route: a route ibcgov\n// enrolled, or any Ethereum client, so a client about to become a route's\n// (by an enrolment, or as a replacement candidate) can be held beforehand:\n// its flags stay with its route.\nfunc flaggable(route string) bool {\n\treturn routes.Has(route) || core.ClientType(route) == ethereum.ClientType\n}\n\n// Flagged reports whether any seat's flag for fn stands on route: what the\n// transfer implementation asks before sending, crediting, paying out or\n// refunding.\nfunc Flagged(route, fn string) bool {\n\troute = routeKeyOf(route)\n\tfound := false\n\tflags.Iterate(flagPrefix(route, fn), flagPrefix(route, fn)+\"~\", func(_ string, v any) bool {\n\t\tfound = v.(*flag).live()\n\t\treturn found\n\t})\n\treturn found\n}\n\n// PayoutsEpoch is how many times a standing payouts flag on route was\n// cleared, and when the last was: the route's meters empty when it changes.\nfunc PayoutsEpoch(route string) (epoch, at int64) {\n\troute = routeKeyOf(route)\n\tv := routes.Get(route)\n\tif v == nil {\n\t\treturn 0, 0\n\t}\n\tr := v.(*routeRec)\n\tepoch, at = r.epoch, r.epochAt\n\t// A payouts flag whose seat's key left was cleared then, though its\n\t// entry is not removed yet: it counts as that clearing (pruneFlag books\n\t// it the same way when it removes the entry).\n\tflags.Iterate(flagPrefix(route, FnPayouts), flagPrefix(route, FnPayouts)+\"~\", func(_ string, v any) bool {\n\t\tif f := v.(*flag); !f.live() {\n\t\t\tepoch++\n\t\t\tif e := f.endedAt(); e \u003e at {\n\t\t\t\tat = e\n\t\t\t}\n\t\t}\n\t\treturn false\n\t})\n\treturn epoch, at\n}\n\n// NamedSuspect reports whether a guardian's standing payouts flag on route\n// names the packet key: what makes its void immediate.\nfunc NamedSuspect(route, key string) bool {\n\troute = routeKeyOf(route)\n\tfound := false\n\tflags.Iterate(flagPrefix(route, FnPayouts), flagPrefix(route, FnPayouts)+\"~\", func(_ string, v any) bool {\n\t\tf := v.(*flag)\n\t\tif !IsGuardianSeat(f.seat) || !f.live() {\n\t\t\treturn false\n\t\t}\n\t\tfor _, k := range f.keys {\n\t\t\tif k == key {\n\t\t\t\tfound = true\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\t})\n\treturn found\n}\n\n// Held reports whether a guardian hold stands now.\nfunc Held() bool { return heldAt(now()) }\n\n// HoldSince is when the current hold began, 0 if none stands.\nfunc HoldSince() int64 {\n\tif !heldAt(now()) {\n\t\treturn 0\n\t}\n\treturn holdSince\n}\n\nfunc heldAt(t int64) bool {\n\tif holdSince == 0 {\n\t\treturn false\n\t}\n\treturn belowSince == 0 || t \u003c belowSince+holdLinger\n}\n\n// guardiansFlagging counts the guardian seats with a live flag, in constant\n// work.\nfunc guardiansFlagging() int {\n\tn := 0\n\tfor s := 0; s \u003c GuardianSeats; s++ {\n\t\tif seatFlags[s] \u003e 0 {\n\t\t\tn++\n\t\t}\n\t}\n\treturn n\n}\n\n// updateHold re-reads the hold after a flag change at t.\nfunc updateHold(t int64) {\n\tif holdSince != 0 \u0026\u0026 belowSince != 0 \u0026\u0026 t \u003e= belowSince+holdLinger {\n\t\tholdSince, belowSince = 0, 0 // the last hold ended\n\t}\n\tif guardiansFlagging() \u003e= 2 {\n\t\tif holdSince == 0 {\n\t\t\tholdSince = t\n\t\t\tchain.Emit(\"Hold\", \"since\", itoa(t))\n\t\t}\n\t\tbelowSince = 0\n\t\treturn\n\t}\n\tif holdSince != 0 \u0026\u0026 belowSince == 0 {\n\t\tbelowSince = t\n\t\tchain.Emit(\"HoldEnding\", \"at\", itoa(t+holdLinger))\n\t}\n}\n\n// parseSuspects reads the packet keys a payouts flag names.\nfunc parseSuspects(s string) []string {\n\tkeys := splitList(s)\n\tif len(keys) \u003e controls.MaxKeys {\n\t\tpanic(\"ibcgov: a flag names at most \" + strconv.Itoa(controls.MaxKeys) + \" packets\")\n\t}\n\tfor _, k := range keys {\n\t\tif _, _, ok := controls.ParsePacketKey(k); !ok {\n\t\t\tpanic(\"ibcgov: not a packet key: \" + k)\n\t\t}\n\t}\n\treturn keys\n}\n\n// SetFlag sets, or re-asserts, the caller's flag on fn of route. keys names\n// the packets a guardian's payouts flag suspects (comma-separated packet\n// keys, \"client/sequence\" with 20 digits), \"\" otherwise; a re-assertion\n// replaces them. The caller is cur.Previous(): the seat's key itself.\nfunc SetFlag(cur realm, route, fn, keys string) {\n\tseat := SeatOf(cur.Previous().Address())\n\tif seat \u003c 0 {\n\t\tpanic(\"ibcgov: not a guardian or a watch key: \" + who(0, cur.Previous()))\n\t}\n\troute = routeKeyOf(route)\n\tif !flaggable(route) {\n\t\tpanic(\"ibcgov: no route or Ethereum client \" + route)\n\t}\n\tif !isFn(fn) {\n\t\tpanic(\"ibcgov: the function is sends, credits, payouts or refunds, not \" + fn)\n\t}\n\tlist := parseSuspects(keys)\n\tif len(list) \u003e 0 \u0026\u0026 (fn != FnPayouts || !IsGuardianSeat(seat)) {\n\t\tpanic(\"ibcgov: only a guardian's payouts flag names packets\")\n\t}\n\tt := now()\n\tk := flagKey(route, fn, seat)\n\tif v := flags.Get(k); v != nil \u0026\u0026 v.(*flag).live() {\n\t\tseatFlags[seat]-- // a re-assertion replaces the live flag\n\t} else if v != nil {\n\t\tpruneFlag(k, v.(*flag)) // a departed key's entry at the same place\n\t}\n\tflags.Set(k, \u0026flag{seat: seat, gen: seatGen[seat], at: t, keys: list})\n\tseatFlags[seat]++\n\tupdateHold(t)\n\tchain.Emit(\"Flag\", \"route\", route, \"fn\", fn, \"seat\", strconv.Itoa(seat), \"packets\", strconv.Itoa(len(list)))\n}\n\n// ClearFlag clears the caller's own flag on fn of route. A route with losses\n// that have no restoring decision stays closed in the transfer app whatever\n// the flags (ADR pr6375 §7.9), so clearing a flag, ending a hold, never\n// reopens it: the restoring decisions the hold would defer can then land.\nfunc ClearFlag(cur realm, route, fn string) {\n\tseat := SeatOf(cur.Previous().Address())\n\tif seat \u003c 0 {\n\t\tpanic(\"ibcgov: not a guardian or a watch key: \" + who(0, cur.Previous()))\n\t}\n\troute = routeKeyOf(route)\n\tif v := flags.Get(flagKey(route, fn, seat)); v == nil || !v.(*flag).live() {\n\t\tpanic(\"ibcgov: no such flag of yours\")\n\t}\n\tt := now()\n\tclearFlag(route, fn, seat, t)\n\tupdateHold(t)\n}\n\n// clearFlag clears a live flag.\nfunc clearFlag(route, fn string, seat int, t int64) {\n\tflags.Remove(flagKey(route, fn, seat))\n\tseatFlags[seat]--\n\tif fn == FnPayouts {\n\t\tbumpPayoutsEpoch(route, t)\n\t}\n\tchain.Emit(\"Unflag\", \"route\", route, \"fn\", fn, \"seat\", strconv.Itoa(seat))\n}\n\n// pruneFlag removes the entry of a flag whose seat's key left: it stopped\n// counting then; a payouts flag's clearing is booked as of then, as\n// PayoutsEpoch already reads it.\nfunc pruneFlag(k string, f *flag) {\n\tended := f.endedAt()\n\tdropStale(k, f)\n\tparts := strings.Split(k, \"|\")\n\tif parts[1] == FnPayouts {\n\t\tif v := routes.Get(parts[0]); v != nil {\n\t\t\tr := v.(*routeRec)\n\t\t\tr.epoch++\n\t\t\tif ended \u003e r.epochAt {\n\t\t\t\tr.epochAt = ended\n\t\t\t}\n\t\t}\n\t}\n}\n\n// PruneFlags removes up to n entries of flags whose seat's key left,\n// visiting at most n entries from where the last call stopped, and returns\n// how many it removed; anyone may call it. A departed key's flags stop\n// counting at once (seat generations); this only reclaims their storage.\nfunc PruneFlags(cur realm, n int) int {\n\tif n \u003c= 0 || n \u003e controls.MaxKeys {\n\t\tpanic(\"ibcgov: prune 1 to \" + strconv.Itoa(controls.MaxKeys) + \" entries a call\")\n\t}\n\tvar stale []string\n\tvisited := 0\n\tlast := \"\"\n\tflags.Iterate(after(pruneAfter), \"\", func(k string, v any) bool {\n\t\tif visited == n {\n\t\t\treturn true\n\t\t}\n\t\tvisited++\n\t\tlast = k\n\t\tif !v.(*flag).live() {\n\t\t\tstale = append(stale, k)\n\t\t}\n\t\treturn false\n\t})\n\tpruneAfter = last // \"\" once the walk reaches the end: the next starts over\n\tfor _, k := range stale {\n\t\tpruneFlag(k, flags.Get(k).(*flag))\n\t}\n\treturn len(stale)\n}\n\n// after is the first key an iteration resumes at, past cursor (\"\" from the\n// start).\nfunc after(cursor string) string {\n\tif cursor == \"\" {\n\t\treturn \"\"\n\t}\n\treturn cursor + \"\\x00\"\n}\n\nfunc bumpPayoutsEpoch(route string, t int64) {\n\tif v := routes.Get(route); v != nil {\n\t\tr := v.(*routeRec)\n\t\tr.epoch++\n\t\tr.epochAt = t\n\t}\n}\n\n// clearFns clears every seat's flags of fns on route and returns how many.\nfunc clearFns(route string, fns []string, t int64) int {\n\tn := 0\n\tfor _, fn := range fns {\n\t\tvar live []int\n\t\tstale := map[string]*flag{}\n\t\tflags.Iterate(flagPrefix(route, fn), flagPrefix(route, fn)+\"~\", func(k string, v any) bool {\n\t\t\tif f := v.(*flag); f.live() {\n\t\t\t\tlive = append(live, f.seat)\n\t\t\t} else {\n\t\t\t\tstale[k] = f\n\t\t\t}\n\t\t\treturn false\n\t\t})\n\t\tfor k, f := range stale {\n\t\t\tpruneFlag(k, f)\n\t\t}\n\t\tfor _, s := range live {\n\t\t\tclearFlag(route, fn, s, t)\n\t\t\tn++\n\t\t}\n\t}\n\tupdateHold(t)\n\treturn n\n}\n\n// clearSeat ends every flag of a seat (its key leaves), in constant work:\n// the seat's generation grows, so its flags stop counting; their entries\n// are pruned lazily.\nfunc clearSeat(seat int, t int64) {\n\tif seatFlags[seat] \u003e 0 {\n\t\tgenEnds.Set(genKey(seat, seatGen[seat]), \u0026genEnd{at: t, left: seatFlags[seat]})\n\t}\n\tseatGen[seat]++\n\tseatFlags[seat] = 0\n\tchain.Emit(\"SeatFlagsEnded\", \"seat\", strconv.Itoa(seat))\n\tupdateHold(t)\n}\n\n// Flags lists the standing flags of a route as \"fn seat \u003cn\u003e at \u003ctime\u003e\" lines,\n// with the packets a payouts flag names.\nfunc Flags(route string) []string {\n\troute = routeKeyOf(route)\n\tvar out []string\n\tflags.Iterate(route+\"|\", route+\"|~\", func(k string, v any) bool {\n\t\tf := v.(*flag)\n\t\tif !f.live() {\n\t\t\treturn false\n\t\t}\n\t\tline := strings.Split(k, \"|\")[1] + \" seat \" + strconv.Itoa(f.seat) + \" at \" + itoa(f.at)\n\t\tif len(f.keys) \u003e 0 {\n\t\t\tline += \" naming \" + strings.Join(f.keys, \",\")\n\t\t}\n\t\tout = append(out, line)\n\t\treturn false\n\t})\n\treturn out\n}\n\n// ------------------------------------------------------------------ seats\n\n// seatsParams is a seat change: each seat in order gets its address (\"\" to\n// vacate). held records the seats' holders when the decision was made: a\n// removal of a seat whose occupant changed since is skipped.\ntype seatsParams struct {\n\t// holdAnchor is the start of the hold that first deferred this decision\n\t// once it was due (0 before): it executes from holdAnchor +\n\t// holdReplacementWait whatever holds stand then, so a hold cleared and\n\t// set again never moves its deadline.\n\tholdAnchor int64\n\tseats      []int\n\taddrs      []address\n\theld       []address\n\theldGen    []uint64 // each seat's generation when the request was built\n}\n\n// holdDeadline implements holdBounded: a seat change lands from\n// holdReplacementWait into the hold that first deferred it once it was due.\nfunc (p *seatsParams) holdDeadline() (anchor, deadline int64, first bool) {\n\tfirst = p.holdAnchor == 0\n\tif first {\n\t\tp.holdAnchor = holdSince\n\t}\n\treturn p.holdAnchor, p.holdAnchor + holdReplacementWait, first\n}\n\nfunc (p *seatsParams) apply(cur realm, t int64) (string, string) {\n\t// Re-check against live state: no address may end up in two seats.\n\tnext := seats\n\tfor i, s := range p.seats {\n\t\t// A vacancy removes the very tenure it was requested against: the\n\t\t// same key in the same generation. A seat that changed hands since,\n\t\t// even back to that key (a new generation), is left alone.\n\t\tif p.addrs[i] == \"\" \u0026\u0026 (seats[s] != p.held[i] || seatGen[s] != p.heldGen[i]) {\n\t\t\tcontinue\n\t\t}\n\t\tnext[s] = p.addrs[i]\n\t}\n\tfor i := 0; i \u003c SeatCount; i++ {\n\t\tfor j := i + 1; j \u003c SeatCount; j++ {\n\t\t\tif next[i] != \"\" \u0026\u0026 next[i] == next[j] {\n\t\t\t\treturn \"\", \"address \" + string(next[i]) + \" would hold seats \" + strconv.Itoa(i) + \" and \" + strconv.Itoa(j)\n\t\t\t}\n\t\t}\n\t}\n\tvar changed []string\n\tfor i := 0; i \u003c SeatCount; i++ {\n\t\tif next[i] != seats[i] {\n\t\t\tclearSeat(i, t)\n\t\t\tseats[i] = next[i]\n\t\t\tchanged = append(changed, strconv.Itoa(i))\n\t\t\tchain.Emit(\"SeatSet\", \"seat\", strconv.Itoa(i), \"address\", string(next[i]))\n\t\t}\n\t}\n\tif len(changed) == 0 {\n\t\treturn \"\", \"nothing to change\"\n\t}\n\treturn \"seats \" + strings.Join(changed, \",\"), \"\"\n}\n\n// NewSetSeatsRequest changes guardian and watch-key seats: changes is a\n// comma-separated list of \"seat:address\" (seat 0–5; an empty address vacates\n// the seat). At most two guardian seats per decision; no address in two\n// seats. Every seat change, a vacancy included (ADR pr6375 §7.4), waits 7\n// days, and through a guardian hold no sooner than day 21 of it.\nfunc NewSetSeatsRequest(cur realm, changes string) Request {\n\titems := splitList(changes)\n\tif len(items) == 0 || len(items) \u003e SeatCount {\n\t\tpanic(\"ibcgov: name 1 to 6 seat changes\")\n\t}\n\tp := \u0026seatsParams{}\n\tseen := map[int]bool{}\n\tguardians := 0\n\tvar parts []string\n\tfor _, it := range items {\n\t\ti := strings.Index(it, \":\")\n\t\tif i \u003c 0 {\n\t\t\tpanic(\"ibcgov: a seat change is seat:address, not \" + it)\n\t\t}\n\t\tn, err := strconv.Atoi(it[:i])\n\t\tif err != nil || n \u003c 0 || n \u003e= SeatCount || seen[n] {\n\t\t\tpanic(\"ibcgov: not a seat, or named twice: \" + it[:i])\n\t\t}\n\t\tseen[n] = true\n\t\tvar a address\n\t\tif raw := strings.TrimSpace(it[i+1:]); raw != \"\" {\n\t\t\ta = mustAddr(raw)\n\t\t\tif seats[n] == a {\n\t\t\t\tpanic(\"ibcgov: seat \" + strconv.Itoa(n) + \" holds \" + string(a) + \" already\")\n\t\t\t}\n\t\t}\n\t\tif IsGuardianSeat(n) {\n\t\t\tguardians++\n\t\t}\n\t\tp.seats = append(p.seats, n)\n\t\tp.addrs = append(p.addrs, a)\n\t\tp.held = append(p.held, seats[n])\n\t\tp.heldGen = append(p.heldGen, seatGen[n])\n\t\tparts = append(parts, strconv.Itoa(n)+\"=\"+string(a))\n\t}\n\tif guardians \u003e 2 {\n\t\tpanic(\"ibcgov: a decision changes at most two guardian seats\")\n\t}\n\t// No address may end up in two seats (re-checked when it applies).\n\tnext := seats\n\tfor i, n := range p.seats {\n\t\tnext[n] = p.addrs[i]\n\t}\n\tfor i := 0; i \u003c SeatCount; i++ {\n\t\tfor j := i + 1; j \u003c SeatCount; j++ {\n\t\t\tif next[i] != \"\" \u0026\u0026 next[i] == next[j] {\n\t\t\t\tpanic(\"ibcgov: \" + string(next[i]) + \" would hold seats \" + strconv.Itoa(i) + \" and \" + strconv.Itoa(j))\n\t\t\t}\n\t\t}\n\t}\n\ttext := \"set seats \" + strings.Join(parts, \",\")\n\treturn propose(cur, \"set seats\", text, func(cur realm, made, t int64) {\n\t\t// Fresh parameters for every recording: each decision keeps its own\n\t\t// hold anchor, so a Request governed twice makes two decisions that\n\t\t// never share one.\n\t\tq := \u0026seatsParams{seats: p.seats, addrs: p.addrs, held: p.held, heldGen: p.heldGen}\n\t\tdecide(cur, \"set seats\", text, q, made, t, true)\n\t})\n}\n\n// ------------------------------------------------------------- unpausing\n\n// unpauseRouteParams clears every seat's flags of fns on a route.\ntype unpauseRouteParams struct {\n\troute string\n\tfns   []string\n}\n\nfunc (*unpauseRouteParams) unpause() {}\n\nfunc (p *unpauseRouteParams) apply(cur realm, t int64) (string, string) {\n\t// The client named may have become a route's alias since the decision.\n\tn := clearFns(routeKeyOf(p.route), p.fns, t)\n\tif n == 0 {\n\t\treturn \"\", \"no flag stood\"\n\t}\n\treturn \"cleared \" + strconv.Itoa(n) + \" flags\", \"\"\n}\n\n// NewUnpauseRouteRequest clears every seat's flags of fns (comma-separated:\n// sends, credits, payouts, refunds) on route, at once (ADR pr6375 §7.4: on\n// gno.land GovDAO unpauses immediately). Spent under a guardian hold. A route\n// with losses that have no restoring decision stays closed in the transfer\n// app all the same.\nfunc NewUnpauseRouteRequest(cur realm, route, fns string) Request {\n\troute = routeKeyOf(route)\n\tif !flaggable(route) {\n\t\tpanic(\"ibcgov: no route or Ethereum client \" + route)\n\t}\n\tlist := splitList(fns)\n\tif len(list) == 0 {\n\t\tpanic(\"ibcgov: name the functions to unpause\")\n\t}\n\tfor _, fn := range list {\n\t\tif !isFn(fn) {\n\t\t\tpanic(\"ibcgov: not a function: \" + fn)\n\t\t}\n\t}\n\ttext := \"unpause \" + strings.Join(list, \",\") + \" on route \" + route\n\tp := \u0026unpauseRouteParams{route: route, fns: list}\n\treturn propose(cur, \"unpause route\", text, func(cur realm, made, t int64) {\n\t\tdecide(cur, \"unpause route\", text, p, made, t, false)\n\t})\n}\n"},{"name":"ibcgov.gno","body":"// Package ibcgov is the governance realm of the IBC realms for the Ethereum\n// route (ADR pr6375 R9, R10, R11, R14, §5.4, §7.4–§7.7, §7.12):\n//\n//   - the decision ledger (gno.land/p/aib/decisions/v0): every change is a\n//     decision built by one of the New…Request builders, which take no free\n//     text (the parameters are the record), recorded with a number and either\n//     applied at once (tightening, removals, cancels, unpauses) or queued for\n//     7 days (every loosening change), then applied by anyone with Execute;\n//   - the governor: a compiled key decides through the same builders, checks\n//     and delays (Govern) until it calls Yield once, after which only GovDAO's\n//     proposals decide;\n//   - the guardians on gno.land: six seats (0–3 guardians, 4–5 watch keys)\n//     whose flags pause a route's sends, credits, payouts or refunds at once,\n//     and whose holds (two guardian seats flagging) defer every queued\n//     decision;\n//   - the sole holder of the loosening powers over the IBC core and the\n//     transfer app (core.IBCGovPath, transfer.IBCGovPath);\n//   - the sender of governance packets to IBCAdmin on Ethereum: the gov port,\n//     the §5.4 envelope, contiguous per-epoch sequences, one-time dispatch\n//     authorizations and re-dispatch of identical bytes.\n//\n// Builders are called in a MsgRun: before the yield the governor runs\n//\n//\tibcgov.Govern(cross(cur), ibcgov.NewCoreUpdateImplRequest(cross(cur), path))\n//\n// and after it a GovDAO member proposes\n//\n//\tdao.MustCreateProposal(cross(cur), ibcgov.NewCoreUpdateImplRequest(cross(cur), path).Proposal())\n//\n// The decision ledger of gnolang/bridges (solana/realm/p/decisions/v0) and\n// the governor, flag and dispatch patterns of its ethereum/realm/hlmailbox and\n// ethereum/realm/hlgov realms (Copyright (C) 2026 NewTendermint, LLC, GNO\n// Network General Public License) are the model for this realm.\npackage ibcgov\n\nimport (\n\t\"chain/runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/aib/decisions/v0\"\n\t\"gno.land/r/aib/ibc/core\"\n)\n\nconst (\n\t// PkgPath is this realm's path, which the IBC core and the transfer app\n\t// gate their loosening powers on.\n\tPkgPath      = \"gno.land/r/aib/ibc/ibcgov\"\n\tCorePath     = \"gno.land/r/aib/ibc/core\"\n\tTransferPath = \"gno.land/r/aib/ibc/apps/transfer\"\n\n\t// LaunchGovernor is the governor key (ADR pr6375 §7.7). PLACEHOLDER: a\n\t// test address (testutils.TestAddress(\"ibcgov-governor\")); the deployed\n\t// source carries the real key, which members read before the launch. \"\"\n\t// would mean GovDAO decides from the first block.\n\tLaunchGovernor = address(\"g1kg6w7a4f0kup8wj9y33jdkqvvux7n559avyzcs\")\n\n\ttitlePrefix = \"gno.land/r/aib/ibc/ibcgov: \"\n)\n\nvar (\n\t// yielded is the block height of the governor's yield, 0 before.\n\tyielded int64\n\t// ledger numbers every decision, gno-side and Ethereum-bound.\n\tledger decisions.Ledger\n)\n\nfunc init(cur realm) {\n\t// Register the gov port. On chain init runs once; the guard is for the gno\n\t// test harness, which may instantiate this realm twice.\n\tif core.AppPkgPath(GovPort) == PkgPath {\n\t\treturn\n\t}\n\tcore.RegisterApp(cross(cur), GovPort, \u0026App{})\n}\n\nfunc now() int64 { return time.Now().Unix() }\n\nfunc chainHeight() int64 {\n\th := runtime.ChainHeight()\n\tif h \u003c 1 {\n\t\th = 1\n\t}\n\treturn h\n}\n\nfunc itoa(n int64) string { return strconv.FormatInt(n, 10) }\n\nfunc u64s(n uint64) string { return strconv.FormatUint(n, 10) }\n\n// u256s is an amount in decimal digits (ufmt takes no uint256).\nfunc u256s(v uint256) string { return strconv.FormatUint256(v, 10) }\n\n// who names a previous realm in a refusal: its pkgpath, or a user's address.\nfunc who(_ int, r realm) string {\n\tif p := r.PkgPath(); p != \"\" {\n\t\treturn p\n\t}\n\treturn string(r.Address())\n}\n\n// isIdent reports whether s is a short identifier: 1 to max characters of\n// a-z, 0-9 and '-' (incidents, causes: builders take no free text).\nfunc isIdent(s string, max int) bool {\n\tif len(s) == 0 || len(s) \u003e max {\n\t\treturn false\n\t}\n\tfor i := 0; i \u003c len(s); i++ {\n\t\tc := s[i]\n\t\tif !(c \u003e= 'a' \u0026\u0026 c \u003c= 'z' || c \u003e= '0' \u0026\u0026 c \u003c= '9' || c == '-') {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// isDenom reports whether s may name a denom: 1 to 128 characters of letters,\n// digits and \"/:._-\".\nfunc isDenom(s string) bool {\n\tif len(s) == 0 || len(s) \u003e 128 {\n\t\treturn false\n\t}\n\tfor i := 0; i \u003c len(s); i++ {\n\t\tc := s[i]\n\t\tif !(c \u003e= 'a' \u0026\u0026 c \u003c= 'z' || c \u003e= 'A' \u0026\u0026 c \u003c= 'Z' || c \u003e= '0' \u0026\u0026 c \u003c= '9' || strings.IndexByte(\"/:._-\", c) \u003e= 0) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// splitList splits a comma-separated argument, trimming spaces and dropping\n// empty items.\nfunc splitList(s string) []string {\n\tvar out []string\n\tfor _, p := range strings.Split(s, \",\") {\n\t\tp = strings.TrimSpace(p)\n\t\tif p != \"\" {\n\t\t\tout = append(out, p)\n\t\t}\n\t}\n\treturn out\n}\n"},{"name":"ledger.gno","body":"package ibcgov\n\nimport (\n\t\"chain\"\n\n\t\"gno.land/p/aib/decisions/v0\"\n)\n\n// The decision lifecycle (ADR pr6375 §6.6, §7.5): a decision is recorded when\n// its closure runs. An at-once decision (tightening, removal, cancel, unpause\n// on gno.land) applies in the same transaction; a loosening one is queued\n// for decisions.Delay and anyone applies it with Execute once due. Parameters\n// are re-checked against live state when the decision applies: a refusal\n// then spends the decision (recorded, no effect, never retried). A guardian\n// hold defers every queued decision, except a guardian replacement after day\n// 21 of the hold, and spends every unpause.\n\n// unpauser is implemented by the parameters of an unpause decision, which a\n// guardian hold spends.\ntype unpauser interface{ unpause() }\n\n// deferrer is implemented by a decision whose execution a standing flag\n// defers (it stays pending): deferredBy is the reason, \"\" if none.\ntype deferrer interface{ deferredBy() string }\n\n// holdBounded is implemented by a decision a guardian hold defers only until\n// a deadline: holdDeadline anchors it at the standing hold on the first\n// deferral, and returns the anchor, the deadline and whether this call set\n// the anchor.\ntype holdBounded interface {\n\tholdDeadline() (anchor, deadline int64, first bool)\n}\n\n// applier is implemented by every gno-side decision's parameters: apply makes\n// the change and returns a note, or the reason it cannot apply now.\ntype applier interface {\n\tapply(cur realm, t int64) (note, refusal string)\n}\n\n// decide records a decision and applies it at once, or queues it for the\n// 7-day delay.\nfunc decide(cur realm, kind, text string, params applier, made, t int64, queued bool) *decisions.Decision {\n\tif queued {\n\t\td := ledger.Record(kind, text, params, made, t, t+decisions.Delay)\n\t\td.Note = \"queued 7 days\"\n\t\tchain.Emit(\"Decision\", \"id\", itoa(d.ID), \"kind\", kind, \"state\", \"queued\", \"due\", itoa(d.Due))\n\t\treturn d\n\t}\n\td := ledger.Record(kind, text, params, made, t, t)\n\tapplyDecision(cur, d, t)\n\treturn d\n}\n\n// spendAtRecording records a decision refused when it is recorded: spent\n// at once, its effect never applied, so the request that made it (an\n// approved proposal included) is used up, never retried later.\nfunc spendAtRecording(kind, text string, params applier, made, t int64, refusal string) *decisions.Decision {\n\td := ledger.Record(kind, text, params, made, t, t)\n\tledger.Spend(d, \"spent: \"+refusal)\n\tchain.Emit(\"Decision\", \"id\", itoa(d.ID), \"kind\", kind, \"state\", \"spent\")\n\treturn d\n}\n\nfunc applyDecision(cur realm, d *decisions.Decision, t int64) {\n\tp := d.Params.(applier)\n\tif _, ok := p.(unpauser); ok \u0026\u0026 heldAt(t) {\n\t\tledger.Spend(d, \"spent: a guardian hold stands, which spends every unpause\")\n\t\tchain.Emit(\"Decision\", \"id\", itoa(d.ID), \"kind\", d.Kind, \"state\", \"spent\")\n\t\treturn\n\t}\n\tnote, refusal := p.apply(cur, t)\n\tif refusal != \"\" {\n\t\tledger.Spend(d, \"spent: \"+refusal)\n\t\tchain.Emit(\"Decision\", \"id\", itoa(d.ID), \"kind\", d.Kind, \"state\", \"spent\")\n\t\treturn\n\t}\n\tledger.Apply(d, t, note)\n\tchain.Emit(\"Decision\", \"id\", itoa(d.ID), \"kind\", d.Kind, \"state\", \"applied\")\n}\n\n// Execute applies queued decision id once its delay has passed. Anyone may\n// call it. A guardian hold defers it (it stays pending), except a guardian\n// replacement from day 21 of the hold on.\nfunc Execute(cur realm, id int64) {\n\tt := now()\n\td, err := ledger.Due(id, t)\n\tif err != nil {\n\t\tpanic(\"ibcgov: \" + err.Error())\n\t}\n\tif heldAt(t) {\n\t\thb, ok := d.Params.(holdBounded)\n\t\tif !ok {\n\t\t\tpanic(\"ibcgov: decision \" + itoa(id) + \" is deferred: a guardian hold stands since \" + itoa(holdSince))\n\t\t}\n\t\t// A seat change (a guardian replacement or a vacancy) lands from day\n\t\t// 21 of the hold that first deferred it after it became due, its\n\t\t// anchor, whatever holds stand by then: a later hold, after one was\n\t\t// cleared, never resets that deadline (ADR pr6375 §7.4; Ethereum\n\t\t// anchors it the same way). The first deferral records the anchor\n\t\t// and returns, so the record stands.\n\t\tanchor, deadline, first := hb.holdDeadline()\n\t\tif t \u003c deadline {\n\t\t\tif first {\n\t\t\t\tchain.Emit(\"Decision\", \"id\", itoa(d.ID), \"kind\", d.Kind, \"state\", \"deferred\", \"until\", itoa(deadline))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tpanic(\"ibcgov: decision \" + itoa(id) + \" is deferred: a guardian hold first deferred it on \" + itoa(anchor) + \", until \" + itoa(deadline))\n\t\t}\n\t}\n\t// One flag stops a route's payouts, governed ones included: a decision\n\t// paying out on a flagged function waits (ADR pr6375 R10, §7.4).\n\tif df, ok := d.Params.(deferrer); ok {\n\t\tif reason := df.deferredBy(); reason != \"\" {\n\t\t\tpanic(\"ibcgov: decision \" + itoa(id) + \" is deferred: \" + reason)\n\t\t}\n\t}\n\tapplyDecision(cur, d, t)\n}\n\n// cancelParams withdraws a pending gno-side decision.\ntype cancelParams struct{ id int64 }\n\nfunc (p *cancelParams) apply(cur realm, t int64) (string, string) {\n\td, err := ledger.Cancel(p.id, t)\n\tif err != nil {\n\t\treturn \"\", err.Error()\n\t}\n\tchain.Emit(\"Decision\", \"id\", itoa(d.ID), \"kind\", d.Kind, \"state\", \"cancelled\")\n\t// A cancelled expectation releases its transition's intent.\n\tif ep, ok := d.Params.(*expectParams); ok {\n\t\trelease(ep.from, ep.to, ep.local, ep.gen, ep.owner)\n\t}\n\treturn \"cancelled decision \" + itoa(p.id), \"\"\n}\n\n// NewCancelRequest withdraws pending decision id at once (ADR pr6375 §7.5:\n// cancels are immediate). Guardians cannot cancel; only a decision can.\nfunc NewCancelRequest(cur realm, id int64) Request {\n\td, ok := ledger.Find(id)\n\tif !ok || !d.Pending() {\n\t\tpanic(\"ibcgov: decision \" + itoa(id) + \" is not pending\")\n\t}\n\ttext := \"cancel decision \" + itoa(id) + \" (\" + d.Kind + \")\"\n\treturn propose(cur, \"cancel\", text, func(cur realm, made, t int64) {\n\t\tdecide(cur, \"cancel\", text, \u0026cancelParams{id: id}, made, t, false)\n\t})\n}\n\n// Decision is decision id, by value, and whether it exists.\nfunc Decision(id int64) (decisions.Decision, bool) { return ledger.Find(id) }\n\n// LastDecision is the number of the newest decision.\nfunc LastDecision() int64 { return ledger.Last() }\n"},{"name":"render.gno","body":"package ibcgov\n\nimport (\n\t\"encoding/hex\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/aib/jsonmd/v0\"\n\n\t\"gno.land/p/aib/decisions/v0\"\n)\n\n// Render serves the governance state:\n//\n//\t\"\"                   the page: governor, channel, seats, flags, holds, decisions\n//\t\"decisions\"          every decision, oldest first, as JSON\n//\t\"dispatch/\u003ce\u003e/\u003cseq\u003e\" an Ethereum-bound decision's packet state, as JSON\n//\n// The page's text comes from the builders, which take no free text: client\n// ids, denoms and incidents pass character checks, addresses and keys are\n// parsed, numbers are formatted here. The decision list is written as the\n// ledger keeps it; single values are escaped for inline markdown.\nfunc Render(path string) string {\n\tswitch {\n\tcase path == \"\":\n\t\treturn home()\n\tcase path == \"decisions\":\n\t\treturn jsonmd.Inert(ledger.JSON())\n\tcase strings.HasPrefix(path, \"dispatch/\"):\n\t\tparts := strings.Split(strings.TrimPrefix(path, \"dispatch/\"), \"/\")\n\t\tif len(parts) != 2 {\n\t\t\treturn \"null\"\n\t\t}\n\t\te, err1 := strconv.ParseUint(parts[0], 10, 32)\n\t\ts, err2 := strconv.ParseUint(parts[1], 10, 64)\n\t\tif err1 != nil || err2 != nil {\n\t\t\treturn \"null\"\n\t\t}\n\t\treturn jsonmd.Inert(dispatchJSON(uint32(e), s))\n\t}\n\treturn \"# Not found\\n\"\n}\n\nfunc home() string {\n\tvar b strings.Builder\n\tb.WriteString(\"# IBC governance\\n\\n\")\n\tb.WriteString(\"- \" + decisions.GovernorLine(LaunchGovernor, yielded) + \"\\n\")\n\tb.WriteString(\"- \" + decisions.Clocks() + \"\\n\")\n\tif epoch == 0 {\n\t\tb.WriteString(\"- governance channel: not bound\\n\")\n\t} else {\n\t\tb.WriteString(\"- governance channel: epoch \" + strconv.FormatUint(uint64(epoch), 10) + \" over client \" +\n\t\t\tjsonmd.Text(EpochClient()) + \", last seq \" + u64s(LastSeq(epoch)) + \"\\n\")\n\t}\n\tif Held() {\n\t\tb.WriteString(\"- **guardian hold** since \" + itoa(holdSince) + \": queued decisions are deferred, unpauses are spent\\n\")\n\t}\n\tb.WriteString(\"\\n## Seats\\n\\n\")\n\tfor i := 0; i \u003c SeatCount; i++ {\n\t\trole := \"guardian\"\n\t\tif !IsGuardianSeat(i) {\n\t\t\trole = \"watch key\"\n\t\t}\n\t\tholder := string(seats[i])\n\t\tif holder == \"\" {\n\t\t\tholder = \"empty\"\n\t\t}\n\t\tb.WriteString(\"- \" + strconv.Itoa(i) + \" (\" + role + \"): \" + jsonmd.Text(holder) + \"\\n\")\n\t}\n\tb.WriteString(\"\\n## Routes\\n\\n\")\n\tif routes.Size() == 0 {\n\t\tb.WriteString(\"No route enrolled.\\n\")\n\t}\n\troutes.Iterate(\"\", \"\", func(k string, v any) bool {\n\t\tr := v.(*routeRec)\n\t\t// A flag line carries the packet keys a guardian named, whose client\n\t\t// part is any text: every line is escaped as inline Markdown.\n\t\tb.WriteString(\"- \" + jsonmd.Text(k) + \" (payouts epoch \" + itoa(r.epoch) + \")\\n\")\n\t\tfor _, f := range Flags(k) {\n\t\t\tb.WriteString(\"  - flag: \" + jsonmd.Text(f) + \"\\n\")\n\t\t}\n\t\treturn false\n\t})\n\tb.WriteString(\"\\n## Decisions\\n\\n\")\n\tb.WriteString(ledger.Page())\n\treturn b.String()\n}\n\nfunc dispatchJSON(e uint32, seq uint64) string {\n\td := dispatchOf(e, seq)\n\tif d == nil {\n\t\treturn \"null\"\n\t}\n\treturn \"{\\\"epoch\\\":\" + strconv.FormatUint(uint64(d.epoch), 10) + \",\\\"seq\\\":\" + u64s(d.seq) +\n\t\t\",\\\"kind\\\":\" + strconv.Itoa(int(d.kind)) + \",\\\"decision\\\":\" + itoa(d.decision) +\n\t\t\",\\\"body\\\":\" + strconv.Quote(hex.EncodeToString(d.body)) + \",\\\"status\\\":\" + strconv.Itoa(int(d.status)) +\n\t\t\",\\\"redispatch\\\":\" + strconv.FormatBool(d.redispatch) + \",\\\"in_flight\\\":\" + strconv.FormatBool(d.inFlight) +\n\t\t\",\\\"transport\\\":\" + u64s(d.transport) + \",\\\"sends\\\":\" + strconv.Itoa(d.sends) + \",\\\"event\\\":\" + strconv.Quote(d.event) + \"}\"\n}\n"},{"name":"routes.gno","body":"package ibcgov\n\nimport (\n\t\"chain\"\n\t\"math\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/aib/encoding/ethaddr/v0\"\n\t\"gno.land/p/aib/ibc/ics20/controls/v0\"\n\t\"gno.land/p/aib/ibc/ics20/v0\"\n\t\"gno.land/p/aib/ibc/lightclient/ethereum/v0\"\n\t\"gno.land/r/aib/ibc/apps/transfer\"\n\t\"gno.land/r/aib/ibc/core\"\n)\n\n// The builders of the Ethereum route controls kept by the transfer app (ADR\n// pr6375 §7.1, §7.8, §7.9, §7.12, §7.13). Each applies through\n// transfer.GovApply, which re-checks the parameters against the live state\n// and refuses (the decision is spent) rather than panics. Delays: enrolling a\n// route, a new denom, raising a limit, a new fee collector, unrefusing\n// recipients, credits, sweep payments, losses, recapitalizations, reductions,\n// accepted deficits, reconciliation settlements and route replacements wait 7\n// days; lowering a limit, refusing recipients, a void of packets a guardian\n// named and a restore of withheld claims apply at once.\n\n// view is the transfer app's controls, read only.\nfunc view() controls.Reader {\n\trd, ok := transfer.ExtView(\"controls\").(controls.Reader)\n\tif !ok {\n\t\tpanic(\"ibcgov: the transfer app keeps no route controls (install transfer impl/v1 first)\")\n\t}\n\treturn rd\n}\n\n// routeParams is a decision applied through transfer.GovApply.\ntype routeParams struct {\n\top string\n\tp  controls.Params\n\t// enrolment and replacement also change this realm's records.\n\tenroll  bool\n\treplace string // the old client a replacement retires\n}\n\n// deferredBy defers a decision paying out of a route while the matching\n// function is flagged: a sweep payment under a payouts flag, a settlement\n// paying refunds under a refunds hold.\nfunc (p *routeParams) deferredBy() string {\n\tswitch p.op {\n\tcase controls.OpSweep:\n\t\tif Flagged(p.p.Route, FnPayouts) {\n\t\t\treturn \"the payouts of route \" + p.p.Route + \" are flagged\"\n\t\t}\n\tcase controls.OpSettle:\n\t\tfor _, it := range p.p.Items {\n\t\t\tif it.Action == controls.SettlePay \u0026\u0026 it.Amount \u003e 0 \u0026\u0026 Flagged(p.p.Route, FnRefunds) {\n\t\t\t\treturn \"the refunds of route \" + p.p.Route + \" are held\"\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc (p *routeParams) apply(cur realm, t int64) (string, string) {\n\tidentity := \"\"\n\tif p.enroll {\n\t\t// Re-check the client on the live state (ADR pr6375 §7.5): one that\n\t\t// froze or expired during the delay takes no route, in neither realm.\n\t\tif refusal := checkRouteClient(p.p.Route); refusal != \"\" {\n\t\t\treturn \"\", refusal\n\t\t}\n\t\tvar refusal string\n\t\tif identity, refusal = enrolIdentity(p.p.Route, p.p.Keys[0]); refusal != \"\" {\n\t\t\treturn \"\", refusal\n\t\t}\n\t\tif refusal = checkOneRoute(); refusal != \"\" {\n\t\t\treturn \"\", refusal\n\t\t}\n\t}\n\tif p.replace != \"\" {\n\t\t// Re-check the new client before retiring the old one.\n\t\tif refusal := checkRouteClient(p.p.Client); refusal != \"\" {\n\t\t\treturn \"\", refusal\n\t\t}\n\t\tif view().Client(p.p.Route) != p.replace {\n\t\t\treturn \"\", \"the route's client is no longer \" + p.replace\n\t\t}\n\t\tif refusal := checkIdentity(p.p.Route, p.p.Client); refusal != \"\" {\n\t\t\treturn \"\", refusal\n\t\t}\n\t\tif refusal := checkBoundForReplace(); refusal != \"\" {\n\t\t\treturn \"\", refusal\n\t\t}\n\t}\n\tif refusal := transfer.GovApply(cross(cur), p.op, p.p.Encode()); refusal != \"\" {\n\t\treturn \"\", refusal\n\t}\n\tnote := p.op\n\tif p.enroll {\n\t\troutes.Set(p.p.Route, \u0026routeRec{epochAt: t, identity: identity})\n\t}\n\tif p.replace != \"\" {\n\t\t// The two-phase epoch switch (ADR pr6375 §7.12): while the\n\t\t// governance epoch runs over the old client, it is retired for\n\t\t// transfers only, until IBCAdmin's epoch notice moves the epoch.\n\t\tretired := \" retired\"\n\t\tswitch {\n\t\tcase onExpectedPath(p.replace):\n\t\t\t// The epoch runs over it, or may still move through it.\n\t\t\tcore.RetireClientPort(cross(cur), p.replace, transfer.PortID)\n\t\t\tawaitGov(p.replace)\n\t\t\tretired = \" retired for transfers\"\n\t\tcase govRoute(p.p.Route) \u0026\u0026 !core.IsClientRetiredFor(p.replace, GovPort):\n\t\t\t// A client of the governance channel's route that the graph\n\t\t\t// does not hold may still be a prefix endpoint of a catch-up\n\t\t\t// (the graph may have had no room for its transition): it is\n\t\t\t// never retired for the gov port by a replacement, only by an\n\t\t\t// authenticated epoch switch passing it (one that did already is\n\t\t\t// retired for good here). Its open gov port is\n\t\t\t// inert meanwhile: a notice over it needs an active transition\n\t\t\t// approving it, and decisions go over the epoch's client only.\n\t\t\tcore.RetireClientPort(cross(cur), p.replace, transfer.PortID)\n\t\t\tretired = \" retired for transfers (kept for governance)\"\n\t\tcase !core.IsClientRetired(p.replace):\n\t\t\tcore.RetireClient(cross(cur), p.replace)\n\t\t}\n\t\t// After a migration every meter starts empty (ADR pr6375 §7.1): a\n\t\t// new payouts epoch, reset lazily in constant work.\n\t\tbumpPayoutsEpoch(p.p.Route, t)\n\t\tnote += \": client \" + p.replace + retired + \", meters emptied\"\n\t\t// The new client is the route's alias from now on: flags standing\n\t\t// under its own id join the route's.\n\t\tif n := foldFlags(p.p.Client, p.p.Route, t); n \u003e 0 {\n\t\t\tnote += \", \" + strconv.Itoa(n) + \" flags of \" + p.p.Client + \" moved to the route\"\n\t\t}\n\t\tif onExpectedPath(p.replace) {\n\t\t\tnote += \"; governance epoch \" + strconv.FormatUint(uint64(epoch), 10) + \" stays over \" + EpochClient() + \" until IBCAdmin's epoch notices arrive\"\n\t\t}\n\t}\n\treturn note, \"\"\n}\n\nfunc routeRequest(cur realm, op, kind, text string, p controls.Params, queued bool) Request {\n\trp := \u0026routeParams{op: op, p: p}\n\treturn propose(cur, kind, text, func(cur realm, made, t int64) {\n\t\tdecide(cur, kind, text, rp, made, t, queued)\n\t})\n}\n\n// mustRoute is the key of the route route names, a route key or one of its\n// clients, current or retired (routeKeyOf); it panics if there is none.\nfunc mustRoute(route string) string {\n\tkey := routeKeyOf(route)\n\tif !routes.Has(key) {\n\t\tpanic(\"ibcgov: no route \" + route)\n\t}\n\treturn key\n}\n\nfunc mustIncident(s string) {\n\tif !isIdent(s, 32) {\n\t\tpanic(\"ibcgov: an incident is 1 to 32 characters of a-z, 0-9 and '-'\")\n\t}\n}\n\nfunc mustKeys(list string) []string {\n\tkeys := splitList(list)\n\tif len(keys) == 0 || len(keys) \u003e controls.MaxKeys {\n\t\tpanic(\"ibcgov: name 1 to \" + strconv.Itoa(controls.MaxKeys) + \" packet keys\")\n\t}\n\tfor _, k := range keys {\n\t\tif _, _, ok := controls.ParsePacketKey(k); !ok {\n\t\t\tpanic(\"ibcgov: not a packet key: \" + k)\n\t\t}\n\t}\n\treturn keys\n}\n\n// checkRouteClient is why client cannot take a route now, as a new route's\n// client or a replacement's, or \"\": an Active Ethereum client with its\n// counterparty registered, not retired, and no route's client already. On a\n// fresh deployment the transfer app starts with the route controls, so an\n// Ethereum client that is no route's carries nothing and holds no transfer\n// state a route's key would no longer reach.\nfunc checkRouteClient(client string) string {\n\tswitch {\n\tcase core.ClientType(client) != ethereum.ClientType:\n\t\treturn client + \" is not an Ethereum client\"\n\tcase core.IsClientRetiredAny(client):\n\t\t// Retired on any port, governance's included (an epoch notice moved\n\t\t// past it): a client Ethereum retired takes no route.\n\t\treturn client + \" is retired\"\n\tcase core.ClientStatus(client) != \"Active\":\n\t\treturn client + \" is not Active\"\n\tcase core.CounterpartyClientID(client) == \"\":\n\t\treturn client + \" has no counterparty registered\"\n\t}\n\tif rd, ok := transfer.ExtView(\"controls\").(controls.Reader); ok {\n\t\tif rd.RouteOf(client) != \"\" || rd.RetiredRouteOf(client) != \"\" {\n\t\t\treturn client + \" is a route's client already\"\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// enrolIdentity is the identity client gives a new route, or why it cannot\n// (ADR pr6375 §7.12, \"Route identity is preserved\"): its verifier must name\n// one, and the bridge's router must be the router it reads, so the protected\n// contracts and the history the route authenticates are the same router's.\nfunc enrolIdentity(client, router string) (string, string) {\n\tidentity, read := core.ClientRouteIdentity(client)\n\tswitch {\n\tcase identity == \"\":\n\t\treturn \"\", client + \"'s verifier names no route identity\"\n\tcase read != router:\n\t\treturn \"\", \"the bridge's router \" + router + \" is not the router \" + read + \" that \" + client + \" reads\"\n\t}\n\treturn identity, \"\"\n}\n\n// checkBoundForReplace is why the route's client cannot be replaced yet, or\n// \"\": not before the governance channel is bound. Ethereum's epoch 1 pairs\n// its router client with the route's original gno client; a replacement\n// before the binding would retire that client for good and leave the binding\n// to open epoch 1 over a pair Ethereum does not use, which nothing could\n// repair (Ethereum refuses an emergency recovery until it records its first\n// decision from gno.land). A client that fails before the binding is\n// recovered in place (NewRecoverClientRequest, same id), or the unlaunched\n// deployment is restarted.\nfunc checkBoundForReplace() string {\n\tif epoch == 0 {\n\t\treturn \"the governance channel is not bound yet: the route's client is replaced only after the binding (before it, recover the client in place, or restart the deployment)\"\n\t}\n\treturn \"\"\n}\n\n// checkOneRoute is why another Ethereum route cannot be enrolled, or \"\":\n// one governance channel serves one logical Ethereum transfer route, ever.\n// IBCAdmin replaces, recovers, settles and reopens only the route of the\n// client it governs over (its kind 10 and emergency recovery name that\n// client), so a second route would have no Ethereum replacement or\n// recovery path. The first enrolment fixes the route's key and its\n// deployment's identity; replacements keep both. Several routes, or a\n// second deployment, need a further ibcgov and Ethereum-side support,\n// neither shipped. Checked when an enrolment is requested and again when it\n// executes, so of two requested together only the first applies.\nfunc checkOneRoute() string {\n\tif routes.Size() \u003e 0 {\n\t\treturn \"the governance channel serves one Ethereum route, enrolled already: a second route would have no Ethereum replacement or recovery path\"\n\t}\n\treturn \"\"\n}\n\n// RouteIdentity is the identity route recorded at its enrolment (route is a\n// route key or one of its clients), \"\" if there is no such route: the\n// Ethereum chain id, genesis validators root, ICS26 router and commitments\n// slot every client of the route authenticates.\nfunc RouteIdentity(route string) string {\n\tv := routes.Get(routeKeyOf(route))\n\tif v == nil {\n\t\treturn \"\"\n\t}\n\treturn v.(*routeRec).identity\n}\n\n// checkIdentity is why client cannot replace route's client, or \"\": it must\n// authenticate the history the route recorded at its enrolment (ADR pr6375\n// §7.12). A client of another chain or router, however genuine, would prove\n// that router's receipts, not the route's: moving a route there is a\n// migration of its own, not a replacement.\nfunc checkIdentity(route, client string) string {\n\twant := RouteIdentity(route)\n\tgot, _ := core.ClientRouteIdentity(client)\n\tif want == \"\" || got != want {\n\t\treturn client + \" authenticates \" + quoteIdentity(got) + \", not route \" + route + \"'s \" + quoteIdentity(want)\n\t}\n\treturn \"\"\n}\n\n// govRoute reports whether route's clients can carry the governance epoch:\n// the channel is bound and the route authenticates its identity (a local\n// client approved for a transition must). Before the binding, no client is\n// needed: epoch 1 opens over the route's client then.\nfunc govRoute(route string) bool {\n\treturn epoch != 0 \u0026\u0026 govIdentity != \"\" \u0026\u0026 RouteIdentity(route) == govIdentity\n}\n\nfunc quoteIdentity(s string) string {\n\tif s == \"\" {\n\t\treturn \"no route identity\"\n\t}\n\treturn \"(\" + s + \")\"\n}\n\n// bridgeRealms are the realms of the bridge with no withdrawal entry point.\n// Every implementation the core and transfer proxies ever registered is one\n// too, read from their registries (BridgeRealm), so a registration after\n// deployment is covered at once.\nvar bridgeRealms = []string{\n\tCorePath,\n\tTransferPath,\n\tPkgPath,\n\t\"gno.land/r/aib/ibc/ethrelay\",\n\t// Kept as a library and never registered (a fresh deployment starts\n\t// with impl/v1), so no registry lists it.\n\t\"gno.land/r/aib/ibc/apps/transfer/impl/v0\",\n}\n\n// BridgeRealm is which realm of the bridge a is, or \"\" for none: its path,\n// or \"a registered implementation\". A credit to one of them is swept (ADR\n// pr6375 §7.1), never paid to an address whose code could not move it, and\n// none can be a fee collector.\nfunc BridgeRealm(a address) string {\n\tfor _, p := range bridgeRealms {\n\t\tif chain.PackageAddress(p) == a {\n\t\t\treturn p\n\t\t}\n\t}\n\t// Every registered implementation, superseded ones included (a registry\n\t// only grows): one keyed lookup in each proxy's address index, whatever\n\t// the number registered.\n\tif core.IsImplAddress(a) || transfer.IsImplAddress(a) {\n\t\treturn \"a registered implementation\"\n\t}\n\treturn \"\"\n}\n\n// mustCollector reads a fee collector: an address that is no realm of the\n// bridge (the transfer app checks it again when the decision executes,\n// against every implementation registered by then).\nfunc mustCollector(s string) address {\n\ta := mustAddr(s)\n\tif p := BridgeRealm(a); p != \"\" {\n\t\tpanic(\"ibcgov: the fee collector \" + string(a) + \" is a realm of the bridge (\" + p + \")\")\n\t}\n\treturn a\n}\n\n// mustBridge reads the bridge contracts of a route: \"router ics20 ibcAdmin\",\n// three distinct non-zero Ethereum addresses, space-separated. They are the\n// route's protected recipients from its enrolment on (ADR pr6375 R12, §7.8).\nfunc mustBridge(bridge string) []string {\n\tf := strings.Fields(bridge)\n\tif len(f) != 3 {\n\t\tpanic(\"ibcgov: the bridge contracts are \\\"router ics20 ibcAdmin\\\", three Ethereum addresses, not \" + bridge)\n\t}\n\tfor i, s := range f {\n\t\ta, err := ethaddr.Parse(s)\n\t\tif err != nil {\n\t\t\tpanic(\"ibcgov: \" + err.Error())\n\t\t}\n\t\tf[i] = a.Lower()\n\t}\n\tif err := controls.CheckBridge(f[0], f[1], f[2]); err != nil {\n\t\tpanic(\"ibcgov: \" + err.Error())\n\t}\n\treturn f\n}\n\nfunc bridgeText(f []string) string {\n\treturn \"router \" + f[0] + \", ICS20 transfer \" + f[1] + \", IBCAdmin \" + f[2]\n}\n\n// NewEnrollRouteRequest makes the Ethereum client clientID a controlled route\n// of the transfer app after 7 days (enrolling a route loosens): its key is\n// clientID, its fee collector feeCollector, its protected bridge contracts\n// bridge (\"router ics20 ibcAdmin\", see mustBridge; the router must be the one\n// clientID reads). The route records clientID's identity (RouteIdentity), which\n// every replacement must match. Until a denom is enabled\n// on it (NewSetDenomRequest), the route refuses every send and receive. The\n// client is checked again when the decision executes: one no longer Active\n// leaves it spent, with no route in either realm.\nfunc NewEnrollRouteRequest(cur realm, clientID, feeCollector, bridge string) Request {\n\tif refusal := checkRouteClient(clientID); refusal != \"\" {\n\t\tpanic(\"ibcgov: \" + refusal)\n\t}\n\tif routes.Has(clientID) {\n\t\tpanic(\"ibcgov: \" + clientID + \" is a route already\")\n\t}\n\tif refusal := checkOneRoute(); refusal != \"\" {\n\t\tpanic(\"ibcgov: \" + refusal)\n\t}\n\tfc := mustCollector(feeCollector)\n\tb := mustBridge(bridge)\n\tif _, refusal := enrolIdentity(clientID, b[0]); refusal != \"\" {\n\t\tpanic(\"ibcgov: \" + refusal)\n\t}\n\ttext := \"enroll route \" + clientID + \" with fee collector \" + string(fc) + \" and bridge contracts \" + bridgeText(b)\n\trp := \u0026routeParams{op: controls.OpEnroll, p: controls.Params{Route: clientID, To: fc, Keys: b}, enroll: true}\n\treturn propose(cur, \"enroll route\", text, func(cur realm, made, t int64) {\n\t\tdecide(cur, \"enroll route\", text, rp, made, t, true)\n\t})\n}\n\n// denomText spells out every field of a denom's configuration: the\n// parameters are the record (ADR pr6375 §7.5), in the proposal's description\n// and the decision's text alike.\nfunc denomText(denom string, c controls.DenomConfig) string {\n\tremote := c.Remote\n\tif remote == \"\" {\n\t\tremote = \"-\"\n\t}\n\treturn denom + \" kind=\" + c.Kind + \" capFloor=\" + u256s(c.CapFloor) + \" supplyCap=\" + u256s(c.SupplyCap) +\n\t\t\" fee=\" + u256s(c.Fee) + \" feeFloor=\" + u256s(c.FeeFloor) + \" sendMax=\" + u256s(c.SendMax) + \" sendMin=\" + u256s(c.SendMin) +\n\t\t\" refundCap=\" + u256s(c.RefundCap) + \" remoteToken=\" + remote + \" decimals=\" + strconv.Itoa(c.Decimals)\n}\n\n// denomConfig checks a denom's configuration as NewSetDenomRequest takes it\n// and returns the accounting denom (a minted denom named by its token's\n// Ethereum address becomes its voucher's IBC denom on route) and the\n// configuration.\nfunc denomConfig(route, denom, kind string, capFloor, supplyCap, fee, feeFloor, sendMax, sendMin, refundCap uint256, remoteToken string, decimals int) (string, controls.DenomConfig) {\n\tif !isDenom(denom) {\n\t\tpanic(\"ibcgov: not a denom: \" + denom)\n\t}\n\tif kind == controls.KindMint \u0026\u0026 strings.HasPrefix(denom, \"0x\") {\n\t\ta, err := ethaddr.Parse(denom)\n\t\tif err != nil {\n\t\t\tpanic(\"ibcgov: \" + err.Error())\n\t\t}\n\t\tdenom = ics20.NewDenom(a.Lower(), ics20.NewHop(\"transfer\", route)).IBCDenom()\n\t}\n\tif (kind == controls.KindMint) != strings.HasPrefix(denom, \"ibc/\") {\n\t\tpanic(\"ibcgov: a minted denom is a voucher's ibc/ denom (or its token's address), an unescrowed one is not\")\n\t}\n\tremote := \"\"\n\tif remoteToken != \"\" {\n\t\ta, err := ethaddr.Parse(remoteToken)\n\t\tif err != nil {\n\t\t\tpanic(\"ibcgov: \" + err.Error())\n\t\t}\n\t\tremote = a.Lower()\n\t}\n\tc := controls.DenomConfig{Kind: kind, CapFloor: capFloor, SupplyCap: supplyCap, Fee: fee, FeeFloor: feeFloor,\n\t\tSendMax: sendMax, SendMin: sendMin, RefundCap: refundCap, Remote: remote, Decimals: decimals}\n\tif err := c.Validate(); err != nil {\n\t\tpanic(\"ibcgov: \" + err.Error())\n\t}\n\treturn denom, c\n}\n\n// NewSetDenomRequest enables a denom on route or changes its limits (ADR\n// pr6375 §7.1, §7.8, §9): kind \"unescrow\" (a gno-native token: denom is its\n// escrow denom, \"ugnot\" or a GRC20 alias) or \"mint\" (an Ethereum token: denom\n// is its voucher's IBC denom); capFloor the day cap floor (unescrow) or day\n// cap (mint); supplyCap the voucher supply cap (mint, else 0); fee and its\n// floor; sendMax and sendMin (unescrow, else 0); refundCap the refund meter's\n// day cap; remoteToken (unescrow, else \"\") the token's GnoVoucher on Ethereum,\n// which sends may never name as their receiver; decimals (mint, else 0) the\n// decimals of the token on Ethereum, 0 to 18, which its voucher is created\n// with on its first receive (ADR 0005, \"Voucher decimals\": an ICS20 packet\n// carries no token metadata). Amounts are base units of the\n// denom, as uint256 (an 18-decimal token's limits are set in its own units). For a minted denom, denom may be the token's Ethereum address\n// (0x…): the voucher denom is then computed, ibc/\u003csha256(\"transfer/\u003croute\u003e/\n// \u003clower-case address\u003e\")\u003e, the denom this chain keys the token's vouchers by.\n// A change that only tightens applies at once; a new denom or any loosening\n// waits 7 days, a change of the decimals too. Once the voucher exists its\n// decimals never change: a configuration naming others is refused here and\n// when the decision executes.\nfunc NewSetDenomRequest(cur realm, route, denom, kind string, capFloor, supplyCap, fee, feeFloor, sendMax, sendMin, refundCap uint256, remoteToken string, decimals int) Request {\n\troute = mustRoute(route)\n\tdenom, c := denomConfig(route, denom, kind, capFloor, supplyCap, fee, feeFloor, sendMax, sendMin, refundCap, remoteToken, decimals)\n\tif old, ok := view().DenomConfig(route, denom); ok \u0026\u0026 old.Kind != kind {\n\t\tpanic(\"ibcgov: a denom never changes accounting\")\n\t}\n\tif d, ok := transfer.VoucherDecimals(denom); ok \u0026\u0026 d != decimals {\n\t\tpanic(\"ibcgov: the voucher of \" + denom + \" exists with \" + strconv.Itoa(d) + \" decimals, which never change\")\n\t}\n\t// At once or queued is decided against the configuration when the\n\t// decision is recorded: another decision may have changed it since the\n\t// request was built.\n\tloosens := func() bool {\n\t\told, ok := view().DenomConfig(route, denom)\n\t\treturn !ok || controls.Loosens(old, c)\n\t}\n\ttext := \"denom on route \" + route + \": \" + denomText(denom, c)\n\trp := \u0026routeParams{op: controls.OpDenom, p: controls.Params{Route: route, Denom: denom, Config: c}}\n\treturn propose(cur, \"set denom\", text, func(cur realm, made, t int64) {\n\t\tdecide(cur, \"set denom\", text, rp, made, t, loosens())\n\t})\n}\n\n// NewSetFeeCollectorRequest replaces a route's fee collector after 7 days.\nfunc NewSetFeeCollectorRequest(cur realm, route, feeCollector string) Request {\n\troute = mustRoute(route)\n\tfc := mustCollector(feeCollector)\n\treturn routeRequest(cur, controls.OpCollector, \"set fee collector\", \"fee collector of route \"+route+\": \"+string(fc),\n\t\tcontrols.Params{Route: route, To: fc}, true)\n}\n\nfunc mustEthAddrs(list string) []string {\n\titems := splitList(list)\n\tif len(items) == 0 || len(items) \u003e controls.MaxKeys {\n\t\tpanic(\"ibcgov: name 1 to \" + strconv.Itoa(controls.MaxKeys) + \" addresses\")\n\t}\n\tvar out []string\n\tfor _, it := range items {\n\t\ta, err := ethaddr.Parse(it)\n\t\tif err != nil {\n\t\t\tpanic(\"ibcgov: \" + err.Error())\n\t\t}\n\t\tout = append(out, a.Lower())\n\t}\n\treturn out\n}\n\n// NewRefuseRecipientsRequest adds Ethereum addresses to the route's refused\n// send recipients, at once: contracts beyond the protected ones its\n// enrolment named (the router, ICS20 transfer and IBCAdmin) and its voucher\n// tokens, which are refused already.\nfunc NewRefuseRecipientsRequest(cur realm, route, addrs string) Request {\n\troute = mustRoute(route)\n\tlist := mustEthAddrs(addrs)\n\treturn routeRequest(cur, controls.OpRefuse, \"refuse recipients\", \"refuse recipients on route \"+route+\": \"+strings.Join(list, \",\"),\n\t\tcontrols.Params{Route: route, Keys: list}, false)\n}\n\n// NewUnrefuseRecipientsRequest removes refused send recipients after 7 days.\n// The route's protected bridge contracts (its enrolment's router, ICS20\n// transfer and IBCAdmin, and its voucher tokens) stay refused whatever it\n// names.\nfunc NewUnrefuseRecipientsRequest(cur realm, route, addrs string) Request {\n\troute = mustRoute(route)\n\tlist := mustEthAddrs(addrs)\n\treturn routeRequest(cur, controls.OpUnrefuse, \"unrefuse recipients\", \"stop refusing recipients on route \"+route+\": \"+strings.Join(list, \",\"),\n\t\tcontrols.Params{Route: route, Keys: list}, true)\n}\n\n// NewVoidRequest voids claims or sweep entries of route by packet key (ADR\n// pr6375 §7.9): forged drops the remainders and books what was paid as lost;\n// withheld keeps the remainders booked and restorable. It applies at once when\n// a guardian's standing payouts flag names every packet, after 7 days\n// otherwise. A void never refunds the source.\nfunc NewVoidRequest(cur realm, route, keys string, forged bool, incident string) Request {\n\troute = mustRoute(route)\n\tmustIncident(incident)\n\tlist := mustKeys(keys)\n\trd := view()\n\tfor _, k := range list {\n\t\tif !rd.Voidable(route, k, forged) {\n\t\t\tpanic(\"ibcgov: \" + k + \" cannot be voided that way as it stands\")\n\t\t}\n\t}\n\t// Whether a guardian named every packet is read when the decision is\n\t// recorded.\n\tnamed := func() bool {\n\t\tfor _, k := range list {\n\t\t\tif !NamedSuspect(route, k) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\treturn true\n\t}\n\thow := \"withheld\"\n\tif forged {\n\t\thow = \"forged\"\n\t}\n\ttext := \"void as \" + how + \" on route \" + route + \" (incident \" + incident + \"): \" + strings.Join(list, \",\")\n\trp := \u0026routeParams{op: controls.OpVoid, p: controls.Params{Route: route, Keys: list, Forged: forged, Incident: incident}}\n\treturn propose(cur, \"void\", text, func(cur realm, made, t int64) {\n\t\tdecide(cur, \"void\", text, rp, made, t, !named())\n\t})\n}\n\n// NewRestoreRequest restores withheld claims after 7 days (ADR pr6375 §7.9:\n// a withheld void can be restored). A restoration loosens: it resumes the\n// claims' payouts, so it waits like every loosening and a guardian hold\n// defers it. A forged void and a voided sweep entry are\n// final; transfer re-checks every key when the decision applies.\nfunc NewRestoreRequest(cur realm, route, keys string) Request {\n\troute = mustRoute(route)\n\tlist := mustKeys(keys)\n\trd := view()\n\tfor _, k := range list {\n\t\tif c, ok := rd.Claim(route, k); !ok || c.State != controls.Withheld {\n\t\t\tpanic(\"ibcgov: \" + k + \" is no withheld claim\")\n\t\t}\n\t}\n\ttext := \"restore on route \" + route + \": \" + strings.Join(list, \",\")\n\treturn routeRequest(cur, controls.OpRestore, \"restore\", text, controls.Params{Route: route, Keys: list}, true)\n}\n\n// NewCreditRequest credits a packet by vote after 7 days (ADR pr6375 §7.9):\n// only a packet received on the route with a success acknowledgement, for\n// its whole payable amount (a haircut goes through NewReduceRequest), never\n// above the free lock, deduplicated against every claim of the packet.\nfunc NewCreditRequest(cur realm, route, key, denom, to string, amount uint256) Request {\n\troute = mustRoute(route)\n\tif _, _, ok := controls.ParsePacketKey(key); !ok {\n\t\tpanic(\"ibcgov: not a packet key: \" + key)\n\t}\n\tif !isDenom(denom) || amount == 0 {\n\t\tpanic(\"ibcgov: a credit names a denom and a positive amount\")\n\t}\n\ta := mustAddr(to)\n\ttext := \"credit \" + u256s(amount) + \" \" + denom + \" to \" + string(a) + \" for packet \" + key + \" on route \" + route\n\treturn routeRequest(cur, controls.OpCredit, \"credit\", text, controls.Params{Route: route, Key: key, Denom: denom, To: a, Amount: amount}, true)\n}\n\n// NewSweepRequest pays sweep entries of one denom to an address after 7\n// days.\nfunc NewSweepRequest(cur realm, route, keys, to string) Request {\n\troute = mustRoute(route)\n\tlist := mustKeys(keys)\n\ta := mustAddr(to)\n\tif a == chain.PackageAddress(transfer.PkgPath) {\n\t\tpanic(\"ibcgov: a sweep pays out of the transfer realm, never to it\")\n\t}\n\treturn routeRequest(cur, controls.OpSweep, \"sweep\", \"pay sweep entries on route \"+route+\" to \"+string(a)+\": \"+strings.Join(list, \",\"),\n\t\tcontrols.Params{Route: route, Keys: list, To: a}, true)\n}\n\n// Reconciliation causes a recorded loss may name (ADR pr6375 §7.12).\nvar lossCauses = map[string]bool{\n\t\"forged-delivery\": true, // a delivery the honest source never sent\n\t\"collision\":       true, // a forged receipt occupied an honest packet's sequence\n}\n\n// NewRecordLossRequest records a loss a reconciliation recognized, after 7\n// days: chain \"gno\" or \"ethereum\", cause one of forged-delivery, collision.\n// A packet has one loss, whatever the path that books it: a receive\n// represented on gno.land (a claim or a sweep entry) books its loss by a void\n// as forged or a forged-receive settlement, so a \"gno\" loss of it is refused,\n// and a second loss of any packet is refused (controls.Route.RecordLoss).\nfunc NewRecordLossRequest(cur realm, route, key, denom string, amount uint256, chain, incident, cause string) Request {\n\troute = mustRoute(route)\n\tmustIncident(incident)\n\tif !lossCauses[cause] {\n\t\tpanic(\"ibcgov: a loss cause is forged-delivery or collision (a refund paid for a delivered packet is settled with NewSettleRequest action 4)\")\n\t}\n\tif chain != controls.ChainGno \u0026\u0026 chain != controls.ChainEthereum {\n\t\tpanic(\"ibcgov: a loss happens on gno or ethereum\")\n\t}\n\tif _, _, ok := controls.ParsePacketKey(key); !ok || !isDenom(denom) || amount == 0 {\n\t\tpanic(\"ibcgov: a loss names a packet key, a denom and a positive amount\")\n\t}\n\ttext := \"loss of \" + u256s(amount) + \" \" + denom + \" on \" + chain + \" for packet \" + key + \" on route \" + route + \" (\" + cause + \", incident \" + incident + \")\"\n\treturn routeRequest(cur, controls.OpLoss, \"record loss\", text,\n\t\tcontrols.Params{Route: route, Key: key, Denom: denom, Amount: amount, Chain: chain, Incident: incident, Cause: cause}, true)\n}\n\n// NewRecapitalizeRequest authorizes a recapitalization of route's escrow of\n// denom by amount after 7 days; anyone then pays it with\n// transfer.Recapitalize(route, id). A recapitalization in coins is paid as\n// one coin, an int64, so it is at most the chain's coin maximum; a GRC20\n// one takes the whole uint256 range.\nfunc NewRecapitalizeRequest(cur realm, route, denom string, amount uint256, incident string) Request {\n\troute = mustRoute(route)\n\tmustIncident(incident)\n\tif !isDenom(denom) || amount == 0 {\n\t\tpanic(\"ibcgov: a recapitalization names a denom and a positive amount\")\n\t}\n\tif !transfer.IsGRC20Alias(denom) \u0026\u0026 amount \u003e math.MaxInt64 {\n\t\tpanic(\"ibcgov: a recapitalization in coins is at most \" + u256s(math.MaxInt64) + \" \" + denom + \": no coin can pay more\")\n\t}\n\treturn routeRequest(cur, controls.OpRecapitalize, \"recapitalize\", \"recapitalize \"+u256s(amount)+\" \"+denom+\" on route \"+route+\" (incident \"+incident+\")\",\n\t\tcontrols.Params{Route: route, Denom: denom, Amount: amount, Incident: incident}, true)\n}\n\n// NewRevokeRecapRequest revokes the unpaid recapitalization authorization id\n// of route, at once (a removal): its funding fell through, and its part of\n// the incident's losses is free for another restoring decision. A reduction\n// decided later revokes conflicting authorizations by itself; an accepted\n// deficit does not (a recapitalization may still restore it).\nfunc NewRevokeRecapRequest(cur realm, route string, id int64) Request {\n\troute = mustRoute(route)\n\tif r, ok := view().Recap(route, id); !ok || r.Paid || r.Revoked {\n\t\tpanic(\"ibcgov: no unpaid recapitalization \" + itoa(id) + \" on route \" + route)\n\t}\n\treturn routeRequest(cur, controls.OpRevokeRecap, \"revoke recap\", \"revoke recapitalization \"+itoa(id)+\" on route \"+route,\n\t\tcontrols.Params{Route: route, ID: id}, false)\n}\n\n// NewReduceRequest cuts an open or withheld claim, or an unpaid owed sweep\n// entry, by amount against an incident after 7 days (a published liability\n// reduction; refunds are never reduced).\nfunc NewReduceRequest(cur realm, route, key string, amount uint256, incident string) Request {\n\troute = mustRoute(route)\n\tmustIncident(incident)\n\tif _, _, ok := controls.ParsePacketKey(key); !ok || amount == 0 {\n\t\tpanic(\"ibcgov: a reduction names a packet key and a positive amount\")\n\t}\n\treturn routeRequest(cur, controls.OpReduce, \"reduce\", \"reduce the liability of \"+key+\" on route \"+route+\" by \"+u256s(amount)+\" (incident \"+incident+\")\",\n\t\tcontrols.Params{Route: route, Key: key, Amount: amount, Incident: incident}, true)\n}\n\n// NewAcceptDeficitRequest records an accepted deficit after 7 days: the route\n// reopens with amount of its losses unrecovered, as the trust statement says.\nfunc NewAcceptDeficitRequest(cur realm, route, denom string, amount uint256, incident string) Request {\n\troute = mustRoute(route)\n\tmustIncident(incident)\n\tif !isDenom(denom) || amount == 0 {\n\t\tpanic(\"ibcgov: an accepted deficit names a denom and a positive amount\")\n\t}\n\treturn routeRequest(cur, controls.OpAccept, \"accept deficit\", \"accept a deficit of \"+u256s(amount)+\" \"+denom+\" on route \"+route+\" (incident \"+incident+\")\",\n\t\tcontrols.Params{Route: route, Denom: denom, Amount: amount, Incident: incident}, true)\n}\n\n// NewSettleRequest applies a reconciliation's outcomes to the sends of\n// client, a client a replacement retired from route, after 7 days (ADR pr6375\n// §7.12, the gno analogue of Ethereum's kind 12): items is a comma-separated\n// list of \"sequence:action:amount\" (the amount in base units as decimal\n// digits), action 1 a genuine refund: pay its whole\n// remainder (amount equals what the send carried minus what was refunded: a\n// refund is never cut short; 0 for one paid in full, which only settles the\n// record), 2 reject a queued refund none of which was paid (amount 0), 3\n// settled as delivered, no refund paid (amount 0), 4 a forged refund: the\n// unpaid remainder rejected and what was paid (amount, equal to it) recorded\n// as a loss, 5 a forged receive (sequence an inbound sequence of the retired\n// client; amount, what its receive paid out, recorded as a loss once); at\n// most 64 items. Actions 1 to 4 must match the outcome proven through the\n// replacement client (transfer.ProveOutcome). Each send is settled exactly once; a queued refund leaves the queue\n// under any action.\nfunc NewSettleRequest(cur realm, route, client, items, incident string) Request {\n\troute = mustRoute(route)\n\tmustIncident(incident)\n\tif view().RetiredRouteOf(client) != route {\n\t\tpanic(\"ibcgov: \" + client + \" is no client a replacement retired from route \" + route)\n\t}\n\tlist := splitList(items)\n\tif len(list) == 0 || len(list) \u003e controls.MaxKeys {\n\t\tpanic(\"ibcgov: name 1 to \" + strconv.Itoa(controls.MaxKeys) + \" settlement items\")\n\t}\n\tvar parsed []controls.SettleItem\n\t// A sequence is named once per direction: action 5 names an inbound\n\t// sequence, the others an outbound one, separate spaces.\n\tseen := map[string]bool{}\n\tfor _, it := range list {\n\t\tparts := strings.Split(it, \":\")\n\t\tif len(parts) != 3 {\n\t\t\tpanic(\"ibcgov: a settlement item is sequence:action:amount, not \" + it)\n\t\t}\n\t\tseq, err1 := strconv.ParseUint(parts[0], 10, 64)\n\t\taction, err2 := strconv.ParseUint(parts[1], 10, 8)\n\t\tamount, err3 := strconv.ParseUint256(parts[2], 10)\n\t\tif err1 != nil || err2 != nil || err3 != nil || seq == 0 || seen[settleItemKey(seq, action)] || action \u003c 1 || action \u003e 5 {\n\t\t\tpanic(\"ibcgov: not a settlement item, or a sequence named twice in its direction: \" + it)\n\t\t}\n\t\tif ((action == 4 || action == 5) \u0026\u0026 amount == 0) || ((action == 2 || action == 3) \u0026\u0026 amount != 0) {\n\t\t\tpanic(\"ibcgov: a forged refund or receive names a positive amount, a rejection or a delivery none: \" + it)\n\t\t}\n\t\tseen[settleItemKey(seq, action)] = true\n\t\tparsed = append(parsed, controls.SettleItem{Seq: seq, Action: uint8(action), Amount: amount})\n\t}\n\ttext := \"settle sends of \" + client + \" on route \" + route + \" (incident \" + incident + \"): \" + strings.Join(list, \",\")\n\treturn routeRequest(cur, controls.OpSettle, \"settle\", text,\n\t\tcontrols.Params{Route: route, Client: client, Items: parsed, Incident: incident}, true)\n}\n\n// settleItemKey is a settlement item's packet identity: its sequence in its\n// direction (action 5 an inbound sequence, actions 1 to 4 an outbound one).\nfunc settleItemKey(seq, action uint64) string {\n\tif action == 5 {\n\t\treturn \"in|\" + strconv.FormatUint(seq, 10)\n\t}\n\treturn \"out|\" + strconv.FormatUint(seq, 10)\n}\n\n// NewReplaceRouteRequest replaces route's Ethereum client by newClientID after\n// 7 days (ADR pr6375 §7.12, R18, the gno.land side): the transfer app re-points\n// the route to the new client in constant work (escrow, vouchers and controls\n// stay keyed by the route key; denoms on the wire carry the new client id),\n// the core retires the old client, for transfers only while the governance\n// epoch runs over it or may still move through it (an expected transition,\n// see OnRecvPacket): it is retired for good once IBCAdmin's epoch notices\n// have moved the epoch past it. newClientID is an Active\n// Ethereum client with its counterparty (the new router client) registered,\n// authenticating the route's identity (the same chain, router and commitments\n// slot: RouteIdentity), checked again when the decision executes.\n// The route closes with the replacement: the reconciliation's settlements\n// (NewSettleRequest) and restoring decisions follow, then\n// NewReopenRouteRequest reopens it. suspectFrom is the gno.land height from\n// which acknowledgements and timeouts processed through the old client are\n// suspect (ADR pr6375 §7.12): every send of it processed at or after that\n// height must be settled before the reopening. As on Ethereum, it replaces\n// the route's current client whether the route is open or closed: each\n// replacement is an incident of its own, covering the client it retires,\n// the earlier incidents' records and obligations untouched, a reopening\n// under way aborted. Their proofs then go through the route's current\n// client. The interval never starts before the previous replacement.\nfunc NewReplaceRouteRequest(cur realm, route, newClientID string, suspectFrom int64) Request {\n\troute = mustRoute(route)\n\tif suspectFrom \u003c 0 || suspectFrom \u003e chainHeight() {\n\t\tpanic(\"ibcgov: the suspect interval starts at a past gno.land height (0 for every record)\")\n\t}\n\tif refusal := checkRouteClient(newClientID); refusal != \"\" {\n\t\tpanic(\"ibcgov: \" + refusal)\n\t}\n\tif refusal := checkIdentity(route, newClientID); refusal != \"\" {\n\t\tpanic(\"ibcgov: \" + refusal)\n\t}\n\tif refusal := checkBoundForReplace(); refusal != \"\" {\n\t\tpanic(\"ibcgov: \" + refusal)\n\t}\n\told := view().Client(route)\n\ttext := \"replace the client of route \" + route + \": \" + old + \" by \" + newClientID + \", suspect from gno.land height \" + itoa(suspectFrom)\n\trp := \u0026routeParams{op: controls.OpReplace, p: controls.Params{Route: route, Client: newClientID, Height: suspectFrom}, replace: old}\n\treturn propose(cur, \"replace route\", text, func(cur realm, made, t int64) {\n\t\tdecide(cur, \"replace route\", text, rp, made, t, true)\n\t})\n}\n\n// NewReopenRouteRequest reopens a route a replacement closed, after 7 days\n// (ADR pr6375 §5.4, §7.12, the gno analogue of Ethereum's kind 13): transfer\n// refuses it while any send record of a retired client of the route is\n// still open (its commitment outstanding or its refund queued, not settled by\n// NewSettleRequest), and records every loss not recapitalized or reduced as\n// an accepted deficit.\nfunc NewReopenRouteRequest(cur realm, route string) Request {\n\troute = mustRoute(route)\n\tif !view().Closed(route) {\n\t\tpanic(\"ibcgov: route \" + route + \" is not closed\")\n\t}\n\treturn routeRequest(cur, controls.OpReopen, \"reopen route\", \"reopen route \"+route, controls.Params{Route: route}, true)\n}\n\n// bindParams opens governance epoch 1 over a client.\ntype bindParams struct{ client string }\n\nfunc (p *bindParams) apply(cur realm, t int64) (string, string) {\n\tif epoch != 0 {\n\t\treturn \"\", \"the governance channel is bound already\"\n\t}\n\tif refusal := checkBindClient(p.client); refusal != \"\" {\n\t\treturn \"\", refusal\n\t}\n\tbindChannel(p.client)\n\treturn \"governance epoch 1 over \" + p.client, \"\"\n}\n\n// checkBindClient is why client cannot carry the governance channel, \"\" if\n// it can: an Active Ethereum client with its counterparty registered, the\n// current client of a route ibcgov enrolled. The channel always rides a\n// route, so a replacement of the route, the way out of a client that\n// freezes or expires, moves the channel's epoch with it.\nfunc checkBindClient(client string) string {\n\tswitch {\n\tcase core.ClientType(client) != ethereum.ClientType:\n\t\treturn client + \" is not an Ethereum client\"\n\tcase core.IsClientRetiredFor(client, GovPort):\n\t\treturn client + \" is retired\"\n\tcase core.ClientStatus(client) != \"Active\":\n\t\treturn client + \" is not Active\"\n\tcase core.CounterpartyClientID(client) == \"\":\n\t\treturn client + \" has no counterparty registered\"\n\t}\n\trd, ok := transfer.ExtView(\"controls\").(controls.Reader)\n\tif !ok {\n\t\treturn client + \" is no route's client: no transfer controls\"\n\t}\n\t// The one enrolled route's original client (checkOneRoute), the pair\n\t// Ethereum's epoch 1 uses: no replacement precedes the binding\n\t// (checkBoundForReplace).\n\tif k := rd.RouteOf(client); k == \"\" || !routes.Has(k) || rd.Client(k) != client || k != client {\n\t\treturn client + \" is not the original client of the enrolled route\"\n\t}\n\treturn \"\"\n}\n\n// NewBindGovernanceRequest binds the governance channel to the Ethereum client\n// clientID after 7 days (enrolling a route loosens): it opens epoch 1, whose\n// decisions go to IBCAdmin over clientID. clientID must be the current client\n// of an enrolled route, and Active, when it is made and again when it\n// executes. Once only: later epochs open with a replacement of that route.\nfunc NewBindGovernanceRequest(cur realm, clientID string) Request {\n\tif epoch != 0 {\n\t\tpanic(\"ibcgov: the governance channel is bound already\")\n\t}\n\tif refusal := checkBindClient(clientID); refusal != \"\" {\n\t\tpanic(\"ibcgov: \" + refusal)\n\t}\n\ttext := \"bind the governance channel to \" + clientID + \" (epoch 1)\"\n\tp := \u0026bindParams{client: clientID}\n\treturn propose(cur, \"bind governance\", text, func(cur realm, made, t int64) {\n\t\tdecide(cur, \"bind governance\", text, p, made, t, true)\n\t})\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"0+WcEiDGBLUHrRv1RuzoQ36I7CUFfD8pQ63EQzNUS+80ZtRsynYRIITdWrQnhUu/PFl8d3LlHRw0V1DcB4Db0w=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"impl","path":"gno.land/r/aib/ibc/apps/transfer/impl/v1","files":[{"name":"ack.gno","body":"package impl\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\n\t\"gno.land/p/aib/ibc/ics20/controls/v0\"\n\t\"gno.land/p/aib/ibc/types/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/aib/ibc/apps/transfer\"\n\t\"gno.land/r/aib/ibc/ibcgov\"\n)\n\n// The acknowledgement and timeout callbacks of a route never fail on the\n// controls: they attach the outcome to the send record and pay the refund, or\n// queue it under the refund meter or a refunds hold. Pauses and limits never\n// block them (ADR pr6375 §7.3).\n\n// sendRecord is the record of a send on an Ethereum client the callbacks\n// settle. Every send on a route writes one, so a send without one is refused\n// (ADR pr6375 §7.13: no refund on an Ethereum client bypasses the refund\n// meter or the refunds hold).\nfunc (l *logic) sendRecord(_ int, rlm realm, rt *controls.Route, sourceClient string, sequence uint64) (controls.SendRecord, error) {\n\tkey := controls.PacketKey(sourceClient, sequence)\n\tif rt == nil {\n\t\treturn controls.SendRecord{}, ufmt.Errorf(\"client %s is an Ethereum client of no enrolled route: it sent no %s\", sourceClient, key)\n\t}\n\trec, ok := rt.Send(key)\n\tif !ok {\n\t\treturn controls.SendRecord{}, ufmt.Errorf(\"send %s has no record\", key)\n\t}\n\treturn rec, nil\n}\n\n// OnAcknowledgementPacket is impl/v0's for clients the controls do not cover.\n// On an Ethereum client it settles the send's record: a success completes\n// it, an error refunds the sender under the refund meter and the refunds\n// hold. A send without a record is refused.\nfunc (l *logic) OnAcknowledgementPacket(_ int, rlm realm, sourceClient, destinationClient string, sequence uint64, acknowledgement []byte, payload types.Payload) error {\n\trt, covered := l.routeOf(0, rlm, sourceClient)\n\tif !covered {\n\t\treturn l.Logic.OnAcknowledgementPacket(0, rlm, sourceClient, destinationClient, sequence, acknowledgement, payload)\n\t}\n\trec, err := l.sendRecord(0, rlm, rt, sourceClient, sequence)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar ack types.AppAcknowledgement\n\tif bytes.Equal(acknowledgement, types.UniversalErrorAcknowledgement()) {\n\t\tack = types.NewErrorAppAcknowledgement(errors.New(\"receive packet failed\"))\n\t} else {\n\t\tif err := ack.UnmarshalJSON(acknowledgement); err != nil {\n\t\t\treturn ufmt.Errorf(\"cannot unmarshal ICS-20 transfer packet acknowledgement: %v\", err)\n\t\t}\n\t\tif !ack.Success() {\n\t\t\treturn ufmt.Errorf(\"cannot pass in a custom error acknowledgement with IBC v2\")\n\t\t}\n\t}\n\toutcome := controls.OutcomeSuccess\n\tif !ack.Success() {\n\t\toutcome = controls.OutcomeError\n\t}\n\tl.settle(0, rlm, rt, rec, outcome)\n\tattrs := []string{\n\t\ttransfer.AttributeKeySender, string(rec.Sender),\n\t\ttransfer.AttributeKeyReceiver, rec.Receiver,\n\t\ttransfer.AttributeKeyDenom, rec.Denom,\n\t\ttransfer.AttributeKeyAmount, u(rec.Amount),\n\t\ttransfer.AttributeKeyAck, string(acknowledgement),\n\t}\n\ttransfer.EmitPacket(0, rlm, attrs...)\n\tif ack.Success() {\n\t\ttransfer.EmitPacket(0, rlm, transfer.AttributeKeyAckSuccess, string(ack.Response.Result))\n\t} else {\n\t\ttransfer.EmitPacket(0, rlm, transfer.AttributeKeyAckError, ack.Response.Error)\n\t}\n\treturn nil\n}\n\n// OnTimeoutPacket is impl/v0's for clients the controls do not cover; on an\n// Ethereum client it refunds the sender under the refund meter and the\n// refunds hold. A send without a record is refused.\nfunc (l *logic) OnTimeoutPacket(_ int, rlm realm, sourceClient, destinationClient string, sequence uint64, payload types.Payload) error {\n\trt, covered := l.routeOf(0, rlm, sourceClient)\n\tif !covered {\n\t\treturn l.Logic.OnTimeoutPacket(0, rlm, sourceClient, destinationClient, sequence, payload)\n\t}\n\trec, err := l.sendRecord(0, rlm, rt, sourceClient, sequence)\n\tif err != nil {\n\t\treturn err\n\t}\n\tl.settle(0, rlm, rt, rec, controls.OutcomeTimeout)\n\ttransfer.EmitTimeout(0, rlm, string(rec.Sender), rec.Denom, u(rec.Amount), \"\")\n\treturn nil\n}\n\n// settle attaches the outcome to the record and pays what the refund meter\n// allows now; the rest is queued (and everything under a refunds hold).\nfunc (l *logic) settle(_ int, rlm realm, rt *controls.Route, rec controls.SendRecord, outcome string) {\n\thold := ibcgov.Flagged(rt.Key, ibcgov.FnRefunds) || rt.IsClosed()\n\tpay, err := rt.Settle(now(), chainHeight(), rec.Key, outcome, hold, l.lockOf(rt, rec.Denom))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif pay \u003e 0 {\n\t\tl.payOut(0, rlm, rt, rec.Denom, rec.Sender, pay)\n\t}\n\ttransfer.EmitRoute(0, rlm, \"settle\", \"route\", rt.Key, \"packet\", rec.Key, \"outcome\", outcome,\n\t\t\"payment\", rt.SendPayment(rec.Key), \"refunded\", u(pay))\n}\n\n// payOut moves amount of denom to `to`: out of the route's escrow for a\n// gno-native token, as newly minted vouchers for an Ethereum token.\nfunc (l *logic) payOut(_ int, rlm realm, rt *controls.Route, denom string, to address, amount uint256) {\n\tif amount == 0 {\n\t\treturn\n\t}\n\tacc := rt.Denom(denom)\n\tif acc != nil \u0026\u0026 acc.Config.Kind == controls.KindMint {\n\t\tif err := l.store.MintVoucher(0, rlm, denom, to, amount); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\treturn\n\t}\n\tvar err error\n\tif transfer.IsGRC20Alias(denom) {\n\t\terr = l.store.UnescrowGRC20(0, rlm, rt.Key, string(to), denom, amount)\n\t} else {\n\t\terr = l.store.UnescrowNative(0, rlm, rt.Key, string(to), denom, amount)\n\t}\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n"},{"name":"controls.gno","body":"package impl\n\nimport (\n\t\"bytes\"\n\t\"chain\"\n\t\"math\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/aib/ibc/host/v0\"\n\t\"gno.land/p/aib/ibc/ics20/controls/v0\"\n\t\"gno.land/p/aib/ibc/lightclient/ethereum/v0\"\n\t\"gno.land/p/aib/ibc/types/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/aib/ibc/apps/transfer\"\n\t\"gno.land/r/aib/ibc/core\"\n\t\"gno.land/r/aib/ibc/ibcgov\"\n)\n\n// routeFor is the route a permissionless call names (Claim, ClaimRefunds,\n// ContinueReopen, Recapitalize): by its current client id, or by its route\n// key.\nfunc (l *logic) routeFor(_ int, rlm realm, clientOrRoute string) *controls.Route {\n\treg := l.registry(0, rlm)\n\tif rt := reg.RouteOf(clientOrRoute); rt != nil {\n\t\treturn rt\n\t}\n\tif rt := reg.Route(clientOrRoute); rt != nil {\n\t\treturn rt\n\t}\n\tpanic(\"no route \" + clientOrRoute)\n}\n\n// lockOf is the escrow a gno-native denom of the route holds (its lock), 0\n// for a minted denom.\nfunc (l *logic) lockOf(rt *controls.Route, denom string) uint256 {\n\tif acc := rt.Denom(denom); acc == nil || acc.Config.Kind == controls.KindMint {\n\t\treturn 0\n\t}\n\treturn l.store.Escrowed(rt.Key, denom)\n}\n\n// Claim pays up to n claims of denom on the route (ADR pr6375 §7.1); a claim\n// whose recipient has become a bridge realm is swept instead. Under a\n// payouts flag, on a closed route, or while a loss has no restoring decision,\n// it pays nothing.\nfunc (l *logic) Claim(_ int, rlm realm, clientID, denom string, n int) int {\n\trt := l.routeFor(0, rlm, clientID)\n\tif ibcgov.Flagged(rt.Key, ibcgov.FnPayouts) || rt.ExposureClosed() {\n\t\treturn 0\n\t}\n\tepoch, epochAt := ibcgov.PayoutsEpoch(rt.Key)\n\t// The recipient is checked again at payout: one that became a bridge\n\t// realm after its credit (an implementation registered since) is not\n\t// paid; its claim moves to the owed sweep.\n\tps, moved, err := rt.PayClaimsSweeping(now(), epoch, epochAt, denom, n, l.lockOf(rt, denom), isBridgeRealm)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tfor _, k := range moved {\n\t\ttransfer.EmitRoute(0, rlm, \"claim_swept\", \"route\", rt.Key, \"packet\", k, \"denom\", denom, \"reason\", \"bridge realm\")\n\t}\n\tfor _, p := range ps {\n\t\tl.payOut(0, rlm, rt, denom, p.To, p.Amount)\n\t\tl.payOut(0, rlm, rt, denom, rt.FeeCollector, p.Fee)\n\t\ttransfer.EmitRoute(0, rlm, \"claim\", \"route\", rt.Key, \"packet\", p.Key, \"denom\", denom, \"to\", string(p.To),\n\t\t\t\"amount\", u(p.Amount), \"fee\", u(p.Fee))\n\t}\n\treturn len(ps)\n}\n\n// ClaimRefunds pays up to n queued refunds of denom on the route (ADR pr6375\n// §7.13). Under a refunds hold, or on a route a replacement closed, it pays\n// nothing; a loss does not hold refunds.\nfunc (l *logic) ClaimRefunds(_ int, rlm realm, clientID, denom string, n int) int {\n\trt := l.routeFor(0, rlm, clientID)\n\thold := ibcgov.Flagged(rt.Key, ibcgov.FnRefunds) || rt.IsClosed()\n\tps, err := rt.PayRefunds(now(), denom, n, hold, l.lockOf(rt, denom))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tfor _, p := range ps {\n\t\tl.payOut(0, rlm, rt, denom, p.To, p.Amount)\n\t\ttransfer.EmitRoute(0, rlm, \"refund\", \"route\", rt.Key, \"packet\", p.Key, \"denom\", denom, \"to\", string(p.To),\n\t\t\t\"amount\", u(p.Amount))\n\t}\n\treturn len(ps)\n}\n\n// ProveOutcome reads the destination router's receipt and acknowledgement of\n// a retired client's send through the route's replacement client and stores\n// the outcome on the record (ADR pr6375 §7.12): no receipt, or the packet's\n// receipt with an error acknowledgement, is refundable; the packet's receipt\n// with a success acknowledgement is delivered; another packet's receipt is a\n// collision. The router's receipt is keccak256(abi.encode(packet)).\nfunc (l *logic) ProveOutcome(_ int, rlm realm, packet types.Packet, proofHeight types.Height, receiptProof, ackProof any) {\n\treg := l.registry(0, rlm)\n\tk := reg.RetiredRouteOf(packet.SourceClient)\n\tif k == \"\" {\n\t\tpanic(\"client \" + packet.SourceClient + \" is no client a replacement retired\")\n\t}\n\trt := reg.Route(k)\n\t// The trust generation the proof is read under: the outcome is void once\n\t// the verifier is recovered, frozen, upgraded, migrated or substituted.\n\tgen := core.ClientTrustGeneration(rt.Client)\n\t// CounterpartyValue refuses a verifier that is not Active or retired.\n\treceipt, err := core.CounterpartyValue(rt.Client, proofHeight, receiptProof, host.PacketReceiptKey(packet.DestinationClient, packet.Sequence))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tvar outcome uint8\n\tswitch {\n\tcase isZero(receipt):\n\t\t// An absence is final only once the destination can never receive\n\t\t// the packet: at a proof height whose destination time is at or\n\t\t// past the packet's timeout (IBC's own timeout rule).\n\t\tat, err := core.ClientTimestampAt(rt.Client, proofHeight)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tif at \u003c packet.TimeoutTimestamp {\n\t\t\tpanic(ufmt.Sprintf(\"the destination's time at that height (%d) precedes the packet's timeout (%d): its absence is not final\", at, packet.TimeoutTimestamp))\n\t\t}\n\t\toutcome = controls.ProvenRefundable\n\tcase bytes.Equal(receipt, ethereum.PacketReceiptCommitment(packet)):\n\t\tack, err := core.CounterpartyValue(rt.Client, proofHeight, ackProof, host.PacketAcknowledgementKey(packet.DestinationClient, packet.Sequence))\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\terrAck := types.CommitAcknowledgement(types.Acknowledgement{AppAcknowledgements: [][]byte{types.UniversalErrorAcknowledgement()}})\n\t\tswitch {\n\t\tcase bytes.Equal(ack, successAckCommitment()):\n\t\t\toutcome = controls.ProvenDelivered\n\t\tcase bytes.Equal(ack, errAck):\n\t\t\toutcome = controls.ProvenRefundable\n\t\tdefault:\n\t\t\tpanic(\"the destination wrote no final acknowledgement of the packet\")\n\t\t}\n\tdefault:\n\t\toutcome = controls.ProvenCollision\n\t}\n\tkey := controls.PacketKey(packet.SourceClient, packet.Sequence)\n\tif err := rt.ProveOutcome(key, packet, outcome, proofHeight.RevisionHeight, rt.Client, gen, verifierTrusted); err != nil {\n\t\tpanic(err)\n\t}\n\ttransfer.EmitRoute(0, rlm, \"proven\", \"route\", rt.Key, \"packet\", key, \"outcome\", strconv.Itoa(int(outcome)),\n\t\t\"height\", proofHeight.String())\n}\n\n// verifierTrusted reports whether a client can still vouch for an outcome it\n// proved under trust generation gen: Active (not frozen or expired), not\n// retired, and gen still its current generation (no recovery, freeze,\n// upgrade, migration or substitution since).\nfunc verifierTrusted(client string, gen uint64) bool {\n\treturn client != \"\" \u0026\u0026 core.ClientStatus(client) == \"Active\" \u0026\u0026 !core.IsClientRetiredFor(client, transfer.PortID) \u0026\u0026\n\t\tcore.ClientTrustGeneration(client) == gen\n}\n\nfunc isZero(b []byte) bool {\n\tfor _, c := range b {\n\t\tif c != 0 {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// ContinueReopen runs the next bounded batch of a route's reopening (ADR\n// pr6375 §5.4 kind 13); anyone may.\nfunc (l *logic) ContinueReopen(_ int, rlm realm, route string) bool {\n\trt := l.routeFor(0, rlm, route)\n\taccepted, done, err := rt.ContinueReopen()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\ttransfer.EmitRoute(0, rlm, \"reopen\", \"route\", rt.Key, \"accepted\", u(accepted), \"open\", strconv.FormatBool(done))\n\treturn done\n}\n\n// PrepareReplacement walks the next batch of a replaced route's retired\n// records (ADR pr6375 §7.12); anyone may.\nfunc (l *logic) PrepareReplacement(_ int, rlm realm, route string, n int) bool {\n\trt := l.routeFor(0, rlm, route)\n\treserved, done, err := rt.PrepareReplacement(n)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\ttransfer.EmitRoute(0, rlm, \"prepare\", \"route\", rt.Key, \"reserved\", strconv.Itoa(reserved), \"done\", strconv.FormatBool(done))\n\treturn done\n}\n\n// Recapitalize pays an authorized recapitalization into the route's escrow\n// (ADR pr6375 §7.9): exactly its amount, credited to the accounted backing\n// and debited from the unrecovered losses. A plain transfer to the escrow\n// address stays surplus and repairs nothing.\nfunc (l *logic) Recapitalize(_ int, rlm realm, sent []chain.Coin, route string, id int64) {\n\trt := l.routeFor(0, rlm, route)\n\tr, ok := rt.Recap(id)\n\tif !ok || r.Paid || r.Revoked {\n\t\tpanic(ufmt.Sprintf(\"no unpaid recapitalization %d on route %s\", id, route))\n\t}\n\tpayer := rlm.Previous().Address()\n\tif transfer.IsGRC20Alias(r.Denom) {\n\t\tif len(sent) \u003e 0 {\n\t\t\tpanic(\"a GRC20 recapitalization takes no coins\")\n\t\t}\n\t\tif err := l.store.EscrowGRC20(0, rlm, rt.Key, transfer.ResolveGRC20Alias(r.Denom), payer, r.Amount); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t} else {\n\t\t// A native coin is an int64: an authorization above the chain's\n\t\t// coin maximum can match no coin sent.\n\t\tif len(sent) != 1 || sent[0].Denom != r.Denom || transfer.CoinAmount(sent[0]) != r.Amount {\n\t\t\tpanic(\"send exactly \" + u(r.Amount) + r.Denom + \" in a direct call\")\n\t\t}\n\t\tl.store.AddEscrow(0, rlm, rt.Key, r.Denom, r.Amount)\n\t}\n\tif err := rt.Recapitalize(id, r.Denom, r.Amount, payer); err != nil {\n\t\tpanic(err)\n\t}\n\ttransfer.EmitRoute(0, rlm, \"recapitalize\", \"route\", rt.Key, \"id\", strconv.FormatInt(id, 10), \"denom\", r.Denom,\n\t\t\"amount\", u(r.Amount), \"payer\", string(payer))\n}\n\n// successAckCommitment is the acknowledgement commitment of this app's\n// success acknowledgement, what a credited packet's acknowledgement must be.\nfunc successAckCommitment() []byte {\n\tack := types.NewResultAppAcknowledgement([]byte{byte(1)})\n\treturn types.CommitAcknowledgement(types.Acknowledgement{AppAcknowledgements: [][]byte{ack.MarshalJSON()}})\n}\n\n// creditPending credits a packet whose asynchronous acknowledgement is still\n// unwritten and writes its success acknowledgement in the same transaction\n// (ADR pr6375 §7.9), so it can never be refunded at its source; on a retired\n// client too, which the core allows for a receipt it holds. It returns why it\n// refuses, before any change.\nfunc (l *logic) creditPending(_ int, rlm realm, rt *controls.Route, p controls.Params, pkt types.Packet, height, epoch int64) string {\n\tif height \u003c rt.Activated {\n\t\treturn \"packet \" + p.Key + \" was received before the route's activation\"\n\t}\n\tif core.Paused() {\n\t\treturn \"the core is paused: the packet's acknowledgement cannot be written\"\n\t}\n\tif len(pkt.Payloads) != 1 {\n\t\treturn \"packet \" + p.Key + \" carries no single transfer payload\"\n\t}\n\t_, token, err := unmarshalPayload(pkt.Payloads[0])\n\tif err != nil {\n\t\treturn err.Error()\n\t}\n\t_, accDenom, _ := receivedDenom(rt, pkt.SourceClient, pkt.Payloads[0], token)\n\tcarried, err := token.AmountUint256()\n\tif err != nil {\n\t\treturn err.Error()\n\t}\n\tif accDenom != p.Denom {\n\t\treturn \"packet \" + p.Key + \" carried \" + accDenom + \", not \" + p.Denom\n\t}\n\tif err := rt.CreditPending(now(), chainHeight(), epoch, p.Key, p.Denom, p.To, p.Amount, carried, l.lockOf(rt, p.Denom)); err != nil {\n\t\treturn err.Error()\n\t}\n\tack := types.NewResultAppAcknowledgement([]byte{byte(1)})\n\tclient, seq, _ := controls.ParsePacketKey(p.Key)\n\tcore.WriteAcknowledgement(cross(rlm), client, seq, types.Acknowledgement{AppAcknowledgements: [][]byte{ack.MarshalJSON()}})\n\treturn \"\"\n}\n\n// GovApply applies a governance operation decided by ibcgov (the proxy\n// checked the caller). It re-checks the parameters against the live state and\n// returns why it refuses, which spends the decision; nothing changes then.\nfunc (l *logic) GovApply(_ int, rlm realm, op string, params []byte) string {\n\tp, err := controls.DecodeParams(params)\n\tif err != nil {\n\t\treturn err.Error()\n\t}\n\t// A payee or fee collector is stored and paid in its canonical spelling:\n\t// the GRC20 ledger keys balances by the string, so an uppercase one would\n\t// strand what it receives.\n\tif p.To != \"\" {\n\t\tto, ok := transfer.CanonicalAddress(string(p.To))\n\t\tif !ok {\n\t\t\treturn \"not an address: \" + string(p.To)\n\t\t}\n\t\tp.To = to\n\t}\n\t// A fee collector is never a realm of the bridge (the transfer realm\n\t// itself, the core, an implementation...): a GRC20 fee paid to the\n\t// transfer realm is a self-transfer the token refuses, which would stop\n\t// every GRC20 send and every claim's first installment on the route.\n\tif (op == controls.OpEnroll || op == controls.OpCollector) \u0026\u0026 isBridgeRealm(p.To) {\n\t\treturn \"the fee collector \" + string(p.To) + \" is a realm of the bridge\"\n\t}\n\treg := l.registry(0, rlm)\n\tt := now()\n\tif op == controls.OpEnroll {\n\t\tif !isEthereum(p.Route) || core.IsClientRetiredAny(p.Route) {\n\t\t\treturn p.Route + \" is no usable Ethereum client\"\n\t\t}\n\t\t// The bridge's own contracts are protected recipients from the\n\t\t// enrolment on (ADR pr6375 R12, §7.8): Keys names the router, the\n\t\t// ICS20 transfer and IBCAdmin.\n\t\tif len(p.Keys) != 3 {\n\t\t\treturn \"an enrolment names the route's router, ICS20 transfer and IBCAdmin\"\n\t\t}\n\t\tif err := controls.CheckBridge(p.Keys[0], p.Keys[1], p.Keys[2]); err != nil {\n\t\t\treturn err.Error()\n\t\t}\n\t\trt, err := reg.Enroll(p.Route, p.To, t, chainHeight())\n\t\tif err != nil {\n\t\t\treturn err.Error()\n\t\t}\n\t\tif err := rt.Protect(p.Keys[0], p.Keys[1], p.Keys[2]); err != nil {\n\t\t\tpanic(err) // checked above, on a route just enrolled\n\t\t}\n\t\ttransfer.EmitRoute(0, rlm, \"gov\", \"op\", op, \"route\", p.Route)\n\t\treturn \"\"\n\t}\n\trt := reg.Route(p.Route)\n\tif rt == nil {\n\t\treturn \"no route \" + p.Route\n\t}\n\tepoch, epochAt := ibcgov.PayoutsEpoch(rt.Key)\n\tnote := \"\"\n\tswitch op {\n\tcase controls.OpDenom:\n\t\tminted := strings.HasPrefix(p.Denom, \"ibc/\")\n\t\tif minted != (p.Config.Kind == controls.KindMint) {\n\t\t\treturn \"a minted denom is a voucher's ibc/ denom, an unescrowed one is not\"\n\t\t}\n\t\t// A minted denom's vouchers already in circulation (minted by\n\t\t// impl/v0) count against its supply cap from the start.\n\t\tvar outstanding uint256\n\t\tif tok := l.store.VoucherToken(p.Denom); minted \u0026\u0026 tok != nil {\n\t\t\toutstanding = tok.TotalSupply()\n\t\t\t// grc20 fixes a token's decimals at its creation, and the\n\t\t\t// voucher is the token wallets hold: a configuration naming\n\t\t\t// others is refused (ibcgov refuses it when built as well; a\n\t\t\t// first receive during the delay creates the voucher).\n\t\t\tif d := tok.GetDecimals(); d != p.Config.Decimals {\n\t\t\t\treturn \"the voucher of \" + p.Denom + \" exists with \" + strconv.Itoa(d) + \" decimals, which never change\"\n\t\t\t}\n\t\t}\n\t\t// The meters refill at the old caps up to now first: a new cap\n\t\t// never applies to the time before it.\n\t\terr = rt.Reconfigure(p.Denom, p.Config, t, epoch, epochAt, outstanding, l.lockOf(rt, p.Denom))\n\tcase controls.OpCollector:\n\t\terr = reg.SetFeeCollector(rt.Key, p.To)\n\tcase controls.OpRefuse:\n\t\trt.Refuse(p.Keys)\n\tcase controls.OpUnrefuse:\n\t\trt.Unrefuse(p.Keys)\n\tcase controls.OpVoid:\n\t\terr = rt.Void(p.Keys, p.Forged, p.Incident)\n\tcase controls.OpRestore:\n\t\terr = rt.Restore(p.Keys, t, epoch)\n\tcase controls.OpCredit:\n\t\tclient, seq, _ := controls.ParsePacketKey(p.Key)\n\t\tswitch {\n\t\tcase rt.ExposureClosed():\n\t\t\treturn \"route \" + rt.Key + \" is closed (a replacement awaiting its reopening, or a loss without a restoring decision): it credits nothing\"\n\t\tcase !rt.HasClient(client):\n\t\t\treturn \"packet \" + p.Key + \" was not received on route \" + rt.Key\n\t\tcase len(core.PacketReceipt(client, seq)) == 0:\n\t\t\treturn \"packet \" + p.Key + \" was never received\"\n\t\t}\n\t\tif pkt, height, pending := core.PendingAsyncAck(client, seq); pending {\n\t\t\tif refusal := l.creditPending(0, rlm, rt, p, pkt, height, epoch); refusal != \"\" {\n\t\t\t\treturn refusal\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\tif !bytes.Equal(core.PacketAcknowledgement(client, seq), successAckCommitment()) {\n\t\t\treturn \"packet \" + p.Key + \" has no success acknowledgement: an error-acknowledged packet is refunded at its source\"\n\t\t}\n\t\terr = rt.CreditByVote(t, chainHeight(), epoch, p.Key, p.Denom, p.To, p.Amount, l.lockOf(rt, p.Denom))\n\tcase controls.OpSweep:\n\t\tif rt.ExposureClosed() {\n\t\t\treturn \"route \" + rt.Key + \" is closed (a replacement awaiting its reopening, or a loss without a restoring decision): it pays nothing out\"\n\t\t}\n\t\tif len(p.Keys) == 0 {\n\t\t\treturn \"no sweep entry named\"\n\t\t}\n\t\te, ok := rt.Sweep(p.Keys[0])\n\t\tif !ok {\n\t\t\treturn \"no sweep entry \" + p.Keys[0]\n\t\t}\n\t\t// Every payout checked before anything changes: one that would fail\n\t\t// refuses the decision, the entries kept.\n\t\tvar due []payout\n\t\tfor _, k := range p.Keys {\n\t\t\tif x, ok := rt.Sweep(k); ok \u0026\u0026 !x.Voided \u0026\u0026 !x.Paid {\n\t\t\t\tdue = append(due, payout{x.Denom, p.To, x.Amount})\n\t\t\t}\n\t\t}\n\t\tif refusal := l.payable(rt, due); refusal != \"\" {\n\t\t\treturn refusal\n\t\t}\n\t\tvar ps []controls.Payment\n\t\tps, err = rt.PaySweeps(p.Keys, p.To, l.lockOf(rt, e.Denom))\n\t\tfor _, x := range ps {\n\t\t\tl.payOut(0, rlm, rt, x.Denom, x.To, x.Amount)\n\t\t}\n\tcase controls.OpLoss:\n\t\terr = rt.RecordLoss(p.Key, p.Denom, p.Amount, p.Chain, p.Incident, p.Cause)\n\tcase controls.OpRecapitalize:\n\t\t// A recapitalization in coins is paid as one coin, an int64: an\n\t\t// authorization above the chain's coin maximum could never be paid.\n\t\tif !transfer.IsGRC20Alias(p.Denom) \u0026\u0026 p.Amount \u003e math.MaxInt64 {\n\t\t\treturn \"a recapitalization in coins is at most \" + u(math.MaxInt64) + \" \" + p.Denom + \": no coin can pay more\"\n\t\t}\n\t\tvar id int64\n\t\tid, err = rt.AuthorizeRecap(p.Denom, p.Amount, p.Incident)\n\t\tnote = strconv.FormatInt(id, 10)\n\tcase controls.OpRevokeRecap:\n\t\terr = rt.RevokeRecap(p.ID)\n\tcase controls.OpReduce:\n\t\terr = rt.Reduce(p.Key, p.Amount, p.Incident)\n\tcase controls.OpAccept:\n\t\terr = rt.Accept(p.Denom, p.Amount, p.Incident)\n\tcase controls.OpSettle:\n\t\t// The settlement is planned first, changing nothing: which refunds\n\t\t// it pays now and which it parks. Only the payments it makes now\n\t\t// are checked against their token or the banker; one that would\n\t\t// fail refuses the decision, the records unsettled. Then the plan\n\t\t// applies whole.\n\t\tvar plan *controls.SettlePlan\n\t\tplan, err = rt.PlanSettlement(p.Client, p.Items, p.Incident, func(denom string) uint256 { return l.lockOf(rt, denom) }, verifierTrusted)\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\t\tvar due []payout\n\t\tfor _, x := range plan.Payments {\n\t\t\tdue = append(due, payout{x.Denom, x.To, x.Amount})\n\t\t}\n\t\tif refusal := l.payable(rt, due); refusal != \"\" {\n\t\t\treturn refusal\n\t\t}\n\t\tfor _, x := range rt.ApplySettlement(plan) {\n\t\t\tl.payOut(0, rlm, rt, x.Denom, x.To, x.Amount)\n\t\t}\n\t\tif parked := plan.Parked(); len(parked) \u003e 0 {\n\t\t\tnote = \"parked \" + strconv.Itoa(len(parked)) + \" refunds\"\n\t\t}\n\tcase controls.OpReopen:\n\t\tvar accepted uint256\n\t\tvar done bool\n\t\taccepted, done, err = rt.Reopen()\n\t\tnote = \"accepted deficit \" + u(accepted)\n\t\tif !done {\n\t\t\tnote += \"; continue with transfer.ContinueReopen\"\n\t\t}\n\tcase controls.OpReplace:\n\t\tif !isEthereum(p.Client) || core.IsClientRetiredAny(p.Client) {\n\t\t\treturn p.Client + \" is no usable Ethereum client\"\n\t\t}\n\t\terr = reg.Replace(rt.Key, p.Client, p.Height, chainHeight())\n\t\tif err == nil {\n\t\t\t// The first batch of the preparation runs with the replacement:\n\t\t\t// a retired client of at most MaxKeys records is prepared at once.\n\t\t\tvar done bool\n\t\t\t_, done, err = rt.PrepareReplacement(controls.MaxKeys)\n\t\t\tif err == nil \u0026\u0026 !done {\n\t\t\t\tnote = \"prepare the rest with transfer.PrepareReplacement\"\n\t\t\t}\n\t\t}\n\tdefault:\n\t\treturn \"unknown operation \" + op\n\t}\n\tif err != nil {\n\t\treturn err.Error()\n\t}\n\ttransfer.EmitRoute(0, rlm, \"gov\", \"op\", op, \"route\", rt.Key, \"note\", note)\n\treturn \"\"\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/aib/ibc/apps/transfer/impl/v1\"\ngno = \"0.9\"\n"},{"name":"impl.gno","body":"// Package impl is the second implementation of the ICS-20 transfer logic,\n// behind the gno.land/r/aib/ibc/apps/transfer proxy (see docs/adrs/0002 and\n// docs/adrs/0004). It runs impl/v0's logic unchanged for every client that is\n// not an Ethereum client, and puts every Ethereum client under the route\n// controls of ADR pr6375 §7 (R8, R10, R12, R13, R17, R18, §7.13):\n//\n//   - an Ethereum client carries transfers only as the current client of a\n//     route that ibcgov enrolled, and only for the denoms enabled on it; any\n//     other Ethereum client refuses sends and error-acks receives;\n//   - a send of a gno-native token checks the per-message minimum and\n//     maximum, charges the route fee (paid to the route's fee collector),\n//     refuses the zero, reserved and refused recipients and stops under a\n//     sends flag; a voucher returning to Ethereum (a burn) is never limited,\n//     charged or paused;\n//   - every send writes an immutable send record (the full packet, its\n//     commitment and the send's metadata) in the same transaction; the\n//     acknowledgement and timeout callbacks attach the outcome, the local\n//     processing height and the payment state;\n//   - a refund (error acknowledgement or timeout) is paid under the route's\n//     refund meter or queued, and only queued under a refunds hold; queued\n//     refunds are paid by the permissionless ClaimRefunds;\n//   - a receive is refused with an error acknowledgement before any state\n//     change under a credits flag, for a denom not enabled on the route, or\n//     past a minted denom's supply cap; otherwise it books a claim keyed by\n//     the packet (or a sweep entry), in the transaction of the receipt, and\n//     returns a success acknowledgement; claims are paid by the permissionless\n//     Claim within the route's meters, nothing under a payouts flag;\n//   - escrow, vouchers and controls are keyed by the route key; the denoms on\n//     the wire carry the route's current client id, translated both ways, so a\n//     route replacement re-points the route in constant work.\n//\n// The controls state lives in the proxy (Store.InitExt, gno.land/p/aib/ibc/\n// ics20/controls/v0); the flags and holds live in gno.land/r/aib/ibc/ibcgov,\n// which decides every governance operation (GovApply). Like v0, this realm\n// holds no state, is always invoked non-crossing by the proxy, as the proxy,\n// and exports nothing that mutates.\npackage impl\n\nimport (\n\t\"chain/runtime\"\n\t\"time\"\n\n\t\"gno.land/p/aib/ibc/ics20/controls/v0\"\n\t\"gno.land/r/aib/ibc/apps/transfer\"\n\timplv0 \"gno.land/r/aib/ibc/apps/transfer/impl/v0\"\n)\n\nconst version = \"v1\"\n\n// extKey is the proxy extension slot of the route controls.\nconst extKey = \"controls\"\n\ntype logic struct {\n\ttransfer.Logic // impl/v0, for every client the controls do not cover\n\tstore          transfer.Store\n}\n\nvar (\n\t_ transfer.Logic         = (*logic)(nil)\n\t_ transfer.ControlsLogic = (*logic)(nil)\n)\n\n// New builds the implementation against the proxy's store.\nfunc New(s transfer.Store) transfer.Logic {\n\treturn \u0026logic{Logic: implv0.New(s), store: s}\n}\n\nfunc init(cur realm) {\n\ttransfer.RegisterImpl(cross(cur), New, version, nil, true) // installs over any\n}\n\nfunc (l *logic) Version() string { return version }\n\n// OnInstall creates the route controls in the proxy, empty: every meter\n// starts empty (ADR pr6375 §7.1). impl/v1 is the implementation a fresh\n// deployment starts with; it also re-installs over itself and over an\n// implementation without the controls (the proxy never installs one of\n// those once a route exists), keeping the controls state already there.\nfunc (l *logic) OnInstall(_ int, rlm realm, prevPath, prevVersion string) {\n\tl.store.InitExt(0, rlm, extKey, controls.NewRegistryAny)\n}\n\n// registry is the controls state, mutable: only for the proxy's live rlm.\nfunc (l *logic) registry(_ int, rlm realm) *controls.Registry {\n\treturn l.store.Ext(0, rlm, extKey).(*controls.Registry)\n}\n\n// reader is the controls state, read only.\nfunc (l *logic) reader() controls.Reader {\n\trd, _ := l.store.ExtView(extKey).(controls.Reader)\n\treturn rd\n}\n\nfunc now() int64 { return time.Now().Unix() }\n\nfunc chainHeight() int64 { return runtime.ChainHeight() }\n"},{"name":"payable.gno","body":"package impl\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"math\"\n\t\"math/overflow\"\n\n\t\"gno.land/p/aib/ibc/ics20/controls/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/aib/ibc/apps/transfer\"\n\t\"gno.land/r/nt/grc20reg/v1\"\n)\n\n// payout is one payment a governed decision is about to make.\ntype payout struct {\n\tdenom  string\n\tto     address\n\tamount uint256\n}\n\n// payable is why making ps would fail, or \"\": every check payOut's token or\n// banker call would fail on, made before anything changes, so a governed\n// payout that cannot be made is refused (the decision spent) with every\n// entitlement kept, never aborted halfway. Amounts are checked per denom in\n// total: a recipient that is a valid address other than this realm; for a\n// minted denom, its voucher token and the room left under its uint256\n// supply; for a GRC20, its registered token, the route's escrow accounting\n// and this realm's token balance; for a native coin, the escrow accounting,\n// the chain's coin maximum and this realm's bank balance.\nfunc (l *logic) payable(rt *controls.Route, ps []payout) string {\n\tself := chain.PackageAddress(transfer.PkgPath)\n\ttotals := map[string]uint256{}\n\tvar order []string\n\tfor _, p := range ps {\n\t\tif p.amount == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tif !p.to.IsValid() || p.to == self {\n\t\t\treturn ufmt.Sprintf(\"cannot pay %s: not a valid recipient other than the transfer realm\", string(p.to))\n\t\t}\n\t\tif _, ok := totals[p.denom]; !ok {\n\t\t\torder = append(order, p.denom)\n\t\t}\n\t\ttotal, ok := overflow.Addu256(totals[p.denom], p.amount)\n\t\tif !ok {\n\t\t\treturn \"the payments of \" + p.denom + \" overflow\"\n\t\t}\n\t\ttotals[p.denom] = total\n\t}\n\tfor _, denom := range order {\n\t\ttotal := totals[denom]\n\t\tacc := rt.Denom(denom)\n\t\tif acc != nil \u0026\u0026 acc.Config.Kind == controls.KindMint {\n\t\t\ttok := l.store.VoucherToken(denom)\n\t\t\tif tok == nil {\n\t\t\t\treturn \"no voucher token for \" + denom\n\t\t\t}\n\t\t\tif _, ok := overflow.Addu256(tok.TotalSupply(), total); !ok {\n\t\t\t\treturn \"minting \" + u(total) + \" \" + denom + \" would overflow its token supply\"\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif err := l.store.CheckEscrow(rt.Key, denom, total); err != nil {\n\t\t\treturn err.Error()\n\t\t}\n\t\tif transfer.IsGRC20Alias(denom) {\n\t\t\ttok := grc20reg.Get(transfer.ResolveGRC20Alias(denom))\n\t\t\tif tok == nil {\n\t\t\t\treturn \"GRC20 token \" + transfer.ResolveGRC20Alias(denom) + \" not found in grc20reg\"\n\t\t\t}\n\t\t\tif have := tok.BalanceOf(self); have \u003c total {\n\t\t\t\treturn \"this realm holds \" + u(have) + \" of \" + denom + \", short of \" + u(total)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif total \u003e math.MaxInt64 {\n\t\t\treturn \"paying \" + u(total) + \" \" + denom + \" is above the chain's coin maximum\"\n\t\t}\n\t\tif have := banker.NewReadonlyBanker().GetCoin(self, denom); have \u003c int64(total) {\n\t\t\treturn ufmt.Sprintf(\"this realm holds %d %s, short of %d\", have, denom, int64(total))\n\t\t}\n\t}\n\treturn \"\"\n}\n"},{"name":"recv.gno","body":"package impl\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/p/aib/ibc/ics20/abi/v0\"\n\t\"gno.land/p/aib/ibc/ics20/controls/v0\"\n\t\"gno.land/p/aib/ibc/ics20/v0\"\n\t\"gno.land/p/aib/ibc/types/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/aib/ibc/apps/transfer\"\n\timplv0 \"gno.land/r/aib/ibc/apps/transfer/impl/v0\"\n\t\"gno.land/r/aib/ibc/ibcgov\"\n)\n\n// isBridgeRealm reports whether a is a realm of the bridge\n// (ibcgov.BridgeRealm): a credit to one is swept, never paid.\nfunc isBridgeRealm(a address) bool { return ibcgov.BridgeRealm(a) != \"\" }\n\n// receivedDenom is the denom a received token is booked under on rt, its\n// accounting denom and accounting: a gno-native token returning is released\n// from the route's escrow; an Ethereum token arriving is a voucher keyed by\n// the route key.\nfunc receivedDenom(rt *controls.Route, sourceClient string, payload types.Payload, token ics20.Token) (denom ics20.Denom, accDenom, kind string) {\n\tif token.Denom.HasPrefix(payload.SourcePort, sourceClient) {\n\t\tdenom = ics20.Denom{Base: token.Denom.Base, Trace: token.Denom.Trace[1:]}\n\t\treturn denom, denom.IBCDenom(), controls.KindUnescrow\n\t}\n\ttrace := append([]ics20.Hop{ics20.NewHop(payload.DestinationPort, rt.Key)}, token.Denom.Trace...)\n\tdenom = ics20.Denom{Base: token.Denom.Base, Trace: trace}\n\treturn denom, denom.IBCDenom(), controls.KindMint\n}\n\n// OnRecvPacket is impl/v0's for clients the controls do not cover. On a route\n// it books the transfer as a claim keyed by the packet (or a sweep entry) in\n// the transaction of the receipt, and answers a success acknowledgement; the\n// claim is paid later by Claim. Every refusal answers an error acknowledgement\n// before any state change, so the sender is refunded on Ethereum: a credits\n// flag, a closed route, an Ethereum client of no route, a blocked receiver, a\n// denom not enabled on the route, a minted denom past its supply cap. An\n// undecodable receiver is no refusal: it is swept, owed to a decision.\nfunc (l *logic) OnRecvPacket(_ int, rlm realm, sourceClient, destinationClient string, sequence uint64, payload types.Payload) types.RecvPacketResult {\n\trt, covered := l.routeOf(0, rlm, destinationClient)\n\tif !covered {\n\t\treturn l.Logic.OnRecvPacket(0, rlm, sourceClient, destinationClient, sequence, payload)\n\t}\n\tvar (\n\t\tdata   ics20.FungibleTokenPacketData\n\t\ttoken  ics20.Token\n\t\tackErr error\n\t\tswept  bool\n\t)\n\trefuse := func(err error) types.RecvPacketResult {\n\t\tackErr = err\n\t\tack := types.NewErrorAppAcknowledgement(err)\n\t\ttransfer.EmitPacket(0, rlm,\n\t\t\ttransfer.AttributeKeySender, data.Sender,\n\t\t\ttransfer.AttributeKeyReceiver, data.Receiver,\n\t\t\ttransfer.AttributeKeyDenom, token.Denom.Path(),\n\t\t\ttransfer.AttributeKeyAmount, token.Amount,\n\t\t\ttransfer.AttributeKeyMemo, data.Memo,\n\t\t\ttransfer.AttributeKeyAckSuccess, strconv.FormatBool(ack.Success()),\n\t\t\ttransfer.AttributeKeyAckError, ackErr.Error(),\n\t\t)\n\t\treturn types.RecvPacketResult{Status: types.PacketStatus_Failure}\n\t}\n\tif rt == nil {\n\t\treturn refuse(ufmt.Errorf(\"client %s is an Ethereum client of no enrolled route\", destinationClient))\n\t}\n\tif rt.ExposureClosed() {\n\t\treturn refuse(ufmt.Errorf(\"route %s is closed (a replacement awaiting its reopening, or a loss without a restoring decision)\", rt.Key))\n\t}\n\tif err := validatePayload(destinationClient, sourceClient, payload); err != nil {\n\t\treturn refuse(err)\n\t}\n\tvar err error\n\tdata, token, err = unmarshalPayload(payload)\n\tif err != nil {\n\t\treturn refuse(err)\n\t}\n\tif err := implv0.ValidateBaseDenomNoSlash(token); err != nil {\n\t\treturn refuse(err)\n\t}\n\tif ibcgov.Flagged(rt.Key, ibcgov.FnCredits) {\n\t\treturn refuse(ufmt.Errorf(\"credits are paused on route %s\", rt.Key))\n\t}\n\t// An undecodable receiver is booked as an owed sweep entry keeping the\n\t// raw receiver, with a success acknowledgement: it is paid only by a\n\t// 7-day decision naming the payee (ADR pr6375 §6.8, §7.1).\n\treason := \"\"\n\treceiver, ok := transfer.CanonicalAddress(data.Receiver)\n\tif !ok {\n\t\treceiver, reason = address(data.Receiver), \"invalid receiver\"\n\t} else if transfer.IsBlocked(receiver) {\n\t\treturn refuse(ufmt.Errorf(\"receiver %s is blocked\", data.Receiver))\n\t}\n\tamount, err := token.AmountUint256()\n\tif err != nil {\n\t\treturn refuse(err)\n\t}\n\t// A genuine send from Ethereum never carries more than abi.MaxAmount\n\t// (its transfer app refuses it, ADR pr6375 E6): a receive above it can\n\t// only be a forgery, refused before any state change.\n\tif amount \u003e abi.MaxAmount {\n\t\treturn refuse(ufmt.Errorf(\"the amount %s is above Ethereum's maximum of 2^96 - 1\", u(amount)))\n\t}\n\n\tdenom, accDenom, kind := receivedDenom(rt, sourceClient, payload, token)\n\tacc := rt.Denom(accDenom)\n\tif acc != nil \u0026\u0026 acc.Config.Kind != kind {\n\t\treturn refuse(ufmt.Errorf(\"denom %s is accounted as %s on route %s\", accDenom, acc.Config.Kind, rt.Key))\n\t}\n\t// CheckCredit refuses a denom not enabled on the route: acc is set from\n\t// here.\n\tif err := rt.CheckCredit(accDenom, amount); err != nil {\n\t\treturn refuse(err)\n\t}\n\n\t// No refusal from here: book the credit.\n\tif kind == controls.KindMint {\n\t\tif !l.store.HasDenom(accDenom) {\n\t\t\tl.store.SetDenom(0, rlm, denom)\n\t\t}\n\t\ttransfer.EmitDenom(0, rlm, denom.HashHex(), string(denom.MarshalJSON()))\n\t\t// The first receive creates the voucher with the decimals governance\n\t\t// configured; a later configuration cannot change them (GovApply).\n\t\tl.store.GetOrCreateVoucher(0, rlm, denom.Base, accDenom, acc.Config.Decimals)\n\t}\n\tif reason == \"\" \u0026\u0026 isBridgeRealm(receiver) {\n\t\treason = \"bridge realm\"\n\t}\n\tepoch, _ := ibcgov.PayoutsEpoch(rt.Key)\n\tlock := l.store.Escrowed(rt.Key, accDenom)\n\tkey := controls.PacketKey(destinationClient, sequence)\n\tswept, err = rt.Credit(now(), chainHeight(), epoch, key, accDenom, receiver, amount, controls.ByPacket, reason, lock)\n\tif err != nil {\n\t\t// CheckCredit passed and the key is the receipt's: a failure here is\n\t\t// a broken invariant, which must abort the receive.\n\t\tpanic(err)\n\t}\n\tack := types.NewResultAppAcknowledgement([]byte{byte(1)})\n\tbooked := \"claim\"\n\tif swept {\n\t\tbooked = \"sweep\"\n\t}\n\ttransfer.EmitPacket(0, rlm,\n\t\ttransfer.AttributeKeySender, data.Sender,\n\t\ttransfer.AttributeKeyReceiver, data.Receiver,\n\t\ttransfer.AttributeKeyDenom, token.Denom.Path(),\n\t\ttransfer.AttributeKeyAmount, token.Amount,\n\t\ttransfer.AttributeKeyMemo, data.Memo,\n\t\ttransfer.AttributeKeyAckSuccess, strconv.FormatBool(ack.Success()),\n\t)\n\ttransfer.EmitRoute(0, rlm, \"credit\", \"route\", rt.Key, \"packet\", key, \"denom\", accDenom,\n\t\t\"receiver\", string(receiver), \"amount\", u(amount), \"booked\", booked)\n\treturn types.RecvPacketResult{Status: types.PacketStatus_Success, Acknowledgement: ack.MarshalJSON()}\n}\n"},{"name":"render.gno","body":"package impl\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/aib/jsonmd/v0\"\n\n\t\"gno.land/p/nt/mux/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\timplv0 \"gno.land/r/aib/ibc/apps/transfer/impl/v0\"\n)\n\n// Render serves impl/v0's routes and the route controls' JSON for monitors\n// and tooling (ADR pr6375 §7.11), every JSON page inert in Markdown\n// (jsonmd.Inert: an owed sweep entry keeps a raw, attacker-chosen receiver):\n//\n//\troutes                       every route\n//\troutes/{route}               a route: client, fee collector, refused\n//\t                             recipients, and per denom its configuration,\n//\t                             meters and accounting terms\n//\troutes/{route}/claims        claims by packet key\n//\troutes/{route}/sweeps        sweep entries by packet key\n//\troutes/{route}/sends         send records with their outcomes, by packet key\n//\troutes/{route}/losses        the loss ledger, by direction and packet key\n//\n// Each of the last four is a page {\"entries\":[...],\"next\":\u003ccursor\u003e}: pass\n// next as ?from= for the following page (\"\" when there is none).\nfunc (l *logic) Render(path string) string {\n\trouter := mux.NewRouter()\n\trouter.HandleFunc(\"\", l.home)\n\trouter.HandleFunc(\"routes\", func(w *mux.ResponseWriter, r *mux.Request) { w.Write(l.reader().RoutesJSON()) })\n\trouter.HandleFunc(\"routes/{route}\", func(w *mux.ResponseWriter, r *mux.Request) {\n\t\tw.Write(jsonmd.Inert(l.reader().RouteJSON(r.GetVar(\"route\"))))\n\t})\n\trouter.HandleFunc(\"routes/{route}/claims\", func(w *mux.ResponseWriter, r *mux.Request) {\n\t\tw.Write(jsonmd.Inert(l.reader().ClaimsJSON(r.GetVar(\"route\"), r.Query.Get(\"from\"))))\n\t})\n\trouter.HandleFunc(\"routes/{route}/sweeps\", func(w *mux.ResponseWriter, r *mux.Request) {\n\t\tw.Write(jsonmd.Inert(l.reader().SweepsJSON(r.GetVar(\"route\"), r.Query.Get(\"from\"))))\n\t})\n\trouter.HandleFunc(\"routes/{route}/sends\", func(w *mux.ResponseWriter, r *mux.Request) {\n\t\tw.Write(jsonmd.Inert(l.reader().SendsJSON(r.GetVar(\"route\"), r.Query.Get(\"from\"))))\n\t})\n\trouter.HandleFunc(\"routes/{route}/losses\", func(w *mux.ResponseWriter, r *mux.Request) {\n\t\tw.Write(jsonmd.Inert(l.reader().LossesJSON(r.GetVar(\"route\"), r.Query.Get(\"from\"))))\n\t})\n\timplv0.NewRenderer(l.store).Register(router)\n\treturn router.Render(path)\n}\n\n// home is impl/v0's home page with the routes under control.\nfunc (l *logic) home(w *mux.ResponseWriter, r *mux.Request) {\n\tvar out strings.Builder\n\tv0 := mux.NewRouter()\n\trd := implv0.NewRenderer(l.store)\n\trd.Register(v0)\n\tout.WriteString(l.routeLinks(rd, v0.Render(\"\")))\n\tout.WriteString(\"\\n## Ethereum routes\\n\\n\")\n\tkeys := l.reader().Routes()\n\tif len(keys) == 0 {\n\t\tout.WriteString(\"No route enrolled: Ethereum clients carry no transfer.\\n\")\n\t}\n\tfor _, k := range keys {\n\t\t// Route keys are client ids the core made; denoms are written\n\t\t// escaped all the same, and the JSON route holds everything else.\n\t\tout.WriteString(ufmt.Sprintf(\"- `%s` (client `%s`): denoms %s, see [routes/%s](:routes/%s)\\n\",\n\t\t\tk, l.reader().Client(k), jsonmd.Text(strings.Join(l.reader().Denoms(k), \", \")), k, k))\n\t}\n\tw.Write(out.String())\n}\n\n// routeLinks rewrites impl/v0's \"send back\" links of the vouchers of a route\n// whose client a replacement retired (ADR pr6375 §7.12, Codex review #18):\n// v0 builds them from the voucher's first trace hop, the route key, which\n// names the retired client. They go through the route's current client\n// instead, a closed route's too: a voucher return is exit only, taken while\n// the route awaits its reopening (ADR pr6375 §7.12).\nfunc (l *logic) routeLinks(rd *implv0.Renderer, page string) string {\n\trdr := l.reader()\n\tfor _, d := range rd.DenomsByClientOrder() {\n\t\tkey := d.Trace[0].ClientId\n\t\tcurrent := rdr.Client(key)\n\t\tif current == \"\" || current == key {\n\t\t\tcontinue\n\t\t}\n\t\tibcDenom := d.IBCDenom()\n\t\told := ufmt.Sprintf(\"[send back](%s) via `%s`\", implv0.NewTransferLink(key, \"\", ibcDenom).URL(), key)\n\t\trepl := ufmt.Sprintf(\"[send back](%s) via `%s`\", implv0.NewTransferLink(current, \"\", ibcDenom).URL(), current)\n\t\tpage = strings.ReplaceAll(page, old, repl)\n\t}\n\treturn page\n}\n"},{"name":"send.gno","body":"package impl\n\nimport (\n\t\"chain\"\n\t\"chain/runtime/unsafe\"\n\t\"math\"\n\t\"math/overflow\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/aib/encoding/ethaddr/v0\"\n\t\"gno.land/p/aib/ibc/ics20/abi/v0\"\n\t\"gno.land/p/aib/ibc/ics20/controls/v0\"\n\t\"gno.land/p/aib/ibc/ics20/v0\"\n\t\"gno.land/p/aib/ibc/lightclient/ethereum/v0\"\n\t\"gno.land/p/aib/ibc/types/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/aib/ibc/apps/transfer\"\n\timplv0 \"gno.land/r/aib/ibc/apps/transfer/impl/v0\"\n\t\"gno.land/r/aib/ibc/core\"\n\t\"gno.land/r/aib/ibc/ibcgov\"\n\t\"gno.land/r/nt/grc20reg/v1\"\n)\n\n// isEthereum reports whether clientID is an Ethereum client of the core: the\n// clients the route controls cover.\nfunc isEthereum(clientID string) bool {\n\treturn core.ClientType(clientID) == ethereum.ClientType\n}\n\n// routeOf resolves a client: covered is false for a client the controls do\n// not cover (impl/v0's path); otherwise rt is the route whose current client\n// it is, or nil for an Ethereum client no enrolled route has as its current\n// client, which carries no transfer.\nfunc (l *logic) routeOf(_ int, rlm realm, clientID string) (rt *controls.Route, covered bool) {\n\tif !isEthereum(clientID) {\n\t\treturn nil, false\n\t}\n\treturn l.registry(0, rlm).RouteOf(clientID), true\n}\n\n// u is an amount in decimal digits (ufmt takes no uint256).\nfunc u(v uint256) string { return strconv.FormatUint256(v, 10) }\n\nfunc mustRoute(rt *controls.Route, clientID string) *controls.Route {\n\tif rt == nil {\n\t\tpanic(ufmt.Sprintf(\"client %s is an Ethereum client of no enrolled route: it carries no transfer\", clientID))\n\t}\n\treturn rt\n}\n\n// toWire rewrites the first hop of a voucher's trace from the route key to the\n// route's current client id: the denom a counterparty knows.\nfunc toWire(d ics20.Denom, rt *controls.Route) ics20.Denom {\n\tif d.HasPrefix(transfer.PortID, rt.Key) {\n\t\ttrace := append([]ics20.Hop{ics20.NewHop(transfer.PortID, rt.Client)}, d.Trace[1:]...)\n\t\treturn ics20.Denom{Base: d.Base, Trace: trace}\n\t}\n\treturn d\n}\n\n// toInternal rewrites the first hop from the current client id to the route\n// key: the denom this chain keys its vouchers by.\nfunc toInternal(d ics20.Denom, rt *controls.Route) ics20.Denom {\n\tif d.HasPrefix(transfer.PortID, rt.Client) {\n\t\ttrace := append([]ics20.Hop{ics20.NewHop(transfer.PortID, rt.Key)}, d.Trace[1:]...)\n\t\treturn ics20.Denom{Base: d.Base, Trace: trace}\n\t}\n\treturn d\n}\n\n// checkRecipient applies the send-side recipient filters (ADR pr6375 §7.8):\n// the receiver is an Ethereum address, not the zero address, not one reserved\n// at or below 0xffff, not one of the route's refused bridge contracts and not\n// the token itself.\nfunc checkRecipient(rt *controls.Route, receiver, token string) {\n\ta, err := ethaddr.Parse(receiver)\n\tif err != nil {\n\t\tpanic(\"the receiver is \" + err.Error())\n\t}\n\tswitch {\n\tcase a.IsZero():\n\t\tpanic(\"the receiver is the zero address\")\n\tcase a.Reserved():\n\t\tpanic(\"the receiver is a reserved address (at or below 0xffff)\")\n\tcase strings.EqualFold(a.Lower(), token):\n\t\tpanic(\"the receiver is the token itself\")\n\tcase rt.IsRefused(a.Lower()):\n\t\tpanic(\"the receiver is a contract of the bridge\")\n\t}\n}\n\n// Transfer is impl/v0's Transfer for clients the controls do not cover. On a\n// route it applies the send-side controls, builds an ABI packet, sends it and\n// writes the send record. Every amount on the route is bounded by\n// abi.MaxAmount, the most Ethereum's transfer app takes (ADR pr6375 E6): a\n// send above it would only come back refused, so it is refused here.\nfunc (l *logic) Transfer(_ int, rlm realm, sent []chain.Coin, clientID, receiver, denom string, amount uint256, timeoutTimestamp uint64, memo string) (\n\tpacket types.MsgSendPacket, sequence uint64,\n) {\n\trt, covered := l.routeOf(0, rlm, clientID)\n\tif !covered {\n\t\treturn l.Logic.Transfer(0, rlm, sent, clientID, receiver, denom, amount, timeoutTimestamp, memo)\n\t}\n\trt = mustRoute(rt, clientID)\n\tif amount == 0 {\n\t\tpanic(\"the amount must be positive\")\n\t}\n\tif amount \u003e abi.MaxAmount {\n\t\tpanic(\"the amount is above Ethereum's maximum of 2^96 - 1: \" + u(amount))\n\t}\n\tsender := unsafe.OriginCaller()\n\tvar (\n\t\twireDenom string // the denom path in the packet\n\t\taccDenom  string // the accounting denom of the send record\n\t\tkind      string\n\t\tfee       uint256\n\t)\n\tswitch {\n\tcase strings.HasPrefix(denom, \"ibc/\"):\n\t\t// A voucher returning to Ethereum: burnt in OnSendPacket; never\n\t\t// limited, charged or paused, and taken on a route a replacement\n\t\t// closed too (exit only, ADR pr6375 §7.12): it lowers the supply\n\t\t// and adds no exposure.\n\t\tif len(sent) \u003e 0 {\n\t\t\tpanic(\"voucher transfer must not include native coins\")\n\t\t}\n\t\tif l.store.VoucherToken(denom) == nil {\n\t\t\tpanic(ufmt.Errorf(\"voucher token not found for denom %s\", denom))\n\t\t}\n\t\td, found := l.store.Denom(denom)\n\t\tif !found {\n\t\t\tpanic(ufmt.Sprintf(\"denom %s not found in store\", denom))\n\t\t}\n\t\tif !d.HasPrefix(transfer.PortID, rt.Key) {\n\t\t\tpanic(ufmt.Sprintf(\"voucher %s did not come over route %s\", denom, rt.Key))\n\t\t}\n\t\t// Every send on a route is recorded, a return too, so its refund is\n\t\t// metered and held like any other (ADR pr6375 §7.13). The staged\n\t\t// activation enabled every denom with vouchers in circulation.\n\t\tacc := rt.Denom(denom)\n\t\tif acc == nil {\n\t\t\tpanic(ufmt.Sprintf(\"denom %s is not enabled on route %s\", denom, rt.Key))\n\t\t}\n\t\tif acc.Config.Kind != controls.KindMint {\n\t\t\tpanic(ufmt.Sprintf(\"denom %s is accounted as %s on route %s\", denom, acc.Config.Kind, rt.Key))\n\t\t}\n\t\tcheckRecipient(rt, receiver, strings.ToLower(d.Base))\n\t\twireDenom = toWire(d, rt).Path()\n\t\tl.store.SetPendingVoucherSend(0, rlm, sender.String(), clientID, wireDenom, amount)\n\t\taccDenom, kind = denom, controls.KindMint\n\tcase grc20reg.Get(denom) != nil:\n\t\tif len(sent) \u003e 0 {\n\t\t\tpanic(\"GRC20 transfer must not include native coins\")\n\t\t}\n\t\talias := transfer.GRC20Alias(denom)\n\t\tacc := l.mustSendable(rt, alias, amount)\n\t\tcheckRecipient(rt, receiver, acc.Config.Remote)\n\t\tfee = acc.Config.Fee\n\t\tif fee \u003e 0 {\n\t\t\tif err := l.store.EscrowGRC20(0, rlm, rt.Key, denom, sender, fee); err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\tif err := l.store.UnescrowGRC20(0, rlm, rt.Key, string(rt.FeeCollector), alias, fee); err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t}\n\t\tl.store.SetPendingGRC20Send(0, rlm, sender.String(), clientID, alias, amount)\n\t\twireDenom, accDenom, kind = alias, alias, controls.KindUnescrow\n\tcase strings.Contains(denom, \"/\"):\n\t\tpanic(ufmt.Sprintf(\"GRC20 token %s not found in grc20reg\", denom))\n\tdefault:\n\t\tacc := l.mustSendable(rt, denom, amount)\n\t\tcheckRecipient(rt, receiver, acc.Config.Remote)\n\t\tfee = acc.Config.Fee\n\t\t// A native coin is an int64: the amount plus the route fee must fit\n\t\t// one, or no coin sent could match it.\n\t\ttotal, ok := overflow.Addu256(amount, fee)\n\t\tif !ok || total \u003e math.MaxInt64 {\n\t\t\tpanic(\"the amount plus the route fee is above the chain's coin maximum\")\n\t\t}\n\t\tif len(sent) != 1 || sent[0].Denom != denom || transfer.CoinAmount(sent[0]) != total {\n\t\t\tpanic(\"send exactly \" + u(total) + denom + \": the amount plus the route fee of \" + u(fee))\n\t\t}\n\t\tl.store.SetPendingNativeEscrow(0, rlm, chain.NewCoin(denom, int64(amount)))\n\t\tif fee \u003e 0 {\n\t\t\t// The fee landed here with the amount: book it to the route and\n\t\t\t// pay it to the collector, so the escrow accounting stays exact.\n\t\t\tl.store.AddEscrow(0, rlm, rt.Key, denom, fee)\n\t\t\tif err := l.store.UnescrowNative(0, rlm, rt.Key, string(rt.FeeCollector), denom, fee); err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t}\n\t\twireDenom, accDenom, kind = denom, denom, controls.KindUnescrow\n\t}\n\n\tdata := ics20.NewFungibleTokenPacketData(wireDenom, u(amount), sender.String(), receiver, memo)\n\tvalue, err := abi.Marshal(data)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tpayload := types.Payload{\n\t\tSourcePort: transfer.PortID, DestinationPort: transfer.PortID,\n\t\tEncoding: abi.Encoding, Value: value, Version: transfer.V1,\n\t}\n\tpacket = types.MsgSendPacket{SourceClient: clientID, Payloads: []types.Payload{payload}, TimeoutTimestamp: timeoutTimestamp}\n\tsequence = core.SendPacket(cross(rlm), packet)\n\n\t// The send record (ADR pr6375 §7.13), in the transaction of the send.\n\tfull := types.NewPacket(sequence, clientID, core.CounterpartyClientID(clientID), timeoutTimestamp, payload)\n\terr = rt.RecordSend(controls.SendRecord{\n\t\tKey: controls.PacketKey(clientID, sequence), Packet: full, Commitment: types.CommitPacket(full),\n\t\tSender: sender, Receiver: receiver, Denom: accDenom, Kind: kind, Amount: amount, Fee: fee,\n\t\tSentHeight: chainHeight(), SentAt: now(),\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn packet, sequence\n}\n\n// mustSendable is the accounting of a gno-native denom a send may carry on\n// rt: enabled, not paused by a sends flag, within the send limits.\nfunc (l *logic) mustSendable(rt *controls.Route, denom string, amount uint256) *controls.Denom {\n\tacc := rt.Denom(denom)\n\tif acc == nil || acc.Config.Kind != controls.KindUnescrow {\n\t\tpanic(ufmt.Sprintf(\"denom %s is not enabled for sends on route %s\", denom, rt.Key))\n\t}\n\tif rt.IsClosed() {\n\t\tpanic(ufmt.Sprintf(\"route %s is closed until a governed reopening: it carries no gno-native send\", rt.Key))\n\t}\n\tif rt.ExposureClosed() {\n\t\tpanic(ufmt.Sprintf(\"route %s has a loss without a restoring decision: it takes no gno-native send\", rt.Key))\n\t}\n\tif !rt.Bridged() {\n\t\tpanic(ufmt.Sprintf(\"route %s has no protected bridge contracts set: it takes no gno-native send\", rt.Key))\n\t}\n\tif ibcgov.Flagged(rt.Key, ibcgov.FnSends) {\n\t\tpanic(ufmt.Sprintf(\"sends are paused on route %s\", rt.Key))\n\t}\n\tif err := acc.CheckSend(amount); err != nil {\n\t\tpanic(err)\n\t}\n\treturn acc\n}\n\n// OnSendPacket is impl/v0's for clients the controls do not cover. On a route\n// it takes what Transfer authorized: it burns a returning voucher (its\n// internal denom keyed by the route key) or escrows a gno-native token under\n// the route key.\nfunc (l *logic) OnSendPacket(_ int, rlm realm, sourceClient, destinationClient string, sequence uint64, payload types.Payload) error {\n\trt, covered := l.routeOf(0, rlm, sourceClient)\n\tif !covered {\n\t\treturn l.Logic.OnSendPacket(0, rlm, sourceClient, destinationClient, sequence, payload)\n\t}\n\tif rt == nil {\n\t\treturn ufmt.Errorf(\"client %s is an Ethereum client of no enrolled route\", sourceClient)\n\t}\n\tif err := validatePayload(sourceClient, destinationClient, payload); err != nil {\n\t\treturn err\n\t}\n\tdata, token, err := unmarshalPayload(payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\tsigner := unsafe.OriginCaller().String()\n\tif data.Sender != signer {\n\t\treturn ufmt.Errorf(\"invalid FungibleTokenPacketData: sender %s is different from signer %s\", data.Sender, signer)\n\t}\n\tif transfer.IsBlocked(address(data.Sender)) {\n\t\treturn ufmt.Errorf(\"sender %s is blocked\", data.Sender)\n\t}\n\tif err := implv0.ValidateBaseDenomNoSlash(token); err != nil {\n\t\treturn err\n\t}\n\tamount, err := token.AmountUint256()\n\tif err != nil {\n\t\treturn err\n\t}\n\tswitch {\n\tcase token.Denom.HasPrefix(payload.SourcePort, sourceClient):\n\t\texpected, ok := l.store.ConsumePendingVoucherSend(0, rlm)\n\t\tif !ok {\n\t\t\treturn ufmt.Errorf(\"voucher packet must be initiated through transfer.Transfer\")\n\t\t}\n\t\tif err := implv0.ValidatePendingSend(expected, data.Sender, sourceClient, token.Denom.Path(), amount); err != nil {\n\t\t\treturn ufmt.Errorf(\"voucher packet %v\", err)\n\t\t}\n\t\tinternal := toInternal(token.Denom, rt).IBCDenom()\n\t\tif err := l.store.BurnVoucher(0, rlm, internal, address(data.Sender), amount); err != nil {\n\t\t\treturn ufmt.Errorf(\"burn voucher %s error: %v\", internal, err)\n\t\t}\n\tcase transfer.IsGRC20Alias(token.Denom.Base):\n\t\texpected, ok := l.store.ConsumePendingGRC20Send(0, rlm)\n\t\tif !ok {\n\t\t\treturn ufmt.Errorf(\"GRC20 packet must be initiated through transfer.Transfer\")\n\t\t}\n\t\tif err := implv0.ValidatePendingSend(expected, data.Sender, sourceClient, token.Denom.Path(), amount); err != nil {\n\t\t\treturn ufmt.Errorf(\"GRC20 packet %v\", err)\n\t\t}\n\t\tdenomKey := transfer.ResolveGRC20Alias(token.Denom.Base)\n\t\tif err := l.store.EscrowGRC20(0, rlm, rt.Key, denomKey, address(data.Sender), amount); err != nil {\n\t\t\treturn err\n\t\t}\n\tdefault:\n\t\texpected, ok := l.store.PendingNativeEscrow()\n\t\tif !ok {\n\t\t\treturn ufmt.Errorf(\"native packet must be initiated through transfer.Transfer\")\n\t\t}\n\t\tcoin, err := token.ToCoin()\n\t\tif err != nil {\n\t\t\treturn ufmt.Errorf(\"token to coin error: %v\", err)\n\t\t}\n\t\tif coin.Denom != expected.Denom || coin.Amount != expected.Amount {\n\t\t\treturn ufmt.Errorf(\"escrowed coin %s is not equal to fungible packet data token %s\", expected, coin)\n\t\t}\n\t\tl.store.AddEscrow(0, rlm, rt.Key, coin.Denom, transfer.CoinAmount(coin))\n\t}\n\ttransfer.EmitTransfer(0, rlm, data.Sender, data.Receiver, token.Denom.Path(), token.Amount, data.Memo)\n\treturn nil\n}\n"},{"name":"util.gno","body":"package impl\n\nimport (\n\t\"gno.land/p/aib/ibc/ics20/abi/v0\"\n\t\"gno.land/p/aib/ibc/ics20/v0\"\n\t\"gno.land/p/aib/ibc/types/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/aib/ibc/apps/transfer\"\n)\n\n// validatePayload enforces the transfer port on both sides, the local client\n// id in the {string}-{number} format and the Ethereum router's client id in\n// its own grammar (types.IsValidRouterClientID: \"client-N\" or a custom id\n// such as \"gnoland\"), the ICS20 version and a known encoding.\nfunc validatePayload(localClient, routerClient string, payload types.Payload) error {\n\tif payload.SourcePort != transfer.PortID || payload.DestinationPort != transfer.PortID {\n\t\treturn ufmt.Errorf(\"payload port ID is invalid: expected %s, got sourcePort: %s destPort: %s\", transfer.PortID, payload.SourcePort, payload.DestinationPort)\n\t}\n\tif !types.IsValidClientID(localClient) {\n\t\treturn ufmt.Errorf(\"client ID %s must be in valid format: {string}-{number}\", localClient)\n\t}\n\tif !types.IsValidRouterClientID(routerClient) {\n\t\treturn ufmt.Errorf(\"client ID %s is no Ethereum router client id\", routerClient)\n\t}\n\tif payload.Version != transfer.V1 {\n\t\treturn ufmt.Errorf(\"invalid ICS20 version: expected %s, got %s\", transfer.V1, payload.Version)\n\t}\n\tif payload.Encoding != transfer.EncodingProtobuf \u0026\u0026 payload.Encoding != abi.Encoding {\n\t\treturn ufmt.Errorf(\"invalid encoding: expected %s or %s, got %s\", transfer.EncodingProtobuf, abi.Encoding, payload.Encoding)\n\t}\n\treturn nil\n}\n\n// unmarshalPayload decodes the packet data of payload, sent or received on\n// an Ethereum route, in its encoding.\nfunc unmarshalPayload(payload types.Payload) (ics20.FungibleTokenPacketData, ics20.Token, error) {\n\tvar (\n\t\tdata ics20.FungibleTokenPacketData\n\t\terr  error\n\t)\n\tif payload.Encoding == abi.Encoding {\n\t\tdata, err = abi.Unmarshal(payload.Value)\n\t} else {\n\t\terr = data.ProtoUnmarshal(payload.Value)\n\t}\n\tif err != nil {\n\t\treturn data, ics20.Token{}, ufmt.Errorf(\"decoding FungibleTokenPacketData: %v\", err)\n\t}\n\t// An Ethereum route's data is validated by its own rules only\n\t// (ValidateBasicEthereum), not the SDK's first: a router client id hop\n\t// of more than 64 characters, or a blank receiver (an owed sweep), is\n\t// valid here.\n\tdenom, err := data.ValidateBasicEthereum()\n\tif err != nil {\n\t\treturn data, ics20.Token{}, ufmt.Errorf(\"invalid FungibleTokenPacketData: %v\", err)\n\t}\n\ttoken := ics20.Token{Denom: denom, Amount: data.Amount}\n\treturn data, token, nil\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"GrVhbsVkNltLfjjw8+4W31I6tsN/onfKmN4JXASvAiBPFarhCAgJ9mQfgLZqQOY63tCuE6PzPue2ZYqKBQyyGw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"impl","path":"gno.land/r/aib/ibc/core/impl/v0","files":[{"name":"client.gno","body":"package impl\n\nimport (\n\t\"chain/runtime/unsafe\"\n\t\"errors\"\n\n\t\"gno.land/p/aib/ibc/host/v0\"\n\t\"gno.land/p/aib/ibc/lightclient/ethereum/v0\"\n\t\"gno.land/p/aib/ibc/lightclient/tendermint/v0\"\n\t\"gno.land/p/aib/ibc/lightclient/v0\"\n\t\"gno.land/p/aib/ibc/types/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/aib/ibc/core\"\n)\n\nfunc (l *logic) mustClient(clientID string) core.Client {\n\tc := l.store.Client(clientID)\n\tif c == nil {\n\t\tpanic(ufmt.Sprintf(\"client %s not found\", clientID))\n\t}\n\treturn c\n}\n\n// checkStates refuses client and consensus states that are not the light\n// client packages' own concrete types, by value, before any of their methods\n// runs: a caller-supplied implementation would run its own code in the\n// core's realm context. Whether the two belong to the same client type is\n// CreateClient's next check, on our own types' methods.\nfunc checkStates(cs lightclient.ClientState, cons lightclient.ConsensusState) error {\n\tswitch cs.(type) {\n\tcase tendermint.ClientState, ethereum.ClientState:\n\tdefault:\n\t\treturn errors.New(\"the client state must be a light client's own type\")\n\t}\n\tswitch cons.(type) {\n\tcase tendermint.ConsensusState, ethereum.ConsensusState:\n\tdefault:\n\t\treturn errors.New(\"the consensus state must be a light client's own type\")\n\t}\n\treturn nil\n}\n\n// checkClientMessage refuses a client message that is not a light client\n// package's own concrete type, before any of its methods runs.\nfunc checkClientMessage(m lightclient.ClientMessage) error {\n\tswitch m.(type) {\n\tcase *tendermint.MsgHeader, *tendermint.Misbehaviour, *ethereum.Header, *ethereum.Misbehaviour:\n\t\treturn nil\n\t}\n\treturn errors.New(\"the client message must be a light client's own type\")\n}\n\n// CreateClient initializes the light client of the given type with the\n// states and records relayer as its creator.\nfunc (l *logic) CreateClient(_ int, rlm realm, relayer address, clientState lightclient.ClientState, consensusState lightclient.ConsensusState) string {\n\tif err := checkStates(clientState, consensusState); err != nil {\n\t\tpanic(err)\n\t}\n\tif clientState.ClientType() != consensusState.ClientType() {\n\t\tpanic(\"client type for client state and consensus state do not match\")\n\t}\n\tif err := types.ValidateClientType(clientState.ClientType()); err != nil {\n\t\tpanic(ufmt.Sprintf(\n\t\t\t\"client type does not meet naming constraints: %v\", err,\n\t\t))\n\t}\n\n\tif err := clientState.ValidateBasic(); err != nil {\n\t\tpanic(err)\n\t}\n\tif err := consensusState.ValidateBasic(); err != nil {\n\t\tpanic(err)\n\t}\n\tvar newLightClient func() lightclient.Interface\n\tswitch typ := clientState.ClientType(); typ {\n\tcase lightclient.Tendermint:\n\t\tnewLightClient = tendermint.NewLightClient\n\tcase ethereum.ClientType:\n\t\tnewLightClient = ethereum.NewLightClient\n\tdefault:\n\t\tpanic(\"unhandled light client type \" + typ)\n\t}\n\tc := l.store.AddClient(0, rlm, clientState.ClientType(), relayer, newLightClient)\n\tlc := c.LightClient()\n\terr := lc.Initialize(clientState, consensusState)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif status := lc.Status(); status != lightclient.Active {\n\t\tpanic(ufmt.Sprintf(\"cannot create client (%s) with status %s\", c.ID(), status))\n\t}\n\tcore.EmitCreateClient(0, rlm, c.ID(), c.Type(), lc.LatestHeight().String())\n\treturn c.ID()\n}\n\n// RegisterCounterparty registers the IBC v2 counterparty info for the given\n// clientID. It must be called by the same relayer that called CreateClient.\nfunc (l *logic) RegisterCounterparty(_ int, rlm realm, clientID string, counterpartyMerklePrefix [][]byte, counterpartyClientID string) {\n\t// An Ethereum client's counterparty is a client of the ICS26 router,\n\t// whose ids follow its own grammar (\"client-N\", or a custom id such as\n\t// \"gnoland\"); every other counterparty is an SDK chain's {type}-{N}.\n\tif !types.IsValidClientID(counterpartyClientID) \u0026\u0026 !types.IsValidRouterClientID(counterpartyClientID) {\n\t\tpanic(\"invalid counterparty client id\")\n\t}\n\tc := l.mustClient(clientID)\n\tvalid := types.IsValidClientID(counterpartyClientID)\n\tif c.Type() == ethereum.ClientType {\n\t\tvalid = types.IsValidRouterClientID(counterpartyClientID)\n\t\t// An Ethereum router keeps its commitments at the bare ICS24 keys:\n\t\t// eureka's canonical prefix is a single empty element. Any other\n\t\t// would move every proof of the client off the router's state.\n\t\tif len(counterpartyMerklePrefix) != 1 || len(counterpartyMerklePrefix[0]) != 0 {\n\t\t\tpanic(\"an Ethereum counterparty's merkle prefix is a single empty element\")\n\t\t}\n\t}\n\tif !valid {\n\t\tpanic(\"invalid counterparty client id\")\n\t}\n\tcaller := unsafe.OriginCaller()\n\tif c.Creator() != caller {\n\t\tpanic(ufmt.Sprintf(\"expected same signer as CreateClient submitter %s, got %s\", c.Creator(), caller))\n\t}\n\tif c.CounterpartyClientID() != \"\" {\n\t\tpanic(\"cannot register counterparty once it is already set\")\n\t}\n\tc.SetCounterparty(0, rlm, counterpartyClientID, counterpartyMerklePrefix)\n}\n\n// UpdateClient updates the given light client with a new header, or submits a\n// misbehaviour (clientMessage can be either).\nfunc (l *logic) UpdateClient(_ int, rlm realm, clientID string, clientMessage lightclient.ClientMessage) {\n\tif err := checkClientMessage(clientMessage); err != nil {\n\t\tpanic(err)\n\t}\n\tif err := clientMessage.ValidateBasic(); err != nil {\n\t\tpanic(err)\n\t}\n\tc := l.mustClient(clientID)\n\tif c.Type() != clientMessage.ClientType() {\n\t\tpanic(\"client type for client state and client message do not match\")\n\t}\n\tlc := c.LightClient()\n\tif status := lc.Status(); status != lightclient.Active {\n\t\tpanic(ufmt.Sprintf(\"cannot update client (%s) with status %s\", c.ID(), status))\n\t}\n\n\terr := lc.VerifyClientMessage(clientMessage)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfoundMisbehavior := lc.CheckForMisbehaviour(clientMessage)\n\tif foundMisbehavior {\n\t\tlc.UpdateStateOnMisbehaviour(clientMessage)\n\t\tcore.EmitSubmitMisbehaviour(0, rlm, c.ID(), c.Type())\n\t\treturn\n\t}\n\n\tconsensusHeights := lc.UpdateState(clientMessage)\n\n\tvar consensusHeightsStr string\n\tfor i, h := range consensusHeights {\n\t\tconsensusHeightsStr += h.String()\n\t\tif i \u003c len(consensusHeights)-1 {\n\t\t\tconsensusHeightsStr += \", \"\n\t\t}\n\t}\n\tcore.EmitUpdateClient(0, rlm, c.ID(), c.Type(), consensusHeightsStr)\n}\n\n// UpgradeClient upgrades the client to a new client and consensus state,\n// verified by proofs that the counterparty chain committed to those states\n// at its UpgradePath under the current client's latest consensus root.\nfunc (l *logic) UpgradeClient(_ int, rlm realm, clientID string, clientState, consensusState, proofUpgradeClient, proofUpgradeConsensusState any) {\n\tif err := host.ClientIdentifierValidator(clientID); err != nil {\n\t\tpanic(err)\n\t}\n\tc := l.mustClient(clientID)\n\tlc := c.LightClient()\n\tif status := lc.Status(); status != lightclient.Active {\n\t\tpanic(ufmt.Sprintf(\"cannot upgrade client (%s) with status %s\", c.ID(), status))\n\t}\n\tif err := lc.VerifyUpgradeAndUpdateState(clientState, consensusState, proofUpgradeClient, proofUpgradeConsensusState); err != nil {\n\t\tpanic(err)\n\t}\n\tcore.EmitUpgradeClient(0, rlm, c.ID(), c.Type(), lc.LatestHeight().String())\n}\n\n// RecoverClient recovers a frozen or expired subject client using a healthy\n// substitute client that tracks the same counterparty chain.\nfunc (l *logic) RecoverClient(_ int, rlm realm, subjectClientID, substituteClientID string) {\n\tif subjectClientID == substituteClientID {\n\t\tpanic(\"subject and substitute client IDs must differ\")\n\t}\n\tsubject := l.store.Client(subjectClientID)\n\tif subject == nil {\n\t\tpanic(ufmt.Sprintf(\"subject client %s not found\", subjectClientID))\n\t}\n\tsubstitute := l.store.Client(substituteClientID)\n\tif substitute == nil {\n\t\tpanic(ufmt.Sprintf(\"substitute client %s not found\", substituteClientID))\n\t}\n\tif subject.Type() != substitute.Type() {\n\t\tpanic(ufmt.Sprintf(\n\t\t\t\"subject client type %s does not match substitute client type %s\",\n\t\t\tsubject.Type(), substitute.Type(),\n\t\t))\n\t}\n\tif status := subject.LightClient().Status(); status != lightclient.Frozen \u0026\u0026 status != lightclient.Expired {\n\t\tpanic(ufmt.Sprintf(\n\t\t\t\"cannot recover subject client %s with status %s\",\n\t\t\tsubject.ID(), status,\n\t\t))\n\t}\n\tif status := substitute.LightClient().Status(); status != lightclient.Active {\n\t\tpanic(ufmt.Sprintf(\n\t\t\t\"substitute client %s must be Active, got %s\",\n\t\t\tsubstitute.ID(), status,\n\t\t))\n\t}\n\tif err := subject.LightClient().RecoverClient(substitute.LightClient()); err != nil {\n\t\tpanic(err)\n\t}\n\tcore.EmitRecoverClient(0, rlm, subject.ID(), substitute.ID(), subject.Type())\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/aib/ibc/core/impl/v0\"\ngno = \"0.9\"\n"},{"name":"impl.gno","body":"// Package impl is the first implementation of the IBC core lifecycle, behind\n// the gno.land/r/aib/ibc/core proxy (see docs/adrs/0001). It holds no state of\n// its own: it reads and writes the proxy's store through core.Store, emits\n// through the proxy's Emit* wrappers and is always invoked non-crossing by the\n// proxy's dispatchers, so the current realm is the proxy for the whole call\n// and rlm is the proxy's live realm value. It exports nothing that mutates.\npackage impl\n\nimport (\n\t\"gno.land/r/aib/ibc/core\"\n)\n\nconst version = \"v0\"\n\ntype logic struct {\n\tstore core.Store\n}\n\nvar _ core.Logic = (*logic)(nil)\n\n// New builds the implementation against the proxy's store. The proxy calls it\n// through the constructor registered below.\nfunc New(s core.Store) core.Logic {\n\treturn \u0026logic{store: s}\n}\n\nfunc init(cur realm) {\n\tcore.RegisterImpl(cross(cur), New, version, nil) // installs over any\n}\n\nfunc (l *logic) Version() string {\n\treturn version\n}\n\n// OnInstall has nothing to migrate: this is the first implementation and the\n// store it expects is the store the proxy was deployed with.\nfunc (l *logic) OnInstall(_ int, rlm realm, prevPath, prevVersion string) {}\n"},{"name":"packet.gno","body":"package impl\n\nimport (\n\t\"bytes\"\n\t\"encoding/hex\"\n\t\"time\"\n\n\t\"gno.land/p/aib/ibc/lightclient/ethereum/v0\"\n\n\t\"gno.land/p/aib/ibc/host/v0\"\n\t\"gno.land/p/aib/ibc/lightclient/extproof/v0\"\n\t\"gno.land/p/aib/ibc/lightclient/v0\"\n\t\"gno.land/p/aib/ibc/types/v0\"\n\t\"gno.land/p/aib/ics23/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/aib/ibc/core\"\n)\n\nconst maxTimeoutDelta time.Duration = 24 * time.Hour\n\n// validatePacket validates a packet of the local client local: by the ICS26\n// router's client id grammar on the counterparty side when local is an\n// Ethereum client (types.Packet.ValidateBasicRouter), by the SDK's rule\n// otherwise (types.Packet.ValidateBasic), unchanged for every other client.\nfunc (l *logic) validatePacket(p types.Packet, local string) error {\n\tif c := l.store.Client(local); c != nil \u0026\u0026 c.Type() == ethereum.ClientType {\n\t\treturn p.ValidateBasicRouter()\n\t}\n\treturn p.ValidateBasic()\n}\n\nfunc (l *logic) SendPacket(_ int, rlm realm, msg types.MsgSendPacket) (sequence uint64) {\n\tif err := msg.ValidateBasic(); err != nil {\n\t\tpanic(err)\n\t}\n\tclientID := msg.SourceClient\n\tc := l.mustClient(clientID)\n\tlc := c.LightClient()\n\tif status := lc.Status(); status != lightclient.Active {\n\t\tpanic(ufmt.Sprintf(\"client (%s) status is %s\", clientID, status))\n\t}\n\n\t// Ensure counterparty has been registered\n\tcounterpartyClientID := c.CounterpartyClientID()\n\tif counterpartyClientID == \"\" {\n\t\tpanic(ufmt.Sprintf(\"counterparty not found for client %s\", clientID))\n\t}\n\n\t// timeoutTimestamp must be greater than current block time and less or equal\n\t// to current block time + maxTimeoutDelta.\n\tvar (\n\t\ttimeout    = time.Unix(int64(msg.TimeoutTimestamp), 0)\n\t\tminTimeout = time.Now()\n\t\tmaxTimeout = minTimeout.Add(maxTimeoutDelta)\n\t)\n\tif !timeout.After(minTimeout) {\n\t\tpanic(ufmt.Sprintf(\n\t\t\t\"timeout is less than or equal the current block timestamp, %d \u003c= %d\",\n\t\t\tmsg.TimeoutTimestamp, minTimeout.Unix(),\n\t\t))\n\t}\n\tif timeout.After(maxTimeout) {\n\t\tpanic(ufmt.Sprintf(\n\t\t\t\"timeout is after the max allowed timeout, %d \u003e %d\",\n\t\t\tmsg.TimeoutTimestamp, maxTimeout.Unix(),\n\t\t))\n\t}\n\n\t// check if the latest consensus timestamp is lower than timeoutTimestamp\n\tlatestTimestamp, err := lc.TimestampAtHeight(lc.LatestHeight())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif latestTimestamp \u003e= msg.TimeoutTimestamp {\n\t\tpanic(ufmt.Errorf(\"latest timestamp: %d, timeout timestamp: %d\",\n\t\t\tlatestTimestamp, msg.TimeoutTimestamp))\n\t}\n\n\t// construct packet from given fields\n\tsequence = c.AllocateSendSequence(0, rlm)\n\tpacket := types.NewPacket(sequence, msg.SourceClient, counterpartyClientID, msg.TimeoutTimestamp, msg.Payloads...)\n\tif err := l.validatePacket(packet, msg.SourceClient); err != nil {\n\t\tpanic(ufmt.Errorf(\"constructed packet failed basic validation: %v\", err))\n\t}\n\n\t// set the packet commitment\n\tc.SetPacketCommitment(0, rlm, packet)\n\n\t// emit events\n\tcore.EmitSendPacket(0, rlm, packet)\n\n\t// Invoke registed app OnSendPacket() for each payload.SourcePort.\n\tfor i, payload := range msg.Payloads {\n\t\tapp := l.store.Route(payload.SourcePort)\n\t\tif app.Paused {\n\t\t\tpanic(ufmt.Sprintf(\"app %q is paused\", payload.SourcePort))\n\t\t}\n\t\terr := app.OnSendPacket(\n\t\t\tcross(rlm), msg.SourceClient, counterpartyClientID, sequence, payload,\n\t\t)\n\t\tif err != nil {\n\t\t\tpanic(ufmt.Sprintf(\n\t\t\t\t\"send packet failed for payload #%d app %q: %v\", i, payload.SourcePort, err,\n\t\t\t))\n\t\t}\n\t}\n\n\treturn sequence\n}\n\nfunc (l *logic) RecvPacket(_ int, rlm realm, msg types.MsgRecvPacket) types.ResponseResultType {\n\textProof, hasExtProof := extproof.FromExt(msg.Ext)\n\tif hasExtProof {\n\t\t// msg.ValidateBasic requires an ICS23 proof; apply the rest of it.\n\t\tif len(msg.ProofCommitment) != 0 {\n\t\t\tpanic(\"a message cannot carry both an ICS23 proof and an Ext proof\")\n\t\t}\n\t\tif err := l.validatePacket(msg.Packet, msg.Packet.DestinationClient); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t} else if err := msg.ValidateBasic(); err != nil {\n\t\tpanic(err)\n\t}\n\n\tclientID := msg.Packet.DestinationClient\n\tc := l.mustClient(clientID)\n\tlc := c.LightClient()\n\t// check client is active\n\tif status := lc.Status(); status != lightclient.Active {\n\t\tpanic(ufmt.Sprintf(\"client (%s) status is %s\", clientID, status))\n\t}\n\t// check counterparty\n\tif c.CounterpartyClientID() != msg.Packet.SourceClient {\n\t\tpanic(ufmt.Sprintf(\n\t\t\t\"counterparty id (%s) does not match packet source id (%s)\",\n\t\t\tc.CounterpartyClientID(), msg.Packet.SourceClient,\n\t\t))\n\t}\n\tcurrentTimestamp := uint64(time.Now().Unix())\n\tif currentTimestamp \u003e= msg.Packet.TimeoutTimestamp {\n\t\tpanic(ufmt.Sprintf(\n\t\t\t\"current timestamp: %d, timeout timestamp: %d\",\n\t\t\tcurrentTimestamp, msg.Packet.TimeoutTimestamp,\n\t\t))\n\t}\n\n\t// REPLAY PROTECTION: Packet receipts will indicate that a packet has already\n\t// been received.\n\t// Packet receipts must not be pruned, unless it has been marked stale by the\n\t// increase of the recvStartSequence. TODO check relevancy of comment\n\tif c.HasPacketReceipt(msg.Packet.Sequence) {\n\t\t// The receipt identifies the packet (types.CommitPacketReceipt): the\n\t\t// same packet again has already been relayed, which Core IBC treats as\n\t\t// a no-op so that an entire relay transaction does not fail and consume\n\t\t// unnecessary fees. A different packet at a received sequence is\n\t\t// refused, as eureka refuses a different receipt at a sequence.\n\t\tif !bytes.Equal(c.PacketReceipt(msg.Packet.Sequence), types.CommitPacketReceipt(msg.Packet)) {\n\t\t\tpanic(ufmt.Sprintf(\n\t\t\t\t\"a different packet was already received at sequence %d on client %s\",\n\t\t\t\tmsg.Packet.Sequence, clientID,\n\t\t\t))\n\t\t}\n\t\treturn types.RESPONSE_NOOP\n\t}\n\n\t// Verify existence of the commitment bytes in the proofs.\n\tvar (\n\t\tkey        = host.PacketCommitmentKey(msg.Packet.SourceClient, msg.Packet.Sequence)\n\t\tmerklePath = types.BuildMerklePath(c.CounterpartyMerklePrefix(), key)\n\t\tvalue      = types.CommitPacket(msg.Packet)\n\t)\n\tif err := verifyMembership(\n\t\tlc, msg.ProofHeight, msg.ProofCommitment, extProof, hasExtProof, merklePath, value,\n\t); err != nil {\n\t\tpanic(ufmt.Sprintf(\n\t\t\t\"failed packet commitment verification for client (%s): %v\",\n\t\t\tclientID, err,\n\t\t))\n\t}\n\n\t// Set Packet Receipt to prevent timeout from occurring on counterparty\n\tc.SetPacketReceipt(0, rlm, msg.Packet)\n\n\t// Emit events\n\tcore.EmitRecvPacket(0, rlm, msg.Packet)\n\n\tl.writeRecvPacketAcknowledgement(0, rlm, c, msg.Packet)\n\n\treturn types.RESPONSE_SUCCESS\n}\n\n// writeRecvPacketAcknowledgement invokes the app of each payload and writes\n// the resulting acknowledgement, unless the app answers asynchronously.\nfunc (l *logic) writeRecvPacketAcknowledgement(_ int, rlm realm, c core.Client, packet types.Packet) {\n\tvar (\n\t\t// build up the recv results for each application callback.\n\t\tack = types.Acknowledgement{\n\t\t\tAppAcknowledgements: [][]byte{},\n\t\t}\n\t\tisSuccess = true\n\t)\n\n\t// Invoke registed app OnRecvPacket() for each payload.DestinationPort.\n\t//\n\t// Packet.ValidateBasic() guarantees exactly one payload, so the loop never\n\t// runs more than once. It matters: the loop breaks on the first failure\n\t// without reverting the callbacks that already succeeded, so with several\n\t// payloads the packet could commit a partial state change while being\n\t// acknowledged (and refunded by the counterparty) as failed. Any move to\n\t// multi-payload packets has to make this loop atomic first.\n\tfor i, payload := range packet.Payloads {\n\t\tapp := l.store.Route(payload.DestinationPort)\n\t\tif app.Paused {\n\t\t\t// Refused without reaching the app: the counterparty sees the\n\t\t\t// sentinel error acknowledgement and refunds its sender.\n\t\t\tisSuccess = false\n\t\t\tack = types.Acknowledgement{\n\t\t\t\tAppAcknowledgements: [][]byte{types.UniversalErrorAcknowledgement()},\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\tres := app.OnRecvPacket(\n\t\t\tcross(rlm), packet.SourceClient, packet.DestinationClient,\n\t\t\tpacket.Sequence, payload,\n\t\t)\n\t\tif res.Status == types.PacketStatus_Failure {\n\t\t\tisSuccess = false\n\t\t\t// construct acknowledgement with single app acknowledgement that is the\n\t\t\t// sentinel error acknowledgement\n\t\t\tack = types.Acknowledgement{\n\t\t\t\tAppAcknowledgements: [][]byte{types.UniversalErrorAcknowledgement()},\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\tif res.Status == types.PacketStatus_Async {\n\t\t\tif len(packet.Payloads) \u003e 1 {\n\t\t\t\tpanic(\"async ack not supported for multi-payload packets\")\n\t\t\t}\n\t\t\tc.SavePendingAsyncAck(0, rlm, packet, app.PkgPath)\n\t\t\treturn\n\t\t}\n\n\t\t// successful app acknowledgement cannot equal sentinel error\n\t\t// acknowledgement\n\t\tif bytes.Equal(res.Acknowledgement, types.UniversalErrorAcknowledgement()) {\n\t\t\tpanic(ufmt.Sprintf(\n\t\t\t\t\"callback error for payload #%d app %q: application acknowledgement cannot be sentinel error acknowledgement\",\n\t\t\t\ti, payload.DestinationPort,\n\t\t\t))\n\t\t}\n\n\t\t// append app acknowledgement to the overall acknowledgement\n\t\tack.AppAcknowledgements = append(ack.AppAcknowledgements, res.Acknowledgement)\n\t}\n\n\t// Sanity check to ensure returned acknowledgement and calculated isSuccess\n\t// boolean matches\n\tif ack.Success() != isSuccess {\n\t\tpanic(\"acknowledgement success flag mismatch\")\n\t}\n\tif err := ack.Validate(); err != nil {\n\t\tpanic(err)\n\t}\n\t// set the acknowledgement so that it can be verified on the other side\n\tc.SetPacketAcknowledgement(0, rlm, packet.Sequence, types.CommitAcknowledgement(ack))\n\tcore.EmitWriteAck(0, rlm, packet, ack)\n}\n\nfunc (l *logic) WriteAcknowledgement(_ int, rlm realm, clientID string, sequence uint64, ack types.Acknowledgement) {\n\tc := l.mustClient(clientID)\n\n\tpacket, appPkgPath, found := c.PendingAsyncAck(sequence)\n\tif !found {\n\t\tpanic(ufmt.Sprintf(\"no pending async ack for client=%s sequence=%d\", clientID, sequence))\n\t}\n\n\t// rlm is the proxy's live cur, so rlm.Previous() is the proxy's caller.\n\tcallerPath := rlm.Previous().PkgPath()\n\tif callerPath != appPkgPath {\n\t\tpanic(ufmt.Sprintf(\n\t\t\t\"caller %s is not authorized to write ack for sequence %d (expected %s)\",\n\t\t\tcallerPath, sequence, appPkgPath,\n\t\t))\n\t}\n\n\tif c.HasPacketAcknowledgement(sequence) {\n\t\tpanic(ufmt.Sprintf(\"acknowledgement already written for sequence %d\", sequence))\n\t}\n\tif err := ack.Validate(); err != nil {\n\t\tpanic(err)\n\t}\n\n\tc.SetPacketAcknowledgement(0, rlm, sequence, types.CommitAcknowledgement(ack))\n\tc.DeletePendingAsyncAck(0, rlm, sequence)\n\tcore.EmitWriteAck(0, rlm, packet, ack)\n}\n\nfunc (l *logic) Acknowledgement(_ int, rlm realm, msg types.MsgAcknowledgement) types.ResponseResultType {\n\textProof, hasExtProof := extproof.FromExt(msg.Ext)\n\tif hasExtProof {\n\t\t// msg.ValidateBasic requires an ICS23 proof; apply the rest of it.\n\t\tif len(msg.ProofAcked) != 0 {\n\t\t\tpanic(\"a message cannot carry both an ICS23 proof and an Ext proof\")\n\t\t}\n\t\tif err := msg.Acknowledgement.Validate(); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tif err := l.validatePacket(msg.Packet, msg.Packet.SourceClient); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t} else if err := msg.ValidateBasic(); err != nil {\n\t\tpanic(err)\n\t}\n\n\tclientID := msg.Packet.SourceClient\n\tc := l.mustClient(clientID)\n\tlc := c.LightClient()\n\t// check client is active\n\tif status := lc.Status(); status != lightclient.Active {\n\t\tpanic(ufmt.Sprintf(\"client (%s) status is %s\", clientID, status))\n\t}\n\t// check counterparty\n\tif c.CounterpartyClientID() != msg.Packet.DestinationClient {\n\t\tpanic(ufmt.Sprintf(\n\t\t\t\"counterparty id (%s) does not match packet destination id (%s)\",\n\t\t\tc.CounterpartyClientID(), msg.Packet.DestinationClient,\n\t\t))\n\t}\n\n\tcommitment := c.PacketCommitment(msg.Packet.Sequence)\n\tif len(commitment) == 0 {\n\t\t// This error indicates that the acknowledgement has already been relayed\n\t\t// or there is a misconfigured relayer attempting to prove an\n\t\t// acknowledgement for a packet never sent. Core IBC will treat this error\n\t\t// as a no-op in order to prevent an entire relay transaction from failing\n\t\t// and consuming unnecessary fees.\n\t\treturn types.RESPONSE_NOOP\n\t}\n\tpacketCommitment := types.CommitPacket(msg.Packet)\n\t// ensure integrity of commitment\n\tif !bytes.Equal(commitment, packetCommitment) {\n\t\th1, h2 := hex.EncodeToString(packetCommitment), hex.EncodeToString(commitment)\n\t\tpanic(ufmt.Sprintf(\n\t\t\t\"commitment bytes are not equal: got (%v), expected (%v)\", h1, h2,\n\t\t))\n\t}\n\n\t// Verify existence of the acknowledgement commitment bytes in the proofs.\n\tvar (\n\t\tkey        = host.PacketAcknowledgementKey(msg.Packet.DestinationClient, msg.Packet.Sequence)\n\t\tmerklePath = types.BuildMerklePath(c.CounterpartyMerklePrefix(), key)\n\t\tvalue      = types.CommitAcknowledgement(msg.Acknowledgement)\n\t)\n\tif err := verifyMembership(\n\t\tlc, msg.ProofHeight, msg.ProofAcked, extProof, hasExtProof, merklePath, value,\n\t); err != nil {\n\t\tpanic(ufmt.Sprintf(\n\t\t\t\"failed packet acknowledgement verification for client (%s): %v\",\n\t\t\tclientID, err,\n\t\t))\n\t}\n\n\tc.DeletePacketCommitment(0, rlm, msg.Packet.Sequence)\n\n\tcore.EmitAcknowledgePacket(0, rlm, msg.Packet)\n\n\t// Invoke registed app OnAcknowledgementPacket() for each payload.SourcePort.\n\trecvSuccess := !bytes.Equal(msg.Acknowledgement.AppAcknowledgements[0],\n\t\ttypes.UniversalErrorAcknowledgement())\n\tfor i, payload := range msg.Packet.Payloads {\n\t\tapp := l.store.Route(payload.SourcePort)\n\t\t// if recv was successful, each payload should have its own acknowledgement\n\t\t// so we send each individual acknowledgment to the application otherwise,\n\t\t// the acknowledgement only contains the sentinel error acknowledgement\n\t\t// which we send to the application. The application is responsible for\n\t\t// knowing that this is an error acknowledgement and executing the\n\t\t// appropriate logic.\n\t\tvar ack []byte\n\t\tif recvSuccess {\n\t\t\tack = msg.Acknowledgement.AppAcknowledgements[i]\n\t\t} else {\n\t\t\tack = types.UniversalErrorAcknowledgement()\n\t\t}\n\t\terr := app.OnAcknowledgementPacket(\n\t\t\tcross(rlm), msg.Packet.SourceClient, msg.Packet.DestinationClient,\n\t\t\tmsg.Packet.Sequence, ack, payload,\n\t\t)\n\t\tif err != nil {\n\t\t\tpanic(ufmt.Sprintf(\n\t\t\t\t\"acknowledgement packet failed for payload #%d app %q: %v\",\n\t\t\t\ti, payload.SourcePort, err,\n\t\t\t))\n\t\t}\n\t}\n\n\treturn types.RESPONSE_SUCCESS\n}\n\nfunc (l *logic) Timeout(_ int, rlm realm, msg types.MsgTimeout) types.ResponseResultType {\n\textProof, hasExtProof := extproof.FromExt(msg.Ext)\n\tif hasExtProof {\n\t\t// msg.ValidateBasic requires an ICS23 proof; apply the rest of it.\n\t\tif len(msg.ProofUnreceived) != 0 {\n\t\t\tpanic(\"a message cannot carry both an ICS23 proof and an Ext proof\")\n\t\t}\n\t\tif err := l.validatePacket(msg.Packet, msg.Packet.SourceClient); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t} else if err := msg.ValidateBasic(); err != nil {\n\t\tpanic(err)\n\t}\n\n\tclientID := msg.Packet.SourceClient\n\tc := l.mustClient(clientID)\n\tlc := c.LightClient()\n\t// check client is active\n\tif status := lc.Status(); status != lightclient.Active {\n\t\tpanic(ufmt.Sprintf(\"client (%s) status is %s\", clientID, status))\n\t}\n\t// check counterparty\n\tif c.CounterpartyClientID() != msg.Packet.DestinationClient {\n\t\tpanic(ufmt.Sprintf(\n\t\t\t\"counterparty id (%s) does not match packet destination id (%s)\",\n\t\t\tc.CounterpartyClientID(), msg.Packet.DestinationClient,\n\t\t))\n\t}\n\t// check that timeout timestamp has passed on the other end\n\tproofTimestamp, err := lc.TimestampAtHeight(msg.ProofHeight)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif proofTimestamp \u003c msg.Packet.TimeoutTimestamp {\n\t\tpanic(ufmt.Errorf(\"proof timestamp: %d, timeout timestamp: %d\",\n\t\t\tproofTimestamp, msg.Packet.TimeoutTimestamp))\n\t}\n\n\tcommitment := c.PacketCommitment(msg.Packet.Sequence)\n\tif len(commitment) == 0 {\n\t\t// This error indicates that the timeout has already been relayed or there\n\t\t// is a misconfigured relayer attempting to prove a timeout for a packet\n\t\t// never sent. Core IBC will treat this error as a no-op in order to\n\t\t// prevent an entire relay transaction from failing and consuming\n\t\t// unnecessary fees.\n\t\treturn types.RESPONSE_NOOP\n\t}\n\tpacketCommitment := types.CommitPacket(msg.Packet)\n\t// ensure integrity of commitment\n\tif !bytes.Equal(commitment, packetCommitment) {\n\t\th1, h2 := hex.EncodeToString(packetCommitment), hex.EncodeToString(commitment)\n\t\tpanic(ufmt.Sprintf(\n\t\t\t\"commitment bytes are not equal: got (%v), expected (%v)\", h1, h2,\n\t\t))\n\t}\n\n\t// Verify packet receipt absence\n\tvar (\n\t\tkey        = host.PacketReceiptKey(msg.Packet.DestinationClient, msg.Packet.Sequence)\n\t\tmerklePath = types.BuildMerklePath(c.CounterpartyMerklePrefix(), key)\n\t)\n\tif err := verifyNonMembership(\n\t\tlc, msg.ProofHeight, msg.ProofUnreceived, extProof, hasExtProof, merklePath,\n\t); err != nil {\n\t\tpanic(ufmt.Sprintf(\n\t\t\t\"failed packet receipt absence verification for client (%s): %v\",\n\t\t\tclientID, err,\n\t\t))\n\t}\n\n\tc.DeletePacketCommitment(0, rlm, msg.Packet.Sequence)\n\n\tcore.EmitTimeoutPacket(0, rlm, msg.Packet)\n\n\tfor i, payload := range msg.Packet.Payloads {\n\t\tapp := l.store.Route(payload.SourcePort)\n\t\terr := app.OnTimeoutPacket(\n\t\t\tcross(rlm), msg.Packet.SourceClient, msg.Packet.DestinationClient,\n\t\t\tmsg.Packet.Sequence, payload,\n\t\t)\n\t\tif err != nil {\n\t\t\tpanic(ufmt.Sprintf(\n\t\t\t\t\"timeout packet failed for payload #%d app %q: %v\",\n\t\t\t\ti, payload.SourcePort, err,\n\t\t\t))\n\t\t}\n\t}\n\n\treturn types.RESPONSE_SUCCESS\n}\n\n// verifyMembership verifies with the message's Ext proof when it carries one,\n// through the client's extproof.Verifier, and with its ICS23 proofs otherwise.\nfunc verifyMembership(lc lightclient.Interface, height types.Height,\n\tproofs []ics23.CommitmentProof, ext extproof.Proof, hasExt bool,\n\tpath types.MerklePath, value []byte,\n) error {\n\tif !hasExt {\n\t\treturn lc.VerifyMembership(height, proofs, path, value)\n\t}\n\tv, ok := lc.(extproof.Verifier)\n\tif !ok {\n\t\treturn ufmt.Errorf(\"light client %T does not accept Ext proofs\", lc)\n\t}\n\treturn v.VerifyMembershipProof(height, ext.Data, path, value)\n}\n\n// verifyNonMembership is verifyMembership for absence proofs.\nfunc verifyNonMembership(lc lightclient.Interface, height types.Height,\n\tproofs []ics23.CommitmentProof, ext extproof.Proof, hasExt bool,\n\tpath types.MerklePath,\n) error {\n\tif !hasExt {\n\t\treturn lc.VerifyNonMembership(height, proofs, path)\n\t}\n\tv, ok := lc.(extproof.Verifier)\n\tif !ok {\n\t\treturn ufmt.Errorf(\"light client %T does not accept Ext proofs\", lc)\n\t}\n\treturn v.VerifyNonMembershipProof(height, ext.Data, path)\n}\n"},{"name":"render.gno","body":"package impl\n\nimport (\n\t\"encoding/base64\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/aib/jsonmd/v0\"\n\n\t\"gno.land/p/aib/ibc/lightclient/ethereum/v0\"\n\t\"gno.land/p/aib/ibc/lightclient/tendermint/v0\"\n\t\"gno.land/p/aib/ibc/lightclient/v0\"\n\t\"gno.land/p/aib/ibc/types/v0\"\n\t\"gno.land/p/aib/jsonpage/v0\"\n\t\"gno.land/p/nt/bptree/v0\"\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/p/onbloc/json/v0\"\n\t\"gno.land/r/aib/ibc/core\"\n)\n\n// Render serves every path of the core realm: the proxy forwards its Render\n// here (see core.Render). The JSON routes under clients/ are read by the\n// relayer through vm/qrender; their shape is pinned by the z0c filetest.\nfunc (l *logic) Render(path string) string {\n\trouter := mux.NewRouter()\n\tNewRenderer(l.store).Register(router)\n\treturn router.Render(path)\n}\n\n// Renderer serves the routes of the core realm from a Store. It is exported,\n// with its handlers and the JSON helpers below, so a later implementation can\n// reuse what it does not change:\n//\n//\trd := implv0.NewRenderer(store)\n//\trouter.HandleFunc(\"\", myHome) // overrides first: mux matches in registration order\n//\trd.Register(router)\n//\n// A later implementation cannot build a Renderer literal (a realm cannot\n// construct another realm's types), hence NewRenderer.\ntype Renderer struct {\n\tstore core.Store\n}\n\n// NewRenderer returns a Renderer reading from s.\nfunc NewRenderer(s core.Store) *Renderer {\n\treturn \u0026Renderer{store: s}\n}\n\n// Register installs every route of this implementation on router. Routes\n// already registered on router take precedence.\nfunc (rd *Renderer) Register(router *mux.Router) {\n\trouter.HandleFunc(\"\", rd.Home)\n\trouter.HandleFunc(\"admin\", rd.Admin)\n\trouter.HandleFunc(\"apps\", rd.Apps)\n\trouter.HandleFunc(\"apps/pending\", rd.PendingApps)\n\trouter.HandleFunc(\"clients\", rd.Clients)\n\trouter.HandleFunc(\"clients/{id}\", rd.Client)\n\trouter.HandleFunc(\"clients/{id}/status\", rd.ClientStatus)\n\trouter.HandleFunc(\"clients/{id}/consensus_states\", rd.ClientConsensusStates)\n\trouter.HandleFunc(\"clients/{id}/consensus_states/{revision_number}/{revision_height}\", rd.ClientConsensusState)\n\trouter.HandleFunc(\"clients/{id}/next_sequence_send\", rd.ClientNextSequenceSend)\n\trouter.HandleFunc(\"clients/{id}/packet_commitments\", rd.ClientPacketCommitments)\n\trouter.HandleFunc(\"clients/{id}/packet_commitments/{sequence}\", rd.ClientPacketCommitment)\n\trouter.HandleFunc(\"clients/{id}/packet_receipts\", rd.ClientPacketReceipts)\n\trouter.HandleFunc(\"clients/{id}/packet_receipts/{sequence}\", rd.ClientPacketReceipt)\n\trouter.HandleFunc(\"clients/{id}/packet_acknowledgements\", rd.ClientPacketAcknowledgements)\n\trouter.HandleFunc(\"clients/{id}/packet_acknowledgements/{sequence}\", rd.ClientPacketAcknowledgement)\n\trouter.HandleFunc(\"clients/{id}/unreceived_packets\", rd.ClientUnreceivedPackets)\n}\n\nfunc (rd *Renderer) Home(w *mux.ResponseWriter, r *mux.Request) {\n\tvar out strings.Builder\n\tout.WriteString(\"# IBC core\\n\\n\")\n\tout.WriteString(\"IBC v2 core state and query endpoints.\\n\\n\")\n\tout.WriteString(RenderImplDisclosure())\n\tids := rd.store.ClientIDs()\n\tout.WriteString(ufmt.Sprintf(\"## Clients (%d)\\n\\n\", len(ids)))\n\tif len(ids) \u003e 0 {\n\t\tout.WriteString(\"| Client | St. | Creator | Cpty | Height | Time | CS | Seq | Cmts | Rcpts | Acks |\\n\")\n\t\tout.WriteString(\"|--------|-----|---------|------|--------|------|----|-----|------|-------|------|\\n\")\n\t\tfor _, id := range ids {\n\t\t\tc := rd.store.Client(id)\n\t\t\tlc := c.LightClient()\n\t\t\tvar (\n\t\t\t\tnConsStates int\n\t\t\t\theightTime  string\n\t\t\t)\n\t\t\tswitch c.Type() {\n\t\t\tcase lightclient.Tendermint:\n\t\t\t\ttm := lc.(*tendermint.TMLightClient)\n\t\t\t\tnConsStates = tm.ConsensusStateByHeight.Size()\n\t\t\t\tif cs, found := tm.GetConsensusState(tm.LatestHeight()); found {\n\t\t\t\t\theightTime = cs.Timestamp.UTC().Format(time.RFC3339)\n\t\t\t\t}\n\t\t\t}\n\t\t\tout.WriteString(ufmt.Sprintf(\n\t\t\t\t\"| [`%s`](/r/aib/ibc/core:clients/%s) | [%s](/r/aib/ibc/core:clients/%s/status) | %s | %s | [%s](/r/aib/ibc/core:clients/%s/consensus_states/%d/%d) | %s | [%d](/r/aib/ibc/core:clients/%s/consensus_states) | [%d](/r/aib/ibc/core:clients/%s/next_sequence_send) | [%d](/r/aib/ibc/core:clients/%s/packet_commitments) | [%d](/r/aib/ibc/core:clients/%s/packet_receipts) | [%d](/r/aib/ibc/core:clients/%s/packet_acknowledgements) |\\n\",\n\t\t\t\tid, id,\n\t\t\t\tlc.Status(), id,\n\t\t\t\tRenderAddr(c.Creator()),\n\t\t\t\tjsonmd.Cell(c.CounterpartyClientID()),\n\t\t\t\tlc.LatestHeight().String(), id, lc.LatestHeight().RevisionNumber, lc.LatestHeight().RevisionHeight,\n\t\t\t\theightTime,\n\t\t\t\tnConsStates, id,\n\t\t\t\tint64(c.NextSequenceSend()), id,\n\t\t\t\tc.PacketCommitments().Size(), id,\n\t\t\t\tc.PacketReceipts().Size(), id,\n\t\t\t\tc.PacketAcknowledgements().Size(), id,\n\t\t\t))\n\t\t}\n\t\tout.WriteString(\"\\n\")\n\t} else {\n\t\tout.WriteString(\"No clients yet.\\n\\n\")\n\t}\n\tports := rd.store.Ports()\n\tout.WriteString(ufmt.Sprintf(\"## Apps (%d)\\n\\n\", len(ports)))\n\tif len(ports) \u003e 0 {\n\t\tout.WriteString(\"| Port | Pkg | Addr |\\n\")\n\t\tout.WriteString(\"|------|-----|------|\\n\")\n\t\tfor _, port := range ports {\n\t\t\tapp := rd.store.Route(port)\n\t\t\tout.WriteString(ufmt.Sprintf(\"| %s | %s | %s |\\n\", jsonmd.Cell(port), RenderPkgPath(app.PkgPath), RenderAddr(app.Address)))\n\t\t}\n\t\tout.WriteString(\"\\n\")\n\t} else {\n\t\tout.WriteString(\"No apps registered yet.\\n\\n\")\n\t}\n\tout.WriteString(\"## JSON endpoints\\n\\n\")\n\tout.WriteString(\"- [`admin`](/r/aib/ibc/core:admin): authority, pause state and relayers\\n\")\n\tout.WriteString(\"- [`apps`](/r/aib/ibc/core:apps): list registered IBC applications\\n\")\n\tout.WriteString(\"- [`apps/pending`](/r/aib/ibc/core:apps/pending): registrations awaiting the authority's approval\\n\")\n\tout.WriteString(\"- [`clients`](/r/aib/ibc/core:clients): list clients (`?page`, `?limit`)\\n\")\n\tout.WriteString(\"- `clients/{id}`: get client details\\n\")\n\tout.WriteString(\"- `clients/{id}/status`: get client status\\n\")\n\tout.WriteString(\"- `clients/{id}/consensus_states`: list client consensus states (`?page`, `?limit`)\\n\")\n\tout.WriteString(\"- `clients/{id}/consensus_states/{revision_number}/{revision_height}`: get a consensus state by height\\n\")\n\tout.WriteString(\"- `clients/{id}/next_sequence_send`: get the next packet sequence to send\\n\")\n\tout.WriteString(\"- `clients/{id}/packet_commitments`: list packet commitments (`?page`, `?limit`)\\n\")\n\tout.WriteString(\"- `clients/{id}/packet_commitments/{sequence}`: get a packet commitment by sequence\\n\")\n\tout.WriteString(\"- `clients/{id}/packet_receipts`: list packet receipts (`?page`, `?limit`)\\n\")\n\tout.WriteString(\"- `clients/{id}/packet_receipts/{sequence}`: get a packet receipt by sequence\\n\")\n\tout.WriteString(\"- `clients/{id}/packet_acknowledgements`: list packet acknowledgements (`?page`, `?limit`)\\n\")\n\tout.WriteString(\"- `clients/{id}/packet_acknowledgements/{sequence}`: get a packet acknowledgement by sequence\\n\")\n\tout.WriteString(\"- `clients/{id}/unreceived_packets?sequences=1,2,3`: return sequences without a local packet receipt\\n\\n\")\n\tw.Write(out.String())\n}\n\nfunc (rd *Renderer) Admin(w *mux.ResponseWriter, r *mux.Request) {\n\tvar out strings.Builder\n\tout.WriteString(\"## Authority \u0026 Relayers\\n\\n\")\n\tout.WriteString(\"- **Authority members**:\\n\")\n\tfor _, m := range core.AuthorityMembers() {\n\t\tout.WriteString(ufmt.Sprintf(\"  - %s\\n\", RenderAddr(m)))\n\t}\n\tout.WriteString(ufmt.Sprintf(\"- **Paused**: %t\\n\", core.Paused()))\n\tout.WriteString(ufmt.Sprintf(\"- **Implementation**: %s\\n\", RenderImplRef(core.ImplPath(), core.ImplVersion())))\n\tout.WriteString(\"- **Registered implementations**:\\n\")\n\tfor _, p := range core.RegisteredImplPaths() {\n\t\tout.WriteString(ufmt.Sprintf(\"  - %s\\n\", RenderPkgPath(p)))\n\t}\n\trelayers := core.Relayers()\n\tif len(relayers) == 0 {\n\t\tout.WriteString(\"- **Relayers**: any (whitelist empty)\\n\")\n\t} else {\n\t\tout.WriteString(\"- **Relayers**:\\n\")\n\t\tfor _, r := range relayers {\n\t\t\tout.WriteString(ufmt.Sprintf(\"  - %s\\n\", RenderAddr(r)))\n\t\t}\n\t}\n\tw.Write(out.String())\n}\n\nfunc (rd *Renderer) Apps(w *mux.ResponseWriter, r *mux.Request) {\n\tvar nodes []*json.Node\n\tfor _, port := range rd.store.Ports() {\n\t\tapp := rd.store.Route(port)\n\t\tnodes = append(nodes, json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\t\"port_id\":  json.StringNode(\"\", port),\n\t\t\t\"pkg_path\": json.StringNode(\"\", app.PkgPath),\n\t\t\t\"address\":  json.StringNode(\"\", app.Address.String()),\n\t\t\t\"paused\":   json.BoolNode(\"\", app.Paused),\n\t\t}))\n\t}\n\tRenderNode(w, json.ArrayNode(\"\", nodes))\n}\n\n// renderPendingApps lists the registrations awaiting approval, oldest key\n// first. Anyone may add to this set, so the listing is capped; the count is\n// always exact.\nfunc (rd *Renderer) PendingApps(w *mux.ResponseWriter, r *mux.Request) {\n\tconst limit = 100\n\tvar nodes []*json.Node\n\tpending := rd.store.PendingApps()\n\tpending.IterateByOffset(0, limit, func(_ string, v any) bool {\n\t\ta := v.(core.App)\n\t\tnodes = append(nodes, json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\t\"pkg_path\": json.StringNode(\"\", a.PkgPath),\n\t\t\t\"address\":  json.StringNode(\"\", a.Address.String()),\n\t\t}))\n\t\treturn false\n\t})\n\tRenderNode(w, json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\"total\":   json.NumberNode(\"\", float64(pending.Size())),\n\t\t\"pending\": json.ArrayNode(\"\", nodes),\n\t}))\n}\n\nfunc (rd *Renderer) Clients(w *mux.ResponseWriter, r *mux.Request) {\n\tRenderNode(w, jsonpage.Render(rd.store.Clients(), r, func(_ string, v any) *json.Node {\n\t\treturn ClientJSON(v.(core.Client))\n\t}))\n}\n\nfunc (rd *Renderer) Client(w *mux.ResponseWriter, r *mux.Request) {\n\tid := r.GetVar(\"id\")\n\tc := rd.store.Client(id)\n\tif c == nil {\n\t\tRenderNode(w, NodeErrorClientNotFound(id))\n\t\treturn\n\t}\n\tRenderNode(w, ClientJSON(c))\n}\n\nfunc (rd *Renderer) ClientStatus(w *mux.ResponseWriter, r *mux.Request) {\n\tid := r.GetVar(\"id\")\n\tc := rd.store.Client(id)\n\tif c == nil {\n\t\tRenderNode(w, NodeErrorClientNotFound(id))\n\t\treturn\n\t}\n\tRenderNode(w, json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\"status\": json.StringNode(\"\", c.LightClient().Status()),\n\t}))\n}\n\nfunc (rd *Renderer) ClientConsensusStates(w *mux.ResponseWriter, r *mux.Request) {\n\tid := r.GetVar(\"id\")\n\tc := rd.store.Client(id)\n\tif c == nil {\n\t\tRenderNode(w, NodeErrorClientNotFound(id))\n\t\treturn\n\t}\n\tRenderNode(w, ConsensusStatesJSON(c, r))\n}\n\nfunc (rd *Renderer) ClientConsensusState(w *mux.ResponseWriter, r *mux.Request) {\n\tvar (\n\t\tid             = r.GetVar(\"id\")\n\t\trevisionNumber = r.GetVar(\"revision_number\")\n\t\trevisionHeight = r.GetVar(\"revision_height\")\n\t\theightStr      = revisionNumber + \"/\" + revisionHeight\n\t)\n\theight, err := types.ParseHeight(heightStr)\n\tif err != nil {\n\t\tRenderNode(w, NodeError(\"cant parse height %s: %v\", heightStr, err))\n\t\treturn\n\t}\n\tc := rd.store.Client(id)\n\tif c == nil {\n\t\tRenderNode(w, NodeErrorClientNotFound(id))\n\t\treturn\n\t}\n\tswitch c.Type() {\n\tcase lightclient.Tendermint:\n\t\tlc := c.LightClient().(*tendermint.TMLightClient)\n\t\tcs, found := lc.GetConsensusState(height)\n\t\tif !found {\n\t\t\tRenderNode(w, NodeError(\"consensus state not found for height %s\", height))\n\t\t\treturn\n\t\t}\n\t\tRenderNode(w, RenderConsensusState(height, cs))\n\tcase ethereum.ClientType:\n\t\tlc, ok := ethVerifier(c.LightClient())\n\t\tif !ok {\n\t\t\tRenderNode(w, NodeError(\"client %s's verifier keeps no ethereum/v0 state\", id))\n\t\t\treturn\n\t\t}\n\t\tcs, found := lc.GetConsensusState(height)\n\t\tif !found {\n\t\t\tRenderNode(w, NodeError(\"consensus state not found for height %s\", height))\n\t\t\treturn\n\t\t}\n\t\tRenderNode(w, RenderEthConsensusState(height, cs))\n\t}\n}\n\nfunc (rd *Renderer) ClientNextSequenceSend(w *mux.ResponseWriter, r *mux.Request) {\n\tid := r.GetVar(\"id\")\n\tc := rd.store.Client(id)\n\tif c == nil {\n\t\tRenderNode(w, NodeErrorClientNotFound(id))\n\t\treturn\n\t}\n\tw.Write(ufmt.Sprintf(`{\"next_sequence_send\": %d}`, int64(c.NextSequenceSend())))\n}\n\nfunc (rd *Renderer) ClientPacketCommitments(w *mux.ResponseWriter, r *mux.Request) {\n\tid := r.GetVar(\"id\")\n\tc := rd.store.Client(id)\n\tif c == nil {\n\t\tRenderNode(w, NodeErrorClientNotFound(id))\n\t\treturn\n\t}\n\tRenderCommitments(w, r, c.PacketCommitments())\n}\n\nfunc (rd *Renderer) ClientPacketCommitment(w *mux.ResponseWriter, r *mux.Request) {\n\tid := r.GetVar(\"id\")\n\tc := rd.store.Client(id)\n\tif c == nil {\n\t\tRenderNode(w, NodeErrorClientNotFound(id))\n\t\treturn\n\t}\n\tRenderCommitment(w, r, c.PacketCommitments())\n}\n\nfunc (rd *Renderer) ClientPacketReceipts(w *mux.ResponseWriter, r *mux.Request) {\n\tid := r.GetVar(\"id\")\n\tc := rd.store.Client(id)\n\tif c == nil {\n\t\tRenderNode(w, NodeErrorClientNotFound(id))\n\t\treturn\n\t}\n\tRenderCommitments(w, r, c.PacketReceipts())\n}\n\nfunc (rd *Renderer) ClientPacketReceipt(w *mux.ResponseWriter, r *mux.Request) {\n\tid := r.GetVar(\"id\")\n\tc := rd.store.Client(id)\n\tif c == nil {\n\t\tRenderNode(w, NodeErrorClientNotFound(id))\n\t\treturn\n\t}\n\tRenderCommitment(w, r, c.PacketReceipts())\n}\n\nfunc (rd *Renderer) ClientPacketAcknowledgements(w *mux.ResponseWriter, r *mux.Request) {\n\tid := r.GetVar(\"id\")\n\tc := rd.store.Client(id)\n\tif c == nil {\n\t\tRenderNode(w, NodeErrorClientNotFound(id))\n\t\treturn\n\t}\n\tRenderCommitments(w, r, c.PacketAcknowledgements())\n}\n\nfunc (rd *Renderer) ClientPacketAcknowledgement(w *mux.ResponseWriter, r *mux.Request) {\n\tid := r.GetVar(\"id\")\n\tc := rd.store.Client(id)\n\tif c == nil {\n\t\tRenderNode(w, NodeErrorClientNotFound(id))\n\t\treturn\n\t}\n\tRenderCommitment(w, r, c.PacketAcknowledgements())\n}\n\nfunc RenderCommitment(w *mux.ResponseWriter, r *mux.Request, tree bptree.ITree) {\n\tseqStr := r.GetVar(\"sequence\")\n\tseq, err := strconv.ParseUint(seqStr, 10, 64)\n\tif err != nil {\n\t\tRenderNode(w, NodeError(\"invalid sequence %q: %v\", seqStr, err))\n\t\treturn\n\t}\n\tv := tree.Get(seqid.ID(seq).Binary())\n\tif v == nil {\n\t\tRenderNode(w, NodeError(\"sequence %s not found\", seqStr))\n\t\treturn\n\t}\n\tRenderNode(w, CommitmentNode(seq, v.([]byte)))\n}\n\nfunc RenderCommitments(w *mux.ResponseWriter, r *mux.Request, tree bptree.ITree) {\n\tRenderNode(w, jsonpage.Render(tree, r, func(k string, v any) *json.Node {\n\t\tid, _ := seqid.FromBinary(k)\n\t\treturn CommitmentNode(uint64(id), v.([]byte))\n\t}))\n}\n\nfunc CommitmentNode(sequence uint64, data []byte) *json.Node {\n\treturn json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\"sequence\": json.StringNode(\"\", strconv.FormatUint(sequence, 10)),\n\t\t\"data\":     json.StringNode(\"\", base64.StdEncoding.EncodeToString(data)),\n\t})\n}\n\n// renderClientUnreceivedPackets returns, given a list of sequences, the\n// sequences for which no counterparty packet commitments have been received.\n// This is done by checking if a receipt exists on this chain for the packet\n// sequence.\nfunc (rd *Renderer) ClientUnreceivedPackets(w *mux.ResponseWriter, r *mux.Request) {\n\tid := r.GetVar(\"id\")\n\tc := rd.store.Client(id)\n\tif c == nil {\n\t\tRenderNode(w, NodeErrorClientNotFound(id))\n\t\treturn\n\t}\n\tseqs := strings.Split(strings.TrimSpace(r.Query.Get(\"sequences\")), \",\")\n\tvar unreceivedSeqs []*json.Node\n\tfor _, seq := range seqs {\n\t\tseqi, err := strconv.ParseUint(seq, 10, 64)\n\t\tif err != nil {\n\t\t\tRenderNode(w, NodeError(ufmt.Sprintf(\"invalid sequence %q: %v\", seq, err)))\n\t\t\treturn\n\t\t}\n\t\tif !c.HasPacketReceipt(seqi) {\n\t\t\tunreceivedSeqs = append(unreceivedSeqs, json.StringNode(\"\", seq))\n\t\t}\n\t}\n\tRenderNode(w, json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\"height\":               RenderHeight(types.GetSelfHeight()),\n\t\t\"unreceived_sequences\": json.ArrayNode(\"\", unreceivedSeqs),\n\t}))\n}\n\n// ClientJSON is the clients/{id} document, also the items of clients.\nfunc ClientJSON(c core.Client) *json.Node {\n\tm := map[string]*json.Node{\n\t\t\"id\":                     json.StringNode(\"\", c.ID()),\n\t\t\"type\":                   json.StringNode(\"\", c.Type()),\n\t\t\"creator\":                json.StringNode(\"\", c.Creator().String()),\n\t\t\"status\":                 json.StringNode(\"\", c.LightClient().Status()),\n\t\t\"counterparty_client_id\": json.StringNode(\"\", c.CounterpartyClientID()),\n\t}\n\tvar prefixes []*json.Node\n\tfor _, p := range c.CounterpartyMerklePrefix() {\n\t\tprefixes = append(prefixes, json.StringNode(\"\", string(p)))\n\t}\n\tm[\"counterparty_merke_prefix\"] = json.ArrayNode(\"\", prefixes)\n\tswitch c.Type() {\n\tcase lightclient.Tendermint:\n\t\tlc := c.LightClient().(*tendermint.TMLightClient)\n\t\tm[\"client_state\"] = json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\t\"chain_id\":         json.StringNode(\"\", lc.ClientState.ChainID),\n\t\t\t\"latest_height\":    RenderHeight(lc.ClientState.LatestHeight),\n\t\t\t\"frozen_height\":    RenderHeight(lc.ClientState.FrozenHeight),\n\t\t\t\"trust_level\":      RenderFraction(lc.ClientState.TrustLevel),\n\t\t\t\"trusting_period\":  RenderDuration(lc.ClientState.TrustingPeriod),\n\t\t\t\"unbonding_period\": RenderDuration(lc.ClientState.UnbondingPeriod),\n\t\t\t\"max_clock_drift\":  RenderDuration(lc.ClientState.MaxClockDrift),\n\t\t\t\"upgrade_path\":     RenderStrings(lc.ClientState.UpgradePath),\n\t\t})\n\t\tlastConsState, found := lc.GetConsensusState(lc.LatestHeight())\n\t\tif found {\n\t\t\tm[\"last_consensus_state\"] = RenderConsensusState(lc.LatestHeight(), lastConsState)\n\t\t}\n\tcase ethereum.ClientType:\n\t\tlc, ok := ethVerifier(c.LightClient())\n\t\tif !ok {\n\t\t\tbreak // a verifier version this implementation cannot render\n\t\t}\n\t\tcs := lc.ClientState\n\t\tm[\"client_state\"] = RenderEthClientState(lc.LatestHeight(), cs)\n\t\tlastConsState, found := lc.GetConsensusState(lc.LatestHeight())\n\t\tif found {\n\t\t\tm[\"last_consensus_state\"] = RenderEthConsensusState(lc.LatestHeight(), lastConsState)\n\t\t}\n\t}\n\treturn json.ObjectNode(\"\", m)\n}\n\nfunc ConsensusStatesJSON(c core.Client, r *mux.Request) *json.Node {\n\tswitch c.Type() {\n\tcase lightclient.Tendermint:\n\t\tlc := c.LightClient().(*tendermint.TMLightClient)\n\t\treturn jsonpage.Render(lc.ConsensusStateByHeight, r, func(k string, v any) *json.Node {\n\t\t\tcs := v.(*tendermint.ConsensusState)\n\t\t\theight := types.ParseHeightNatSort(k)\n\t\t\treturn RenderConsensusState(height, cs)\n\t\t})\n\tcase ethereum.ClientType:\n\t\t// The consensus states this verifier stores itself; after a verifier\n\t\t// replacement, the retired verifier's are served one height at a\n\t\t// time (consensus_states/{revision}/{height}). A state below a\n\t\t// recovery's cut-off is listed as untrusted, its content withheld.\n\t\tlc, ok := ethVerifier(c.LightClient())\n\t\tif !ok {\n\t\t\treturn NodeError(\"client %s's verifier keeps no ethereum/v0 state\", c.ID())\n\t\t}\n\t\treturn jsonpage.Render(lc.ConsensusStateByHeight, r, func(k string, v any) *json.Node {\n\t\t\tcs := v.(*ethereum.ConsensusState)\n\t\t\theight := types.ParseHeightNatSort(k)\n\t\t\tif cs.Slot \u003c lc.TrustedFromSlot {\n\t\t\t\treturn json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\t\t\t\"height\":  RenderHeight(height),\n\t\t\t\t\t\"trusted\": json.BoolNode(\"\", false),\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn RenderEthConsensusState(height, cs)\n\t\t})\n\t}\n\treturn NodeError(\"unhandled client type %s\", c.Type())\n}\n\n// ethState is a verifier exposing ethereum/v0 state: the v0 verifier\n// itself, or a later version wrapping it.\ntype ethState interface {\n\tEthereum() *ethereum.EthLightClient\n}\n\n// ethVerifier is the ethereum/v0 state of an Ethereum client's verifier.\nfunc ethVerifier(lc lightclient.Interface) (*ethereum.EthLightClient, bool) {\n\tif v, ok := lc.(ethState); ok {\n\t\treturn v.Ethereum(), true\n\t}\n\treturn nil, false\n}\n\nfunc RenderConsensusState(height types.Height, cs *tendermint.ConsensusState) *json.Node {\n\treturn json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\"height\":               RenderHeight(height),\n\t\t\"timestamp\":            json.NumberNode(\"\", float64(cs.Timestamp.Unix())),\n\t\t\"root\":                 json.StringNode(\"\", base64.StdEncoding.EncodeToString(cs.Root.Hash)),\n\t\t\"next_validators_hash\": json.StringNode(\"\", base64.StdEncoding.EncodeToString(cs.NextValidatorsHash)),\n\t})\n}\n\n// RenderEthClientState renders the client state of an Ethereum client. uint64\n// values that may exceed 2^53 (fork epochs, FarFutureEpoch among them) are\n// decimal strings.\nfunc RenderEthClientState(height types.Height, cs *ethereum.ClientState) *json.Node {\n\tfp := cs.ForkParameters\n\treturn json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\"chain_id\":                         RenderUint(cs.ChainID),\n\t\t\"genesis_validators_root\":          RenderBytes(cs.GenesisValidatorsRoot),\n\t\t\"min_sync_committee_participants\":  RenderUint(cs.MinSyncCommitteeParticipants),\n\t\t\"sync_committee_size\":              RenderUint(cs.SyncCommitteeSize),\n\t\t\"genesis_time\":                     RenderUint(cs.GenesisTime),\n\t\t\"genesis_slot\":                     RenderUint(cs.GenesisSlot),\n\t\t\"seconds_per_slot\":                 RenderUint(cs.SecondsPerSlot),\n\t\t\"slots_per_epoch\":                  RenderUint(cs.SlotsPerEpoch),\n\t\t\"epochs_per_sync_committee_period\": RenderUint(cs.EpochsPerSyncCommitteePeriod),\n\t\t\"latest_height\":                    RenderHeight(height),\n\t\t\"latest_execution_block_number\":    RenderUint(cs.LatestExecutionBlockNumber),\n\t\t\"is_frozen\":                        json.BoolNode(\"\", cs.IsFrozen),\n\t\t\"ibc_contract_address\":             RenderBytes(cs.IBCContractAddress),\n\t\t\"ibc_commitment_slot\":              RenderBytes(cs.IBCCommitmentSlot),\n\t\t\"fork_parameters\": json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\t\"genesis_fork_version\": RenderBytes(fp.GenesisForkVersion[:]),\n\t\t\t\"altair\":               renderFork(fp.Altair),\n\t\t\t\"bellatrix\":            renderFork(fp.Bellatrix),\n\t\t\t\"capella\":              renderFork(fp.Capella),\n\t\t\t\"deneb\":                renderFork(fp.Deneb),\n\t\t\t\"electra\":              renderFork(fp.Electra),\n\t\t\t\"fulu\":                 renderFork(fp.Fulu),\n\t\t\t\"gloas\":                renderFork(fp.Gloas),\n\t\t}),\n\t})\n}\n\nfunc renderFork(f ethereum.Fork) *json.Node {\n\treturn json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\"version\": RenderBytes(f.Version[:]),\n\t\t\"epoch\":   RenderUint(f.Epoch),\n\t})\n}\n\n// RenderEthConsensusState renders the consensus state of an Ethereum client.\n// next_sync_committee_known tells a relayer whether the client still needs\n// the period's update revealing the next sync committee.\nfunc RenderEthConsensusState(height types.Height, cs *ethereum.ConsensusState) *json.Node {\n\tm := map[string]*json.Node{\n\t\t\"height\":                    RenderHeight(height),\n\t\t\"slot\":                      json.NumberNode(\"\", float64(cs.Slot)),\n\t\t\"timestamp\":                 json.NumberNode(\"\", float64(cs.Timestamp)),\n\t\t\"state_root\":                RenderBytes(cs.StateRoot),\n\t\t\"current_sync_committee\":    renderSyncCommittee(cs.CurrentSyncCommittee),\n\t\t\"next_sync_committee_known\": json.BoolNode(\"\", cs.NextSyncCommittee != nil),\n\t}\n\tif cs.NextSyncCommittee != nil {\n\t\tm[\"next_sync_committee\"] = renderSyncCommittee(*cs.NextSyncCommittee)\n\t}\n\treturn json.ObjectNode(\"\", m)\n}\n\nfunc renderSyncCommittee(s ethereum.SummarizedSyncCommittee) *json.Node {\n\treturn json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\"pubkeys_hash\":     RenderBytes(s.PubkeysHash),\n\t\t\"aggregate_pubkey\": RenderBytes(s.AggregatePubkey),\n\t})\n}\n\nfunc RenderUint(v uint64) *json.Node {\n\treturn json.StringNode(\"\", strconv.FormatUint(v, 10))\n}\n\nfunc RenderBytes(b []byte) *json.Node {\n\treturn json.StringNode(\"\", base64.StdEncoding.EncodeToString(b))\n}\n\nfunc RenderHeight(h types.Height) *json.Node {\n\treturn json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\"revision_number\": json.NumberNode(\"\", float64(h.RevisionNumber)),\n\t\t\"revision_height\": json.NumberNode(\"\", float64(h.RevisionHeight)),\n\t})\n}\n\nfunc RenderFraction(f tendermint.Fraction) *json.Node {\n\treturn json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\"numerator\":   json.NumberNode(\"\", float64(f.Numerator)),\n\t\t\"denominator\": json.NumberNode(\"\", float64(f.Denominator)),\n\t})\n}\n\nfunc RenderDuration(d time.Duration) *json.Node {\n\treturn json.NumberNode(\"\", d.Seconds())\n}\n\nfunc RenderStrings(s []string) *json.Node {\n\tvar nodes []*json.Node\n\tfor _, s := range s {\n\t\tnodes = append(nodes, json.StringNode(\"\", s))\n\t}\n\treturn json.ArrayNode(\"\", nodes)\n}\n\n// RenderNode writes n as JSON, inert in Markdown (jsonmd.Inert): its strings\n// may carry a path segment of the request or a relayer's identifier.\nfunc RenderNode(w *mux.ResponseWriter, n *json.Node) {\n\tbz, err := json.Marshal(n)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tw.Write(jsonmd.Inert(string(bz)))\n}\n\nfunc NodeErrorClientNotFound(id string) *json.Node {\n\treturn NodeError(\"client %s not found\", id)\n}\n\n// NodeError is the JSON error object every route answers on failure.\nfunc NodeError(msg string, args ...any) *json.Node {\n\treturn json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\"error\": json.StringNode(\"\", ufmt.Sprintf(msg, args...)),\n\t})\n}\n\n// RenderImplDisclosure is the upgradeable-realm notice of the home page.\nfunc RenderImplDisclosure() string {\n\treturn ufmt.Sprintf(\"**Upgradeable realm**: the client and packet lifecycle runs in the implementation %s, replaceable by the authority with `UpdateImpl` (see [`admin`](/r/aib/ibc/core:admin)).\\n\\n\",\n\t\tRenderImplRef(core.ImplPath(), core.ImplVersion()))\n}\n\nfunc RenderImplRef(path, version string) string {\n\tif path == \"\" {\n\t\treturn \"none installed\"\n\t}\n\treturn ufmt.Sprintf(\"%s (version %s)\", RenderPkgPath(path), version)\n}\n\nfunc RenderPkgPath(path string) string {\n\tconst prefix = \"gno.land\"\n\tif strings.HasPrefix(path, prefix) {\n\t\treturn ufmt.Sprintf(\"[`%s`](%s)\", path, path[len(prefix):])\n\t}\n\treturn \"`\" + path + \"`\"\n}\n\nfunc RenderAddr(addr address) string {\n\ts := addr.String()\n\tshort := s\n\tif len(s) \u003e 10 {\n\t\tshort = s[:6] + \"…\" + s[len(s)-4:]\n\t}\n\treturn ufmt.Sprintf(\"[%s](/u/%s)\", short, s)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"huBVphwzsiXX3ae9+7ER1AQOeboFS+VsyvmTmS72OgBTUqwvVO3EveJlbA5uG2Gx1Mn3H7kr8NbNeENM1CW2fw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"impl","path":"gno.land/r/aib/ibc/core/impl/v1","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/aib/ibc/core/impl/v1\"\ngno = \"0.9\"\n"},{"name":"impl.gno","body":"// Package impl is the second implementation of the IBC core lifecycle, behind\n// the gno.land/r/aib/ibc/core proxy (see docs/adrs/0001). It runs impl/v0's\n// lifecycle unchanged and adds core.ClientMigrator (ADR pr6375 R16): a\n// governed migration of a live client's verifier that keeps its id, its\n// consensus states and its packet state. Like v0 it holds no state of its own\n// and is always invoked non-crossing by the proxy, as the proxy.\n//\n// It is activated by an UpdateImpl decision of gno.land/r/aib/ibc/ibcgov.\npackage impl\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/aib/ibc/lightclient/ethereum/v0\"\n\t\"gno.land/p/aib/ibc/lightclient/v0\"\n\t\"gno.land/p/aib/ibc/types/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/aib/ibc/core\"\n\timplv0 \"gno.land/r/aib/ibc/core/impl/v0\"\n)\n\nconst version = \"v1\"\n\n// MigrationEthereumForkSchedule replaces the fork schedule of an Ethereum\n// client (ethereum.EthLightClient.MigrateForkParameters); params is\n// ethereum.EncodeForkParameters of the new schedule.\nconst MigrationEthereumForkSchedule = \"ethereum-fork-schedule\"\n\n// MigrationEthereumVerifier replaces an Ethereum client's verifier by\n// another verifier version; params is the target version. Only a conversion\n// this implementation compiles in, declared as plain values (from, to), is\n// run: none in impl/v1 itself; a later implementation declares its own\n// (NewWithConversions), installed by a governed, manifest-checked upgrade.\n// The conversion keeps the client's id, its consensus states (the new\n// verifier serves the retired one's, read only) and its packet state.\nconst MigrationEthereumVerifier = \"ethereum-verifier\"\n\ntype logic struct {\n\tcore.Logic // impl/v0's lifecycle\n\tstore      core.Store\n\tver        string\n\n\t// The verifier conversions this implementation compiles in: the i-th\n\t// converts version convFrom[i] to convTo[i].\n\tconvFrom, convTo []string\n\tconvert          []func(*ethereum.Retired) lightclient.Interface\n}\n\nvar (\n\t_ core.Logic                  = (*logic)(nil)\n\t_ core.ClientMigrator         = (*logic)(nil)\n\t_ core.ClientMigrationChecker = (*logic)(nil)\n)\n\n// New builds the implementation against the proxy's store.\nfunc New(s core.Store) core.Logic {\n\treturn \u0026logic{Logic: implv0.New(s), store: s, ver: version}\n}\n\n// NewWithConversions is impl/v1's logic under version ver with verifier\n// conversions compiled in, the i-th from verifier version from[i] to version\n// to[i] (the three slices of one length): the constructor of a later core\n// implementation that brings new verifier versions, registered under its own\n// path and installed by a governed upgrade. Each convert[i] must be a\n// function of the new verifier's /p/ package; it receives a read-only view\n// of the live verifier (ethereum.Retired), so it cannot write to it, and\n// returns the new one.\nfunc NewWithConversions(s core.Store, ver string, from, to []string, convert []func(*ethereum.Retired) lightclient.Interface) core.Logic {\n\tif len(from) != len(to) || len(to) != len(convert) {\n\t\tpanic(\"NewWithConversions: from, to and convert differ in length\")\n\t}\n\tl := \u0026logic{Logic: implv0.New(s), store: s, ver: ver}\n\tl.convFrom = append(l.convFrom, from...)\n\tl.convTo = append(l.convTo, to...)\n\tl.convert = append(l.convert, convert...)\n\treturn l\n}\n\n// conversion is the conversion of from to to this implementation compiles\n// in, or nil.\nfunc (l *logic) conversion(from, to string) func(*ethereum.Retired) lightclient.Interface {\n\tfor i := range l.convFrom {\n\t\tif l.convFrom[i] == from \u0026\u0026 l.convTo[i] == to {\n\t\t\treturn l.convert[i]\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc init(cur realm) {\n\tcore.RegisterImpl(cross(cur), New, version, []string{\"\", \"v0\"}) // as CheckInstall\n}\n\nfunc (l *logic) Version() string {\n\treturn l.ver\n}\n\n// OnInstall has nothing to migrate: v1 keeps v0's store layout. It installs\n// over v0 or nothing (bootstrap), and re-installs over itself; anything else\n// is refused.\nfunc (l *logic) OnInstall(_ int, rlm realm, prevPath, prevVersion string) {\n\tif err := l.CheckInstall(prevPath, prevVersion); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// CheckInstall is why v1 refuses to install: it installs over v0, nothing or\n// itself, as its manifest declares.\nfunc (l *logic) CheckInstall(prevPath, prevVersion string) error {\n\tswitch prevVersion {\n\tcase \"\", \"v0\", version, l.ver:\n\t\treturn nil\n\t}\n\treturn ufmt.Errorf(\"core impl %s cannot install over version %s\", version, prevVersion)\n}\n\n// forkMigrator is a verifier whose fork schedule a governed migration can\n// change: ethereum/v0's verifier, and any later version that embeds it or\n// implements the same.\ntype forkMigrator interface {\n\tCheckForkParameters(fp ethereum.ForkParameters) error\n\tMigrateForkParameters(fp ethereum.ForkParameters) error\n}\n\n// migration resolves a migration's subject and parameters, or why it cannot\n// apply.\nfunc (l *logic) migration(clientID, migration string, params []byte) (forkMigrator, ethereum.ForkParameters, error) {\n\tc := l.store.Client(clientID)\n\tif c == nil {\n\t\treturn nil, ethereum.ForkParameters{}, ufmt.Errorf(\"client %s not found\", clientID)\n\t}\n\tif migration != MigrationEthereumForkSchedule {\n\t\treturn nil, ethereum.ForkParameters{}, ufmt.Errorf(\"unknown client migration %q\", migration)\n\t}\n\tif c.Type() != ethereum.ClientType {\n\t\treturn nil, ethereum.ForkParameters{}, ufmt.Errorf(\"client %s is not an Ethereum client\", clientID)\n\t}\n\tlc, ok := c.LightClient().(forkMigrator)\n\tif !ok {\n\t\treturn nil, ethereum.ForkParameters{}, ufmt.Errorf(\"client %s's verifier (%s) does not support a fork-schedule migration\", clientID, core.VerifierVersion(clientID))\n\t}\n\tfp, err := ethereum.DecodeForkParameters(params)\n\tif err != nil {\n\t\treturn nil, ethereum.ForkParameters{}, err\n\t}\n\treturn lc, fp, nil\n}\n\n// CheckMigrateClient is why MigrateClient would fail now, or nil, with no\n// change (core.ClientMigrationChecker). A verifier replacement is checked from\n// its registration's declared values and the client's state alone: no\n// conversion runs.\nfunc (l *logic) CheckMigrateClient(clientID, migration string, params []byte) error {\n\tif migration == MigrationEthereumVerifier {\n\t\treturn l.checkConversion(clientID, string(params))\n\t}\n\tlc, fp, err := l.migration(clientID, migration, params)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn lc.CheckForkParameters(fp)\n}\n\n// MigrateClient implements core.ClientMigrator. The proxy has checked that\n// ibcgov decided it and that the client is not retired. A refusal is\n// returned with nothing changed, so the governed decision is spent.\nfunc (l *logic) MigrateClient(_ int, rlm realm, clientID, migration string, params []byte) error {\n\tif migration == MigrationEthereumVerifier {\n\t\treturn l.replaceVerifier(0, rlm, clientID, string(params))\n\t}\n\tlc, fp, err := l.migration(clientID, migration, params)\n\tif err != nil {\n\t\treturn err\n\t}\n\t// MigrateForkParameters checks before it writes: a refusal changes\n\t// nothing. lc is the proxy's verifier: the /p/ method writes under the\n\t// proxy.\n\tif err := lc.MigrateForkParameters(fp); err != nil {\n\t\treturn err\n\t}\n\tcore.EmitMigrateClient(0, rlm, clientID, ethereum.ClientType, migration)\n\treturn nil\n}\n\n// checkConversion is why clientID's verifier cannot be converted to version\n// to now, or nil, from plain values alone (this implementation's declared\n// conversion, the client's type, status and verifier version): nothing is\n// converted.\nfunc (l *logic) checkConversion(clientID, to string) error {\n\tc := l.store.Client(clientID)\n\tswitch {\n\tcase c == nil:\n\t\treturn ufmt.Errorf(\"client %s not found\", clientID)\n\tcase c.Type() != ethereum.ClientType:\n\t\treturn ufmt.Errorf(\"client %s is not an Ethereum client\", clientID)\n\t}\n\tfrom := core.VerifierVersion(clientID)\n\tif l.conversion(from, to) == nil {\n\t\treturn ufmt.Errorf(\"core impl %s converts no %s verifier to %s\", l.ver, from, to)\n\t}\n\t// An Expired client is converted too, keeping its status (ADR pr6375\n\t// R16): a fork or a verifier defect may stop its updates long enough for\n\t// it to expire before the conversion lands, and an ordinary governed\n\t// recovery then follows. A Frozen client is not: its misbehaviour is\n\t// resolved by a recovery first.\n\tif st := c.LightClient().Status(); st != lightclient.Active \u0026\u0026 st != lightclient.Expired {\n\t\treturn ufmt.Errorf(\"client %s is %s, not Active or Expired\", clientID, st)\n\t}\n\t// The conversion reads the live verifier through its ethereum/v0 state:\n\t// a verifier keeping its state otherwise needs migration support of its\n\t// own.\n\tif _, ok := c.LightClient().(ethState); !ok {\n\t\treturn ufmt.Errorf(\"client %s's verifier (%s) exposes no ethereum/v0 state to convert\", clientID, from)\n\t}\n\treturn nil\n}\n\n// replaceVerifier converts clientID's verifier to version to with this\n// implementation's conversion and installs the result. The conversion and\n// every check on its result (it names version to, keeps the live verifier's\n// status, Active or Expired, and its latest height and, for a verifier\n// exposing ethereum/v0 state, the slot from which it trusts consensus states)\n// run inside one recovered boundary in the core's realm: a\n// panic or a mismatch is a refusal, with nothing changed. Only then is the\n// new verifier installed.\nfunc (l *logic) replaceVerifier(_ int, rlm realm, clientID, to string) error {\n\tif err := l.checkConversion(clientID, to); err != nil {\n\t\treturn err\n\t}\n\tc := l.store.Client(clientID)\n\tlive := c.LightClient()\n\tes, ok := live.(ethState)\n\tif !ok {\n\t\treturn ufmt.Errorf(\"client %s's verifier keeps no ethereum/v0 state to convert\", clientID)\n\t}\n\t// The conversion sees the live verifier only through a read-only view.\n\tcand, err := convertAndCheck(l.conversion(core.VerifierVersion(clientID), to), ethereum.Retire(es.Ethereum()), to, live.Status(), live.LatestHeight(), es.Ethereum().TrustedFromSlot)\n\tif err != nil {\n\t\treturn err\n\t}\n\tc.SetLightClient(0, rlm, func(lightclient.Interface) lightclient.Interface { return cand })\n\tcore.EmitMigrateClient(0, rlm, clientID, c.Type(), MigrationEthereumVerifier+\" \"+to)\n\treturn nil\n}\n\n// ethState is a verifier exposing ethereum/v0 state: the v0 verifier, or a\n// later version wrapping it.\ntype ethState interface {\n\tEthereum() *ethereum.EthLightClient\n}\n\n// convertAndCheck runs the conversion on a read-only view of the live\n// verifier and checks the result, any panic on the way returned as a\n// refusal: the view gives the conversion nothing to write with, so a\n// refused conversion leaves nothing half-written.\nfunc convertAndCheck(convert func(*ethereum.Retired) lightclient.Interface, view *ethereum.Retired, want string, status lightclient.Status, height types.Height, trustedFrom uint64) (cand lightclient.Interface, err error) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tcand, err = nil, ufmt.Errorf(\"the converted verifier failed its checks: %v\", r)\n\t\t}\n\t}()\n\tcand = convert(view)\n\tif cand == nil {\n\t\treturn nil, errors.New(\"the conversion returned no verifier\")\n\t}\n\tv, ok := cand.(core.VersionedVerifier)\n\tif !ok || v.VerifierVersion() != want {\n\t\tgot := \"\"\n\t\tif ok {\n\t\t\tgot = v.VerifierVersion()\n\t\t}\n\t\treturn nil, errors.New(\"the converted verifier is \" + got + \", not \" + want)\n\t}\n\tif st := cand.Status(); st != status {\n\t\treturn nil, ufmt.Errorf(\"the converted verifier is %s, not %s\", st, status)\n\t}\n\tif h := cand.LatestHeight(); !h.EQ(height) {\n\t\treturn nil, ufmt.Errorf(\"the converted verifier's latest height is %s, not %s\", h, height)\n\t}\n\tif es, ok := cand.(ethState); ok \u0026\u0026 es.Ethereum().TrustedFromSlot != trustedFrom {\n\t\treturn nil, ufmt.Errorf(\"the converted verifier trusts consensus states from slot %d, not %d\", es.Ethereum().TrustedFromSlot, trustedFrom)\n\t}\n\treturn cand, nil\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"1rzWnq2fG+SeM0rfbtddlr56eo6kLW8F3OcxbobciyQNks8c8EQyWUaqwG0M7iGENJmFdsxN6V9gvx+zs2Pptw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","package":{"name":"ethrelay","path":"gno.land/r/aib/ibc/ethrelay","files":[{"name":"ethrelay.gno","body":"// Package ethrelay is the MsgCall entry point for relaying between gno.land\n// and Ethereum. Each function takes its message as a []byte argument in the\n// wire format (gno.land/p/aib/encoding/wire), which the VM base64-decodes\n// natively, decodes it and forwards it to the IBC core. Relaying through\n// MsgRun instead would charge preprocessing gas for every byte of the\n// message rendered as Gno source.\n//\n// The core authorizes relayers by the transaction signer, which this realm\n// does not change; it adds no authority of its own.\n//\n// Packet messages (RecvPacket, Acknowledgement, Timeout) are, in order:\n//\n//\tUint sequence, Str source_client, Str destination_client, Uint timeout,\n//\tlist\u003cPayload\u003e, Uint proof_revision_number, Uint proof_revision_height,\n//\t[Acknowledgement only: list\u003cblob\u003e app_acknowledgements],\n//\tblob proof (a wire-encoded ethereum.MembershipProof)\n//\tPayload: Str source_port, Str destination_port, Str version, Str encoding, blob value\n//\n// where a list is a Uvarint count followed by its elements and a blob a\n// Uvarint length followed by its bytes.\npackage ethrelay\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/aib/encoding/wire/v0\"\n\t\"gno.land/p/aib/ibc/lightclient/ethereum/v0\"\n\t\"gno.land/p/aib/ibc/lightclient/extproof/v0\"\n\t\"gno.land/p/aib/ibc/types/v0\"\n\t\"gno.land/r/aib/ibc/core\"\n)\n\n// UpdateClient updates the Ethereum client clientID with a wire-encoded\n// ethereum.Header.\nfunc UpdateClient(cur realm, clientID string, header []byte) {\n\th, err := ethereum.DecodeHeader(header)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tcore.UpdateClient(cross(cur), clientID, h)\n}\n\n// SubmitMisbehaviour submits a wire-encoded ethereum.Misbehaviour.\nfunc SubmitMisbehaviour(cur realm, clientID string, misbehaviour []byte) {\n\tm, err := ethereum.DecodeMisbehaviour(misbehaviour)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tcore.UpdateClient(cross(cur), clientID, m)\n}\n\n// RecvPacket delivers a packet sent on Ethereum.\nfunc RecvPacket(cur realm, msg []byte) types.ResponseResultType {\n\td := wire.NewReader(msg)\n\tpacket, height := readPacket(d)\n\tproof := readProof(d)\n\treturn core.RecvPacket(cross(cur), types.MsgRecvPacket{\n\t\tPacket: packet, ProofHeight: height, Ext: extproof.Proof{Data: proof},\n\t})\n}\n\n// Acknowledgement delivers the acknowledgement Ethereum wrote for a packet\n// sent from gno.land.\nfunc Acknowledgement(cur realm, msg []byte) types.ResponseResultType {\n\td := wire.NewReader(msg)\n\tpacket, height := readPacket(d)\n\tacks := readBlobs(d)\n\tproof := readProof(d)\n\treturn core.Acknowledgement(cross(cur), types.MsgAcknowledgement{\n\t\tPacket:          packet,\n\t\tAcknowledgement: types.Acknowledgement{AppAcknowledgements: acks},\n\t\tProofHeight:     height,\n\t\tExt:             extproof.Proof{Data: proof},\n\t})\n}\n\n// Timeout times out a packet sent from gno.land that Ethereum never received.\nfunc Timeout(cur realm, msg []byte) types.ResponseResultType {\n\td := wire.NewReader(msg)\n\tpacket, height := readPacket(d)\n\tproof := readProof(d)\n\treturn core.Timeout(cross(cur), types.MsgTimeout{\n\t\tPacket: packet, ProofHeight: height, Ext: extproof.Proof{Data: proof},\n\t})\n}\n\nvar (\n\terrListLength = errors.New(\"ethrelay: list longer than the message\")\n\terrTrailing   = errors.New(\"ethrelay: trailing bytes\")\n)\n\n// count reads a list length no larger than the remaining bytes, each element\n// taking at least one.\nfunc count(d *wire.Reader) int {\n\tn := d.Uvarint()\n\tif d.Err() != nil {\n\t\tpanic(d.Err())\n\t}\n\tif n \u003e uint64(d.Remaining()) {\n\t\tpanic(errListLength)\n\t}\n\treturn int(n)\n}\n\nfunc readBlobs(d *wire.Reader) [][]byte {\n\tout := make([][]byte, count(d))\n\tfor i := range out {\n\t\tout[i] = d.Blob()\n\t}\n\treturn out\n}\n\nfunc readPacket(d *wire.Reader) (types.Packet, types.Height) {\n\tp := types.Packet{\n\t\tSequence:          d.Uint(),\n\t\tSourceClient:      d.Str(),\n\t\tDestinationClient: d.Str(),\n\t\tTimeoutTimestamp:  d.Uint(),\n\t}\n\tp.Payloads = make([]types.Payload, count(d))\n\tfor i := range p.Payloads {\n\t\tp.Payloads[i] = types.Payload{\n\t\t\tSourcePort: d.Str(), DestinationPort: d.Str(), Version: d.Str(),\n\t\t\tEncoding: d.Str(), Value: d.Blob(),\n\t\t}\n\t}\n\th := types.NewHeight(d.Uint(), d.Uint())\n\tif d.Err() != nil {\n\t\tpanic(d.Err())\n\t}\n\treturn p, h\n}\n\n// readProof reads the message's last field, the MembershipProof.\nfunc readProof(d *wire.Reader) ethereum.MembershipProof {\n\tbz := d.Blob()\n\tif d.Err() != nil {\n\t\tpanic(d.Err())\n\t}\n\tif !d.Done() {\n\t\tpanic(errTrailing)\n\t}\n\tp, err := ethereum.DecodeMembershipProof(bz)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn p\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/aib/ibc/ethrelay\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"lKIh3vgi65PH2sb8EDYowYvbKoodrCcgAO1lft1STO915PS1jg2jga9av5303bLLr8MLfIEjFjNMFUBrDu4ypw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_run","caller":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","send":"","max_deposit":"","package":{"name":"main","path":"","files":[{"name":"govdao_bootstrap.gno","body":"// govdao_bootstrap.gno runs as a MsgRun at genesis (after r/gov/dao/loader/v0\n// initialised the DAO and its tiers with an empty AllowedDAOs): it seats the\n// three generated GovDAO T1 members and locks dao.UpdateImpl's AllowedDAOs to\n// r/gov/dao/impl/v0, as the mainnet bootstrap does. Three T1 members at power 3\n// each: two YES votes reach the 66.66% supermajority.\npackage main\n\nimport (\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/impl/v0\"\n\t\"gno.land/r/gov/dao/memberstore/v0\"\n)\n\nfunc main(cur realm) {\n\tms := memberstore.Get(0, cur)\n\tmust(ms.SetMember(memberstore.T1, address(\"g1vk6zdqj9kg2lhth8rqdf3des2ucpp76m4lzq9q\"), memberstore.NewMember(3)))\n\tmust(ms.SetMember(memberstore.T1, address(\"g1q392jxrvawgfe3q5tu585ap5j99frzkhcqzsu0\"), memberstore.NewMember(3)))\n\tmust(ms.SetMember(memberstore.T1, address(\"g16pcnylx43k0rp272jnhzyp490acmns24qrgua6\"), memberstore.NewMember(3)))\n\tdao.UpdateImpl(cross(cur), dao.NewUpdateRequest(impl.NewGovDAO(), []string{\"gno.land/r/gov/dao/impl/v0\"}))\n}\n\nfunc must(err error) {\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n}\n"}]}}],"fee":{"gas_wanted":"100000000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"ud9X3oBx5eI1UridAvS1Ssg6pl50PQ9NtyJ2UKYyEJV2Bjujhy86GFrnwjKLRUoxr5Fs0BpZ/W4Y/oqzlhyvzw=="}],"memo":""},"metadata":{"timestamp":"0"}},{"tx":{"msg":[{"@type":"/vm.m_run","caller":"g1fkqgqtwc6sevh7lu5d2fukt2stsxa7ja30qpcx","send":"","max_deposit":"","package":{"name":"main","path":"","files":[{"name":"users_preregister.gno","body":"// users_preregister.gno runs as a MsgRun at genesis: it registers the `aib`\n// namespace (the IBC realms' namespace) to the governor key through the\n// genesis-only r/sys/users/init path, so the team can deploy implementation\n// candidates under gno.land/r/aib/... once namespace enforcement is on.\npackage main\n\nimport (\n\tusersinit \"gno.land/r/sys/users/init\"\n)\n\nfunc main(cur realm) {\n\tusersinit.RegisterUser(cross(cur), \"aib\", address(\"g1kg6w7a4f0kup8wj9y33jdkqvvux7n559avyzcs\"))\n}\n"}]}}],"fee":{"gas_wanted":"100000000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"ovHeuC5TIQQUzg4YCl5zMoJru1LMnp+wolvVdSnZe+s0JLSQ7wC+bchfUPwgBJiHGTYFou28leBul2Zikx7fJg=="}],"memo":""},"metadata":{"timestamp":"0"}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx","send":"","max_deposit":"","pkg_path":"gno.land/r/sys/names","func":"Enable"}],"fee":{"gas_wanted":"100000000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A1FpMslSqTGeWWmWtvrGb+Ys+3uGMazrqytw/MpRfpRt"},"signature":"uBWz8pqr4iRQI0s5+cUmNZRWuOSpoOWZA/aPrR4CjcVkKC7fOeaX3dCzTfVkSDNLyl2wX1ANNY4cTSYumJJ79w=="}],"memo":""},"metadata":{"timestamp":"0"}}]}}}}